diff --git a/.gitignore b/.gitignore index 259148f..d17030c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,24 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo +Build +Build.bat +/build/ +MYMETA.json +MYMETA.yml +_build +blib +xs/buildtmp *.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app +MANIFEST.bak +xs/MANIFEST.bak +xs/assertlib* +.init_bundle.ini +.vs/* +local-lib +/src/TAGS +/.vscode/ +build-linux/* +deps/build* +deps/build-linux/* +**/.DS_Store +**/.idea/ +.pkg_cache +CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 94239ba..354e264 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ option(SLIC3R_MSVC_PDB "Generate PDB files on MSVC in Release mode" 1) option(SLIC3R_PERL_XS "Compile XS Perl module and enable Perl unit and integration tests" 0) option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0) option(SLIC3R_UBSAN "Enable UBSan on Clang and GCC" 0) -option(SLIC3R_ENABLE_FORMAT_STEP "Enable compilation of STEP file support" 1) +option(SLIC3R_ENABLE_FORMAT_STEP "Enable compilation of STEP file support" ON) # If SLIC3R_FHS is 1 -> SLIC3R_DESKTOP_INTEGRATION is always 0, othrewise variable. CMAKE_DEPENDENT_OPTION(SLIC3R_DESKTOP_INTEGRATION "Allow perfoming desktop integration during runtime" 1 "NOT SLIC3R_FHS" 0) @@ -50,6 +50,14 @@ if (SLIC3R_STATIC) set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) endif () +# Dependency build management +option(${PROJECT_NAME}_BUILD_DEPS "Build dependencies before the project" OFF) +option(${PROJECT_NAME}_DEPS_OUTPUT_QUIET "Don't print build output for dependencies" OFF) +set(${PROJECT_NAME}_DEPS_PRESET "default" CACHE STRING "Preset of the dependencies when ${PROJECT_NAME}_BUILD_DEPS is ON") +set(${PROJECT_NAME}_DEPS_BUILD_DIR "" CACHE PATH "Binary dir of the dependencies build when ${PROJECT_NAME}_BUILD_DEPS is ON") +if (${PROJECT_NAME}_BUILD_DEPS) + include(deps/autobuild.cmake) +endif () if (APPLE) set(CMAKE_FIND_FRAMEWORK LAST) set(CMAKE_FIND_APPBUNDLE LAST) @@ -175,7 +183,7 @@ if(WIN32) else() message("WIN10SDK_PATH is invalid: ${WIN10SDK_PATH}") message("${WIN10SDK_PATH}/include/winrt/windows.graphics.printing3d.h was not found") - message("STL fixing by the Netfabb service will not be compiled") + message("STL fixing by WinSDK will not be compiled") unset(WIN10SDK_PATH) endif() else() @@ -183,16 +191,16 @@ if(WIN32) set(WIN10SDK_INCLUDE_PATH "$ENV{WindowsSdkDir}/Include/$ENV{WindowsSDKVersion}") if (NOT EXISTS "${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h") message("${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h was not found") - message("STL fixing by the Netfabb service will not be compiled") + message("STL fixing by WinSDK will not be compiled") unset(WIN10SDK_INCLUDE_PATH) endif() endif() if(WIN10SDK_INCLUDE_PATH) - message("Building with Win10 Netfabb STL fixing service support") + message("Building with Win10 STL fixing service support") add_definitions(-DHAS_WIN10SDK) include_directories("${WIN10SDK_INCLUDE_PATH}") else() - message("Building without Win10 Netfabb STL fixing service support") + message("Building without Win10 STL fixing service support") endif() endif() @@ -582,17 +590,12 @@ function(qidislicer_copy_dlls target) # This has to be a separate target due to the windows command line lenght limits add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${TOP_LEVEL_PROJECT_DIR}/deps/GMP/gmp/lib/win${_bits}/libgmp-10.dll ${_out_dir} + COMMAND ${CMAKE_COMMAND} -E copy ${TOP_LEVEL_PROJECT_DIR}/deps/+GMP/gmp/lib/win${_bits}/libgmp-10.dll ${_out_dir} COMMENT "Copy gmp runtime to build tree" VERBATIM) add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${TOP_LEVEL_PROJECT_DIR}/deps/MPFR/mpfr/lib/win${_bits}/libmpfr-4.dll ${_out_dir} - COMMENT "Copy mpfr runtime to build tree" - VERBATIM) - - add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy ${TOP_LEVEL_PROJECT_DIR}/deps/MPFR/mpfr/lib/win${_bits}/WebView2Loader.dll ${_out_dir} + COMMAND ${CMAKE_COMMAND} -E copy ${TOP_LEVEL_PROJECT_DIR}/deps/+MPFR/mpfr/lib/win${_bits}/libmpfr-4.dll ${_out_dir} COMMENT "Copy mpfr runtime to build tree" VERBATIM) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..766053c --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,46 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "default", + "displayName": "Default Config", + "description": "Building with statically linked dependencies", + "binaryDir": "${sourceDir}/build-default", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "SLIC3R_STATIC": true, + "SLIC3R_GTK": "3", + "SLIC3R_ENC_CHECK": false, + "SLIC3R_PCH": true, + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build-default/dist", + "PrusaSlicer_DEPS_PRESET": "default", + "PrusaSlicer_DEPS_OUTPUT_QUIET": false + } + }, + { + "name": "no-occt", + "displayName": "Without STEP", + "description": "Building with statically linked dependencies without STEP file support", + "inherits": "default", + "binaryDir": "${sourceDir}/build-no-occt", + "cacheVariables": { + "SLIC3R_ENABLE_FORMAT_STEP": false, + "QIDISlicer_DEPS_PRESET": "no-occt" + } + }, + { + "name": "shareddeps", + "displayName": "Shared dependencies", + "description": "Building with dynamically linked dependencies from the system", + "binaryDir": "${sourceDir}/shareddeps", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "SLIC3R_STATIC": false, + "SLIC3R_GTK": "3", + "SLIC3R_ENC_CHECK": false, + "SLIC3R_PCH": true, + "QIDISlicer_BUILD_DEPS": false + } + } + ] +} \ No newline at end of file diff --git a/build_win.bat b/build_win.bat index 0afdfb3..e5c188c 100644 --- a/build_win.bat +++ b/build_win.bat @@ -63,10 +63,14 @@ SET PS_DEPS_PATH_FILE_NAME=.DEPS_PATH.txt SET PS_DEPS_PATH_FILE=%~dp0deps\build\%PS_DEPS_PATH_FILE_NAME% SET PS_CONFIG_LIST="Debug;MinSizeRel;Release;RelWithDebInfo" -REM The officially supported toolchain version is 16 (Visual Studio 2019) -REM TODO: Update versions after Boost gets rolled to 1.78 or later +REM Update this script for new versions by setting PS_VERSION_SUPPORTED to a +REM new minimum version and setting PS_VERSION_EXCEEDED to the maximum supported +REM version plus one. +REM The officially supported toolchain versions are: +REM Minimum: 16 (Visual Studio 2019) +REM Maximum: 17 (Visual Studio 2022) SET PS_VERSION_SUPPORTED=16 -SET PS_VERSION_EXCEEDED=17 +SET PS_VERSION_EXCEEDED=18 SET VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe IF NOT EXIST "%VSWHERE%" SET VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe FOR /F "tokens=4 USEBACKQ delims=." %%I IN (`"%VSWHERE%" -nologo -property productId`) DO SET PS_PRODUCT_DEFAULT=%%I diff --git a/cmake/modules/AddCMakeProject.cmake b/cmake/modules/AddCMakeProject.cmake new file mode 100644 index 0000000..c2cbad6 --- /dev/null +++ b/cmake/modules/AddCMakeProject.cmake @@ -0,0 +1,79 @@ +include(ExternalProject) +include(ProcessorCount) + +set(${PROJECT_NAME}_DEP_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/destdir/usr/local" CACHE PATH "Destination directory") +set(${PROJECT_NAME}_DEP_DOWNLOAD_DIR ${CMAKE_CURRENT_BINARY_DIR}/downloads CACHE PATH "Path for downloaded source packages.") +option(${PROJECT_NAME}_DEP_BUILD_VERBOSE "Use verbose output for each dependency build" OFF) + +get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) + message(STATUS "Forcing CMAKE_BUILD_TYPE to Release as it was not specified.") +endif () + +# The value of CMAKE_BUILD_TYPE will be used for building each dependency even if the +# generator is multi-config. Use this var to specify build type regardless of the generator. +function(add_cmake_project projectname) + cmake_parse_arguments(P_ARGS "" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN}) + + set(_pcount ${DEP_${projectname}_MAX_THREADS}) + + if (NOT _pcount) + set(_pcount ${DEP_MAX_THREADS}) + endif () + + if (NOT _pcount) + ProcessorCount(_pcount) + endif () + + if (_pcount EQUAL 0) + set(_pcount 1) + endif () + + set(_build_j "-j${_pcount}") + if (CMAKE_GENERATOR MATCHES "Visual Studio") + set(_build_j "-m:${_pcount}") + endif () + + string(TOUPPER "${CMAKE_BUILD_TYPE}" _build_type_upper) + set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}) + if (_is_multi) + set(_configs_line "") + endif () + + set(_verbose_switch "") + if (${PROJECT_NAME}_DEP_BUILD_VERBOSE) + if (CMAKE_GENERATOR MATCHES "Ninja") + set(_verbose_switch "--verbose") + elseif (CMAKE_GENERATOR MATCHES "Visual Studio") + set(_verbose_switch "-v:d") + endif () + endif () + + ExternalProject_Add( + dep_${projectname} + EXCLUDE_FROM_ALL ON # Not built by default, dep_${projectname} needs to be added to ALL target + INSTALL_DIR ${${PROJECT_NAME}_DEP_INSTALL_PREFIX} + DOWNLOAD_DIR ${${PROJECT_NAME}_DEP_DOWNLOAD_DIR}/${projectname} + BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/builds/${projectname} + CMAKE_ARGS + -DCMAKE_INSTALL_PREFIX:STRING=${${PROJECT_NAME}_DEP_INSTALL_PREFIX} + -DCMAKE_MODULE_PATH:STRING=${CMAKE_MODULE_PATH} + -DCMAKE_PREFIX_PATH:STRING=${${PROJECT_NAME}_DEP_INSTALL_PREFIX} + -DCMAKE_DEBUG_POSTFIX:STRING=${CMAKE_DEBUG_POSTFIX} + -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER} + -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} + -DCMAKE_CXX_FLAGS_${_build_type_upper}:STRING=${CMAKE_CXX_FLAGS_${_build_type_upper}} + -DCMAKE_C_FLAGS_${_build_type_upper}:STRING=${CMAKE_C_FLAGS_${_build_type_upper}} + -DCMAKE_TOOLCHAIN_FILE:STRING=${CMAKE_TOOLCHAIN_FILE} + -DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS} + "${_configs_line}" + ${DEP_CMAKE_OPTS} + ${P_ARGS_CMAKE_ARGS} + ${P_ARGS_UNPARSED_ARGUMENTS} + BUILD_COMMAND ${CMAKE_COMMAND} --build . --config ${CMAKE_BUILD_TYPE} -- ${_build_j} ${_verbose_switch} + INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config ${CMAKE_BUILD_TYPE} + ) + +endfunction(add_cmake_project) diff --git a/cmake/modules/Catch2/Catch.cmake b/cmake/modules/Catch2/Catch.cmake deleted file mode 100644 index 0ffe978..0000000 --- a/cmake/modules/Catch2/Catch.cmake +++ /dev/null @@ -1,175 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -#[=======================================================================[.rst: -Catch ------ - -This module defines a function to help use the Catch test framework. - -The :command:`catch_discover_tests` discovers tests by asking the compiled test -executable to enumerate its tests. This does not require CMake to be re-run -when tests change. However, it may not work in a cross-compiling environment, -and setting test properties is less convenient. - -This command is intended to replace use of :command:`add_test` to register -tests, and will create a separate CTest test for each Catch test case. Note -that this is in some cases less efficient, as common set-up and tear-down logic -cannot be shared by multiple test cases executing in the same instance. -However, it provides more fine-grained pass/fail information to CTest, which is -usually considered as more beneficial. By default, the CTest test name is the -same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. - -.. command:: catch_discover_tests - - Automatically add tests with CTest by querying the compiled test executable - for available tests:: - - catch_discover_tests(target - [TEST_SPEC arg1...] - [EXTRA_ARGS arg1...] - [WORKING_DIRECTORY dir] - [TEST_PREFIX prefix] - [TEST_SUFFIX suffix] - [PROPERTIES name1 value1...] - [TEST_LIST var] - ) - - ``catch_discover_tests`` sets up a post-build command on the test executable - that generates the list of tests by parsing the output from running the test - with the ``--list-test-names-only`` argument. This ensures that the full - list of tests is obtained. Since test discovery occurs at build time, it is - not necessary to re-run CMake when the list of tests changes. - However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set - in order to function in a cross-compiling environment. - - Additionally, setting properties on tests is somewhat less convenient, since - the tests are not available at CMake time. Additional test properties may be - assigned to the set of tests as a whole using the ``PROPERTIES`` option. If - more fine-grained test control is needed, custom content may be provided - through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES` - directory property. The set of discovered tests is made accessible to such a - script via the ``_TESTS`` variable. - - The options are: - - ``target`` - Specifies the Catch executable, which must be a known CMake executable - target. CMake will substitute the location of the built executable when - running the test. - - ``TEST_SPEC arg1...`` - Specifies test cases, wildcarded test cases, tags and tag expressions to - pass to the Catch executable with the ``--list-test-names-only`` argument. - - ``EXTRA_ARGS arg1...`` - Any extra arguments to pass on the command line to each test case. - - ``WORKING_DIRECTORY dir`` - Specifies the directory in which to run the discovered test cases. If this - option is not provided, the current binary directory is used. - - ``TEST_PREFIX prefix`` - Specifies a ``prefix`` to be prepended to the name of each discovered test - case. This can be useful when the same test executable is being used in - multiple calls to ``catch_discover_tests()`` but with different - ``TEST_SPEC`` or ``EXTRA_ARGS``. - - ``TEST_SUFFIX suffix`` - Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of - every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may - be specified. - - ``PROPERTIES name1 value1...`` - Specifies additional properties to be set on all tests discovered by this - invocation of ``catch_discover_tests``. - - ``TEST_LIST var`` - Make the list of tests available in the variable ``var``, rather than the - default ``_TESTS``. This can be useful when the same test - executable is being used in multiple calls to ``catch_discover_tests()``. - Note that this variable is only available in CTest. - -#]=======================================================================] - -#------------------------------------------------------------------------------ -function(catch_discover_tests TARGET) - cmake_parse_arguments( - "" - "" - "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST" - "TEST_SPEC;EXTRA_ARGS;PROPERTIES" - ${ARGN} - ) - - if(NOT _WORKING_DIRECTORY) - set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") - endif() - if(NOT _TEST_LIST) - set(_TEST_LIST ${TARGET}_TESTS) - endif() - - ## Generate a unique name based on the extra arguments - string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS}") - string(SUBSTRING ${args_hash} 0 7 args_hash) - - # Define rule to generate test list for aforementioned test executable - set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake") - set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake") - get_property(crosscompiling_emulator - TARGET ${TARGET} - PROPERTY CROSSCOMPILING_EMULATOR - ) - add_custom_command( - TARGET ${TARGET} POST_BUILD - BYPRODUCTS "${ctest_tests_file}" - COMMAND "${CMAKE_COMMAND}" - -D "TEST_TARGET=${TARGET}" - -D "TEST_EXECUTABLE=$" - -D "TEST_EXECUTOR=${crosscompiling_emulator}" - -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" - -D "TEST_SPEC=${_TEST_SPEC}" - -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" - -D "TEST_PROPERTIES=${_PROPERTIES}" - -D "TEST_PREFIX='${_TEST_PREFIX}'" - -D "TEST_SUFFIX='${_TEST_SUFFIX}'" - -D "TEST_LIST=${_TEST_LIST}" - -D "CTEST_FILE=${ctest_tests_file}" - -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" - VERBATIM - ) - - file(WRITE "${ctest_include_file}" - "if(EXISTS \"${ctest_tests_file}\")\n" - " include(\"${ctest_tests_file}\")\n" - "else()\n" - " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" - "endif()\n" - ) - - if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0") - # Add discovered tests to directory TEST_INCLUDE_FILES - set_property(DIRECTORY - APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" - ) - else() - # Add discovered tests as directory TEST_INCLUDE_FILE if possible - get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) - if (NOT ${test_include_file_set}) - set_property(DIRECTORY - PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" - ) - else() - message(FATAL_ERROR - "Cannot set more than one TEST_INCLUDE_FILE" - ) - endif() - endif() - -endfunction() - -############################################################################### - -set(_CATCH_DISCOVER_TESTS_SCRIPT - ${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake -) diff --git a/cmake/modules/Catch2/CatchAddTests.cmake b/cmake/modules/Catch2/CatchAddTests.cmake deleted file mode 100644 index ca5ebc1..0000000 --- a/cmake/modules/Catch2/CatchAddTests.cmake +++ /dev/null @@ -1,106 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -set(prefix "${TEST_PREFIX}") -set(suffix "${TEST_SUFFIX}") -set(spec ${TEST_SPEC}) -set(extra_args ${TEST_EXTRA_ARGS}) -set(properties ${TEST_PROPERTIES}) -set(script) -set(suite) -set(tests) - -function(add_command NAME) - set(_args "") - foreach(_arg ${ARGN}) - if(_arg MATCHES "[^-./:a-zA-Z0-9_]") - set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument - else() - set(_args "${_args} ${_arg}") - endif() - endforeach() - set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) -endfunction() - -macro(_add_catch_test_labels LINE) - # convert to list of tags - string(REPLACE "][" "]\\;[" tags ${line}) - - add_command( - set_tests_properties "${prefix}${test}${suffix}" - PROPERTIES - LABELS "${tags}" - ) -endmacro() - -macro(_add_catch_test LINE) - set(test ${line}) - # use escape commas to handle properly test cases with commans inside the name - string(REPLACE "," "\\," test_name ${test}) - # ...and add to script - add_command( - add_test "${prefix}${test}${suffix}" - ${TEST_EXECUTOR} - "${TEST_EXECUTABLE}" - "${test_name}" - ${extra_args} - ) - - add_command( - set_tests_properties "${prefix}${test}${suffix}" - PROPERTIES - WORKING_DIRECTORY "${TEST_WORKING_DIR}" - ${properties} - ) - list(APPEND tests "${prefix}${test}${suffix}") -endmacro() - -# Run test executable to get list of available tests -if(NOT EXISTS "${TEST_EXECUTABLE}") - message(FATAL_ERROR - "Specified test executable '${TEST_EXECUTABLE}' does not exist" - ) -endif() -execute_process( - COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-tests - OUTPUT_VARIABLE output - RESULT_VARIABLE result -) -# Catch --list-test-names-only reports the number of tests, so 0 is... surprising -if(${result} EQUAL 0) - message(WARNING - "Test executable '${TEST_EXECUTABLE}' contains no tests!\n" - ) -elseif(${result} LESS 0) - message(FATAL_ERROR - "Error running test executable '${TEST_EXECUTABLE}':\n" - " Result: ${result}\n" - " Output: ${output}\n" - ) -endif() - -string(REPLACE "\n" ";" output "${output}") -set(test) -set(tags_regex "(\\[([^\\[]*)\\])+$") - -# Parse output -foreach(line ${output}) - # lines without leading whitespaces are catch output not tests - if(${line} MATCHES "^[ \t]+") - # strip leading spaces and tabs - string(REGEX REPLACE "^[ \t]+" "" line ${line}) - - if(${line} MATCHES "${tags_regex}") - _add_catch_test_labels(${line}) - else() - _add_catch_test(${line}) - endif() - endif() -endforeach() - -# Create a list of all discovered tests, which users may use to e.g. set -# properties on the tests -add_command(set ${TEST_LIST} ${tests}) - -# Write CTest script -file(WRITE "${CTEST_FILE}" "${script}") diff --git a/cmake/modules/Catch2/ParseAndAddCatchTests.cmake b/cmake/modules/Catch2/ParseAndAddCatchTests.cmake deleted file mode 100644 index 925d932..0000000 --- a/cmake/modules/Catch2/ParseAndAddCatchTests.cmake +++ /dev/null @@ -1,225 +0,0 @@ -#==================================================================================================# -# supported macros # -# - TEST_CASE, # -# - SCENARIO, # -# - TEST_CASE_METHOD, # -# - CATCH_TEST_CASE, # -# - CATCH_SCENARIO, # -# - CATCH_TEST_CASE_METHOD. # -# # -# Usage # -# 1. make sure this module is in the path or add this otherwise: # -# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # -# 2. make sure that you've enabled testing option for the project by the call: # -# enable_testing() # -# 3. add the lines to the script for testing target (sample CMakeLists.txt): # -# project(testing_target) # -# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # -# enable_testing() # -# # -# find_path(CATCH_INCLUDE_DIR "catch.hpp") # -# include_directories(${INCLUDE_DIRECTORIES} ${CATCH_INCLUDE_DIR}) # -# # -# file(GLOB SOURCE_FILES "*.cpp") # -# add_executable(${PROJECT_NAME} ${SOURCE_FILES}) # -# # -# include(ParseAndAddCatchTests) # -# ParseAndAddCatchTests(${PROJECT_NAME}) # -# # -# The following variables affect the behavior of the script: # -# # -# PARSE_CATCH_TESTS_VERBOSE (Default OFF) # -# -- enables debug messages # -# PARSE_CATCH_TESTS_NO_HIDDEN_TESTS (Default OFF) # -# -- excludes tests marked with [!hide], [.] or [.foo] tags # -# PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME (Default ON) # -# -- adds fixture class name to the test name # -# PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME (Default ON) # -# -- adds cmake target name to the test name # -# PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS (Default OFF) # -# -- causes CMake to rerun when file with tests changes so that new tests will be discovered # -# # -# One can also set (locally) the optional variable OptionalCatchTestLauncher to precise the way # -# a test should be run. For instance to use test MPI, one can write # -# set(OptionalCatchTestLauncher ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${NUMPROC}) # -# just before calling this ParseAndAddCatchTests function # -# # -# The AdditionalCatchParameters optional variable can be used to pass extra argument to the test # -# command. For example, to include successful tests in the output, one can write # -# set(AdditionalCatchParameters --success) # -# # -# After the script, the ParseAndAddCatchTests_TESTS property for the target, and for each source # -# file in the target is set, and contains the list of the tests extracted from that target, or # -# from that file. This is useful, for example to add further labels or properties to the tests. # -# # -#==================================================================================================# - -if (CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.8) - message(FATAL_ERROR "ParseAndAddCatchTests requires CMake 2.8.8 or newer") -endif() - -option(PARSE_CATCH_TESTS_VERBOSE "Print Catch to CTest parser debug messages" OFF) -option(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS "Exclude tests with [!hide], [.] or [.foo] tags" OFF) -option(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME "Add fixture class name to the test name" ON) -option(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME "Add target name to the test name" ON) -option(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS "Add test file to CMAKE_CONFIGURE_DEPENDS property" OFF) - -function(ParseAndAddCatchTests_PrintDebugMessage) - if(PARSE_CATCH_TESTS_VERBOSE) - message(STATUS "ParseAndAddCatchTests: ${ARGV}") - endif() -endfunction() - -# This removes the contents between -# - block comments (i.e. /* ... */) -# - full line comments (i.e. // ... ) -# contents have been read into '${CppCode}'. -# !keep partial line comments -function(ParseAndAddCatchTests_RemoveComments CppCode) - string(ASCII 2 CMakeBeginBlockComment) - string(ASCII 3 CMakeEndBlockComment) - string(REGEX REPLACE "/\\*" "${CMakeBeginBlockComment}" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "\\*/" "${CMakeEndBlockComment}" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "${CMakeBeginBlockComment}[^${CMakeEndBlockComment}]*${CMakeEndBlockComment}" "" ${CppCode} "${${CppCode}}") - string(REGEX REPLACE "\n[ \t]*//+[^\n]+" "\n" ${CppCode} "${${CppCode}}") - - set(${CppCode} "${${CppCode}}" PARENT_SCOPE) -endfunction() - -# Worker function -function(ParseAndAddCatchTests_ParseFile SourceFile TestTarget) - # If SourceFile is an object library, do not scan it (as it is not a file). Exit without giving a warning about a missing file. - if(SourceFile MATCHES "\\\$") - ParseAndAddCatchTests_PrintDebugMessage("Detected OBJECT library: ${SourceFile} this will not be scanned for tests.") - return() - endif() - # According to CMake docs EXISTS behavior is well-defined only for full paths. - get_filename_component(SourceFile ${SourceFile} ABSOLUTE) - if(NOT EXISTS ${SourceFile}) - message(WARNING "Cannot find source file: ${SourceFile}") - return() - endif() - ParseAndAddCatchTests_PrintDebugMessage("parsing ${SourceFile}") - file(STRINGS ${SourceFile} Contents NEWLINE_CONSUME) - - # Remove block and fullline comments - ParseAndAddCatchTests_RemoveComments(Contents) - - # Find definition of test names - string(REGEX MATCHALL "[ \t]*(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^\)]+\\)+[ \t\n]*{+[ \t]*(//[^\n]*[Tt][Ii][Mm][Ee][Oo][Uu][Tt][ \t]*[0-9]+)*" Tests "${Contents}") - - if(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS AND Tests) - ParseAndAddCatchTests_PrintDebugMessage("Adding ${SourceFile} to CMAKE_CONFIGURE_DEPENDS property") - set_property( - DIRECTORY - APPEND - PROPERTY CMAKE_CONFIGURE_DEPENDS ${SourceFile} - ) - endif() - - foreach(TestName ${Tests}) - # Strip newlines - string(REGEX REPLACE "\\\\\n|\n" "" TestName "${TestName}") - - # Get test type and fixture if applicable - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^,^\"]*" TestTypeAndFixture "${TestName}") - string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)" TestType "${TestTypeAndFixture}") - string(REGEX REPLACE "${TestType}\\([ \t]*" "" TestFixture "${TestTypeAndFixture}") - - # Get string parts of test definition - string(REGEX MATCHALL "\"+([^\\^\"]|\\\\\")+\"+" TestStrings "${TestName}") - - # Strip wrapping quotation marks - string(REGEX REPLACE "^\"(.*)\"$" "\\1" TestStrings "${TestStrings}") - string(REPLACE "\";\"" ";" TestStrings "${TestStrings}") - - # Validate that a test name and tags have been provided - list(LENGTH TestStrings TestStringsLength) - if(TestStringsLength GREATER 2 OR TestStringsLength LESS 1) - message(FATAL_ERROR "You must provide a valid test name and tags for all tests in ${SourceFile}") - endif() - - # Assign name and tags - list(GET TestStrings 0 Name) - if("${TestType}" STREQUAL "SCENARIO") - set(Name "Scenario: ${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME AND TestFixture) - set(CTestName "${TestFixture}:${Name}") - else() - set(CTestName "${Name}") - endif() - if(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME) - set(CTestName "${TestTarget}:${CTestName}") - endif() - # add target to labels to enable running all tests added from this target - set(Labels ${TestTarget}) - if(TestStringsLength EQUAL 2) - list(GET TestStrings 1 Tags) - string(TOLOWER "${Tags}" Tags) - # remove target from labels if the test is hidden - if("${Tags}" MATCHES ".*\\[!?(hide|\\.)\\].*") - list(REMOVE_ITEM Labels ${TestTarget}) - endif() - string(REPLACE "]" ";" Tags "${Tags}") - string(REPLACE "[" "" Tags "${Tags}") - else() - # unset tags variable from previous loop - unset(Tags) - endif() - - list(APPEND Labels ${Tags}) - - set(HiddenTagFound OFF) - foreach(label ${Labels}) - string(REGEX MATCH "^!hide|^\\." result ${label}) - if(result) - set(HiddenTagFound ON) - break() - endif(result) - endforeach(label) - if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_LESS "3.9") - ParseAndAddCatchTests_PrintDebugMessage("Skipping test \"${CTestName}\" as it has [!hide], [.] or [.foo] label") - else() - ParseAndAddCatchTests_PrintDebugMessage("Adding test \"${CTestName}\"") - if(Labels) - ParseAndAddCatchTests_PrintDebugMessage("Setting labels to ${Labels}") - endif() - - # Escape commas in the test spec - string(REPLACE "," "\\," Name ${Name}) - - # Add the test and set its properties - add_test(NAME "\"${CTestName}\"" COMMAND ${OptionalCatchTestLauncher} $ ${Name} ${AdditionalCatchParameters}) - # Old CMake versions do not document VERSION_GREATER_EQUAL, so we use VERSION_GREATER with 3.8 instead - if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_GREATER "3.8") - ParseAndAddCatchTests_PrintDebugMessage("Setting DISABLED test property") - set_tests_properties("\"${CTestName}\"" PROPERTIES DISABLED ON) - else() - set_tests_properties("\"${CTestName}\"" PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran" - LABELS "${Labels}") - endif() - set_property( - TARGET ${TestTarget} - APPEND - PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") - set_property( - SOURCE ${SourceFile} - APPEND - PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") - endif() - - - endforeach() -endfunction() - -# entry point -function(ParseAndAddCatchTests TestTarget) - ParseAndAddCatchTests_PrintDebugMessage("Started parsing ${TestTarget}") - get_target_property(SourceFiles ${TestTarget} SOURCES) - ParseAndAddCatchTests_PrintDebugMessage("Found the following sources: ${SourceFiles}") - foreach(SourceFile ${SourceFiles}) - ParseAndAddCatchTests_ParseFile(${SourceFile} ${TestTarget}) - endforeach() - ParseAndAddCatchTests_PrintDebugMessage("Finished parsing ${TestTarget}") -endfunction() diff --git a/cmake/modules/UsewxWidgets.cmake b/cmake/modules/UsewxWidgets.cmake new file mode 100644 index 0000000..4f85484 --- /dev/null +++ b/cmake/modules/UsewxWidgets.cmake @@ -0,0 +1,75 @@ +# PrusaSlicer: this is a direct copy of the UsewxWidgets.cmake module +# within the original CMake 3.27 distribution + +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +UsewxWidgets +------------ + +Convenience include for using wxWidgets library. + +Determines if wxWidgets was FOUND and sets the appropriate libs, +incdirs, flags, etc. INCLUDE_DIRECTORIES and LINK_DIRECTORIES are +called. + +USAGE + +:: + + # Note that for MinGW users the order of libs is important! + find_package(wxWidgets REQUIRED net gl core base) + include(${wxWidgets_USE_FILE}) + # and for each of your dependent executable/library targets: + target_link_libraries( ${wxWidgets_LIBRARIES}) + + + +DEPRECATED + +:: + + LINK_LIBRARIES is not called in favor of adding dependencies per target. + + + +AUTHOR + +:: + + Jan Woetzel +#]=======================================================================] + +if (wxWidgets_FOUND) + if (wxWidgets_INCLUDE_DIRS) + if(wxWidgets_INCLUDE_DIRS_NO_SYSTEM) + include_directories(${wxWidgets_INCLUDE_DIRS}) + else() + include_directories(SYSTEM ${wxWidgets_INCLUDE_DIRS}) + endif() + endif() + + if (wxWidgets_LIBRARY_DIRS) + link_directories(${wxWidgets_LIBRARY_DIRS}) + endif() + + if (wxWidgets_DEFINITIONS) + set_property(DIRECTORY APPEND + PROPERTY COMPILE_DEFINITIONS ${wxWidgets_DEFINITIONS}) + endif() + + if (wxWidgets_DEFINITIONS_DEBUG) + set_property(DIRECTORY APPEND + PROPERTY COMPILE_DEFINITIONS_DEBUG ${wxWidgets_DEFINITIONS_DEBUG}) + endif() + + if (wxWidgets_CXX_FLAGS) + # Flags are expected to be a string here, not a list. + string(REPLACE ";" " " wxWidgets_CXX_FLAGS_str "${wxWidgets_CXX_FLAGS}") + string(APPEND CMAKE_CXX_FLAGS " ${wxWidgets_CXX_FLAGS_str}") + unset(wxWidgets_CXX_FLAGS_str) + endif() +else () + message("wxWidgets requested but not found.") +endif() diff --git a/deps/+Blosc/Blosc.cmake b/deps/+Blosc/Blosc.cmake new file mode 100644 index 0000000..5b1e429 --- /dev/null +++ b/deps/+Blosc/Blosc.cmake @@ -0,0 +1,21 @@ +if(BUILD_SHARED_LIBS) + set(_build_shared ON) + set(_build_static OFF) +else() + set(_build_shared OFF) + set(_build_static ON) +endif() + +add_cmake_project(Blosc + URL https://github.com/Blosc/c-blosc/archive/8724c06e3da90f10986a253814af18ca081d8de0.zip + URL_HASH SHA256=53986fd04210b3d94124b7967c857f9766353e576a69595a9393999e0712c035 + CMAKE_ARGS + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DBUILD_SHARED=${_build_shared} + -DBUILD_STATIC=${_build_static} + -DBUILD_TESTS=OFF + -DBUILD_BENCHMARKS=OFF + -DPREFER_EXTERNAL_ZLIB=ON +) + +set(DEP_Blosc_DEPENDS ZLIB) diff --git a/deps/+Boost/Boost.cmake b/deps/+Boost/Boost.cmake new file mode 100644 index 0000000..cb470e9 --- /dev/null +++ b/deps/+Boost/Boost.cmake @@ -0,0 +1,25 @@ + +set(_context_abi_line "") +set(_context_arch_line "") +if (APPLE AND CMAKE_OSX_ARCHITECTURES) + if (CMAKE_OSX_ARCHITECTURES MATCHES "x86") + set(_context_abi_line "-DBOOST_CONTEXT_ABI:STRING=sysv") + elseif (CMAKE_OSX_ARCHITECTURES MATCHES "arm") + set (_context_abi_line "-DBOOST_CONTEXT_ABI:STRING=aapcs") + endif () + set(_context_arch_line "-DBOOST_CONTEXT_ARCHITECTURE:STRING=${CMAKE_OSX_ARCHITECTURES}") +endif () + +add_cmake_project(Boost + URL "https://github.com/boostorg/boost/releases/download/boost-1.83.0/boost-1.83.0.zip" + URL_HASH SHA256=9effa3d7f9d92b8e33e2b41d82f4358f97ff7c588d5918720339f2b254d914c6 + LIST_SEPARATOR | + CMAKE_ARGS + -DBOOST_EXCLUDE_LIBRARIES:STRING=contract|fiber|numpy|stacktrace|wave|test + -DBOOST_LOCALE_ENABLE_ICU:BOOL=OFF # do not link to libicu, breaks compatibility between distros + -DBUILD_TESTING:BOOL=OFF + "${_context_abi_line}" + "${_context_arch_line}" +) + +set(DEP_Boost_DEPENDS ZLIB) diff --git a/deps/CGAL/CGAL.cmake b/deps/+CGAL/CGAL.cmake similarity index 85% rename from deps/CGAL/CGAL.cmake rename to deps/+CGAL/CGAL.cmake index 5d23663..bb6e8fd 100644 --- a/deps/CGAL/CGAL.cmake +++ b/deps/+CGAL/CGAL.cmake @@ -1,11 +1,12 @@ -qidislicer_add_cmake_project( +add_cmake_project( CGAL # GIT_REPOSITORY https://github.com/CGAL/cgal.git # GIT_TAG bec70a6d52d8aacb0b3d82a7b4edc3caa899184b # releases/CGAL-5.0 # For whatever reason, this keeps downloading forever (repeats downloads if finished) URL https://github.com/CGAL/cgal/archive/refs/tags/v5.4.zip URL_HASH SHA256=d7605e0a5a5ca17da7547592f6f6e4a59430a0bc861948974254d0de43eab4c0 - DEPENDS dep_Boost dep_GMP dep_MPFR ) include(GNUInstallDirs) + +set(DEP_CGAL_DEPENDS Boost GMP MPFR) diff --git a/deps/CGAL/cgal/CGALConfigVersion.cmake b/deps/+CGAL/cgal/CGALConfigVersion.cmake similarity index 100% rename from deps/CGAL/cgal/CGALConfigVersion.cmake rename to deps/+CGAL/cgal/CGALConfigVersion.cmake diff --git a/deps/CURL/CURL.cmake b/deps/+CURL/CURL.cmake similarity index 93% rename from deps/CURL/CURL.cmake rename to deps/+CURL/CURL.cmake index 8ee372e..bcfc1ba 100644 --- a/deps/CURL/CURL.cmake +++ b/deps/+CURL/CURL.cmake @@ -56,12 +56,11 @@ if (UNIX AND NOT APPLE) set (_patch_command echo set_target_properties(CURL::libcurl PROPERTIES INTERFACE_COMPILE_DEFINITIONS OPENSSL_CERT_OVERRIDE) >> CMake/curl-config.cmake.in) endif () -qidislicer_add_cmake_project(CURL +add_cmake_project(CURL # GIT_REPOSITORY https://github.com/curl/curl.git # GIT_TAG curl-7_75_0 URL https://github.com/curl/curl/archive/refs/tags/curl-7_75_0.zip URL_HASH SHA256=a63ae025bb0a14f119e73250f2c923f4bf89aa93b8d4fafa4a9f5353a96a765a - DEPENDS ${ZLIB_PKG} # PATCH_COMMAND ${GIT_EXECUTABLE} checkout -f -- . && git clean -df && # ${GIT_EXECUTABLE} apply --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/curl-mods.patch PATCH_COMMAND "${_patch_command}" @@ -71,10 +70,8 @@ qidislicer_add_cmake_project(CURL ${_curl_platform_flags} ) +set(DEP_CURL_DEPENDS ZLIB) if (CMAKE_SYSTEM_NAME STREQUAL "Linux") - add_dependencies(dep_CURL dep_OpenSSL) + list(APPEND DEP_CURL_DEPENDS OpenSSL) endif () -if (MSVC) - add_debug_dep(dep_CURL) -endif () diff --git a/deps/+Catch2/Catch2.cmake b/deps/+Catch2/Catch2.cmake new file mode 100644 index 0000000..b355f16 --- /dev/null +++ b/deps/+Catch2/Catch2.cmake @@ -0,0 +1,6 @@ +add_cmake_project(Catch2 + URL "https://github.com/catchorg/Catch2/archive/refs/tags/v2.13.10.zip" + URL_HASH SHA256=121e7488912c2ce887bfe4699ebfb983d0f2e0d68bcd60434cdfd6bb0cf78b43 + CMAKE_ARGS + -DCATCH_BUILD_TESTING:BOOL=OFF +) \ No newline at end of file diff --git a/deps/Cereal/Cereal.cmake b/deps/+Cereal/Cereal.cmake similarity index 88% rename from deps/Cereal/Cereal.cmake rename to deps/+Cereal/Cereal.cmake index de1c539..1ec9af0 100644 --- a/deps/Cereal/Cereal.cmake +++ b/deps/+Cereal/Cereal.cmake @@ -1,4 +1,4 @@ -qidislicer_add_cmake_project(Cereal +add_cmake_project(Cereal URL "https://github.com/USCiLab/cereal/archive/refs/tags/v1.3.0.zip" URL_HASH SHA256=71642cb54658e98c8f07a0f0d08bf9766f1c3771496936f6014169d3726d9657 CMAKE_ARGS diff --git a/deps/EXPAT/EXPAT.cmake b/deps/+EXPAT/EXPAT.cmake similarity index 56% rename from deps/EXPAT/EXPAT.cmake rename to deps/+EXPAT/EXPAT.cmake index 77cd317..5d35d2d 100644 --- a/deps/EXPAT/EXPAT.cmake +++ b/deps/+EXPAT/EXPAT.cmake @@ -1,10 +1,4 @@ -#qidislicer_add_cmake_project(EXPAT -# # GIT_REPOSITORY https://github.com/nigels-com/glew.git -# # GIT_TAG 3a8eff7 # 2.1.0 -# SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/expat -#) - -qidislicer_add_cmake_project(EXPAT +add_cmake_project(EXPAT URL https://github.com/libexpat/libexpat/archive/refs/tags/R_2_4_3.zip URL_HASH SHA256=8851e199d763dc785277d6d414ed3e70ff683915158b51b8d8781df0e3af950a SOURCE_SUBDIR expat @@ -14,8 +8,4 @@ qidislicer_add_cmake_project(EXPAT -DEXPAT_BUILD_TESTS:BOOL=OFF -DEXPAT_BUILD_DOCS=OFF -DEXPAT_BUILD_PKGCONFIG=OFF -) - -if (MSVC) - add_debug_dep(dep_EXPAT) -endif () +) \ No newline at end of file diff --git a/deps/+GLEW/GLEW.cmake b/deps/+GLEW/GLEW.cmake new file mode 100644 index 0000000..c2db911 --- /dev/null +++ b/deps/+GLEW/GLEW.cmake @@ -0,0 +1,8 @@ +add_cmake_project( + GLEW + URL https://sourceforge.net/projects/glew/files/glew/2.2.0/glew-2.2.0.zip + URL_HASH SHA256=a9046a913774395a095edcc0b0ac2d81c3aacca61787b39839b941e9be14e0d4 + SOURCE_SUBDIR build/cmake + CMAKE_ARGS + -DBUILD_UTILS=OFF +) \ No newline at end of file diff --git a/deps/GMP/GMP.cmake b/deps/+GMP/GMP.cmake similarity index 68% rename from deps/GMP/GMP.cmake rename to deps/+GMP/GMP.cmake index 6b0f970..724db2e 100644 --- a/deps/GMP/GMP.cmake +++ b/deps/+GMP/GMP.cmake @@ -1,6 +1,6 @@ set(_srcdir ${CMAKE_CURRENT_LIST_DIR}/gmp) -set(_dstdir ${DESTDIR}/usr/local) +set(_dstdir ${${PROJECT_NAME}_DEP_INSTALL_PREFIX}) if (MSVC) set(_output ${_dstdir}/include/gmp.h @@ -17,13 +17,24 @@ if (MSVC) add_custom_target(dep_GMP SOURCES ${_output}) else () - set(_gmp_ccflags "-O2 -DNDEBUG -fPIC -DPIC -Wall -Wmissing-prototypes -Wpointer-arith -pedantic -fomit-frame-pointer -fno-common") + string(TOUPPER "${CMAKE_BUILD_TYPE}" _buildtype_upper) + set(_gmp_ccflags "${CMAKE_CXX_FLAGS_${_buildtype_upper}} -fPIC -DPIC -Wall -Wmissing-prototypes -Wpointer-arith -pedantic -fomit-frame-pointer -fno-common") set(_gmp_build_tgt "${CMAKE_SYSTEM_PROCESSOR}") + set(_cross_compile_arg "") if (APPLE) - if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") + if (CMAKE_OSX_ARCHITECTURES) + set(_gmp_build_tgt ${CMAKE_OSX_ARCHITECTURES}) + set(_gmp_ccflags "${_gmp_ccflags} -arch ${CMAKE_OSX_ARCHITECTURES}") + endif () + if (${_gmp_build_tgt} MATCHES "arm") set(_gmp_build_tgt aarch64) endif() + + if (CMAKE_OSX_ARCHITECTURES) + set(_cross_compile_arg --host=${_gmp_build_tgt}-apple-darwin21) + endif () + set(_gmp_ccflags "${_gmp_ccflags} -mmacosx-version-min=${DEP_OSX_TARGET}") set(_gmp_build_tgt "--build=${_gmp_build_tgt}-apple-darwin") elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") @@ -36,19 +47,19 @@ else () set(_gmp_build_tgt "") # let it guess endif() - set(_cross_compile_arg "") if (CMAKE_CROSSCOMPILING) # TOOLCHAIN_PREFIX should be defined in the toolchain file set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX}) endif () ExternalProject_Add(dep_GMP + EXCLUDE_FROM_ALL ON URL https://gmplib.org/download/gmp/gmp-6.2.1.tar.bz2 URL_HASH SHA256=eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c - DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP + DOWNLOAD_DIR ${${PROJECT_NAME}_DEP_DOWNLOAD_DIR}/GMP BUILD_IN_SOURCE ON - CONFIGURE_COMMAND env "CFLAGS=${_gmp_ccflags}" "CXXFLAGS=${_gmp_ccflags}" ./configure ${_cross_compile_arg} --enable-shared=no --enable-cxx=yes --enable-static=yes "--prefix=${DESTDIR}/usr/local" ${_gmp_build_tgt} + CONFIGURE_COMMAND env "CFLAGS=${_gmp_ccflags}" "CXXFLAGS=${_gmp_ccflags}" ./configure ${_cross_compile_arg} --enable-shared=no --enable-cxx=yes --enable-static=yes "--prefix=${${PROJECT_NAME}_DEP_INSTALL_PREFIX}" ${_gmp_build_tgt} BUILD_COMMAND make -j INSTALL_COMMAND make install ) -endif () \ No newline at end of file +endif () diff --git a/deps/GMP/gmp/gmp.COPYING b/deps/+GMP/gmp/gmp.COPYING similarity index 100% rename from deps/GMP/gmp/gmp.COPYING rename to deps/+GMP/gmp/gmp.COPYING diff --git a/deps/GMP/gmp/gmp.README b/deps/+GMP/gmp/gmp.README similarity index 100% rename from deps/GMP/gmp/gmp.README rename to deps/+GMP/gmp/gmp.README diff --git a/deps/GMP/gmp/include/gmp.h b/deps/+GMP/gmp/include/gmp.h similarity index 100% rename from deps/GMP/gmp/include/gmp.h rename to deps/+GMP/gmp/include/gmp.h diff --git a/deps/+GMP/gmp/lib/win32/libgmp-10.dll b/deps/+GMP/gmp/lib/win32/libgmp-10.dll new file mode 100644 index 0000000..387656c Binary files /dev/null and b/deps/+GMP/gmp/lib/win32/libgmp-10.dll differ diff --git a/deps/+GMP/gmp/lib/win32/libgmp-10.lib b/deps/+GMP/gmp/lib/win32/libgmp-10.lib new file mode 100644 index 0000000..53bb34c Binary files /dev/null and b/deps/+GMP/gmp/lib/win32/libgmp-10.lib differ diff --git a/deps/+GMP/gmp/lib/win64/libgmp-10.dll b/deps/+GMP/gmp/lib/win64/libgmp-10.dll new file mode 100644 index 0000000..f92b289 Binary files /dev/null and b/deps/+GMP/gmp/lib/win64/libgmp-10.dll differ diff --git a/deps/+GMP/gmp/lib/win64/libgmp-10.lib b/deps/+GMP/gmp/lib/win64/libgmp-10.lib new file mode 100644 index 0000000..ed35472 Binary files /dev/null and b/deps/+GMP/gmp/lib/win64/libgmp-10.lib differ diff --git a/deps/+JPEG/JPEG.cmake b/deps/+JPEG/JPEG.cmake new file mode 100644 index 0000000..2b31f34 --- /dev/null +++ b/deps/+JPEG/JPEG.cmake @@ -0,0 +1,9 @@ +add_cmake_project(JPEG + URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip + URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f + CMAKE_ARGS + -DENABLE_SHARED=OFF + -DENABLE_STATIC=ON +) + +set(DEP_JPEG_DEPENDS ZLIB) diff --git a/deps/+LibBGCode/LibBGCode.cmake b/deps/+LibBGCode/LibBGCode.cmake new file mode 100644 index 0000000..3403646 --- /dev/null +++ b/deps/+LibBGCode/LibBGCode.cmake @@ -0,0 +1,34 @@ +set(LibBGCode_SOURCE_DIR "" CACHE PATH "Optionally specify local LibBGCode source directory") + +set(_source_dir_line + URL https://github.com/prusa3d/libbgcode/archive/04556c4f64d4b7a5942d8d193d1eb87fc7e1005f.zip + URL_HASH SHA256=f0745b2dae95f0a49ae75bfbe4d775c751499fc4245864675e2dab06c13b2c8f +) + +if (LibBGCode_SOURCE_DIR) + set(_source_dir_line "SOURCE_DIR;${LibBGCode_SOURCE_DIR};BUILD_ALWAYS;ON") +endif () + +# add_cmake_project(LibBGCode_deps +# ${_source_dir_line} +# SOURCE_SUBDIR deps +# BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/ +# CMAKE_ARGS +# -DLibBGCode_Deps_DEP_DOWNLOAD_DIR:PATH=${${PROJECT_NAME}_DEP_DOWNLOAD_DIR} +# -DDEP_CMAKE_OPTS:STRING=-DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON +# -DLibBGCode_Deps_SELECT_ALL:BOOL=OFF +# -DLibBGCode_Deps_SELECT_heatshrink:BOOL=ON +# -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} +# -DLibBGCode_Deps_DEP_INSTALL_PREFIX=${${PROJECT_NAME}_DEP_INSTALL_PREFIX} +# ) + +add_cmake_project(LibBGCode + ${_source_dir_line} + CMAKE_ARGS + -DLibBGCode_BUILD_TESTS:BOOL=OFF + -DLibBGCode_BUILD_CMD_TOOL:BOOL=OFF +) + +# set(DEP_LibBGCode_deps_DEPENDS ZLIB Boost) +# set(DEP_LibBGCode_DEPENDS LibBGCode_deps) +set(DEP_LibBGCode_DEPENDS ZLIB Boost heatshrink) diff --git a/deps/+MPFR/MPFR.cmake b/deps/+MPFR/MPFR.cmake new file mode 100644 index 0000000..e04ce28 --- /dev/null +++ b/deps/+MPFR/MPFR.cmake @@ -0,0 +1,44 @@ +set(_srcdir ${CMAKE_CURRENT_LIST_DIR}/mpfr) +set(_dstdir ${${PROJECT_NAME}_DEP_INSTALL_PREFIX}) + +if (MSVC) + set(_output ${_dstdir}/include/mpfr.h + ${_dstdir}/include/mpf2mpfr.h + ${_dstdir}/lib/libmpfr-4.lib + ${_dstdir}/bin/libmpfr-4.dll) + + add_custom_command( + OUTPUT ${_output} + COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/include/mpfr.h ${_dstdir}/include/ + COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/include/mpf2mpfr.h ${_dstdir}/include/ + COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/lib/win${DEPS_BITS}/libmpfr-4.lib ${_dstdir}/lib/ + COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/lib/win${DEPS_BITS}/libmpfr-4.dll ${_dstdir}/bin/ + ) + + add_custom_target(dep_MPFR SOURCES ${_output}) + +else () + + # set(_cross_compile_arg "") + # if (CMAKE_CROSSCOMPILING) + # # TOOLCHAIN_PREFIX should be defined in the toolchain file + # set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX}) + # endif () + + message(STATUS "${PROJECT_NAME}_DEP_INSTALL_PREFIX=${${PROJECT_NAME}_DEP_INSTALL_PREFIX}") + + ExternalProject_Add(dep_MPFR + EXCLUDE_FROM_ALL ON + #URL http://ftp.vim.org/ftp/gnu/mpfr/mpfr-3.1.6.tar.bz2 https://www.mpfr.org/mpfr-3.1.6/mpfr-3.1.6.tar.bz2 # mirrors are allowed + #URL_HASH SHA256=cf4f4b2d80abb79e820e78c8077b6725bbbb4e8f41896783c899087be0e94068 + URL https://www.mpfr.org/mpfr-current/mpfr-4.2.1.tar.bz2 + URL_HASH SHA256=b9df93635b20e4089c29623b19420c4ac848a1b29df1cfd59f26cab0d2666aa0 + DOWNLOAD_DIR ${${PROJECT_NAME}_DEP_DOWNLOAD_DIR}/MPFR + BUILD_IN_SOURCE ON + CONFIGURE_COMMAND env "CFLAGS=${_gmp_ccflags}" "CXXFLAGS=${_gmp_ccflags}" ./configure ${_cross_compile_arg} --prefix=${${PROJECT_NAME}_DEP_INSTALL_PREFIX} --enable-shared=no --enable-static=yes --with-gmp=${${PROJECT_NAME}_DEP_INSTALL_PREFIX} ${_gmp_build_tgt} + BUILD_COMMAND make -j + INSTALL_COMMAND make install + ) +endif () + +set(DEP_MPFR_DEPENDS GMP) diff --git a/deps/MPFR/mpfr/include/mpf2mpfr.h b/deps/+MPFR/mpfr/include/mpf2mpfr.h similarity index 100% rename from deps/MPFR/mpfr/include/mpf2mpfr.h rename to deps/+MPFR/mpfr/include/mpf2mpfr.h diff --git a/deps/MPFR/mpfr/include/mpfr.h b/deps/+MPFR/mpfr/include/mpfr.h similarity index 100% rename from deps/MPFR/mpfr/include/mpfr.h rename to deps/+MPFR/mpfr/include/mpfr.h diff --git a/deps/+MPFR/mpfr/lib/win32/libmpfr-4.dll b/deps/+MPFR/mpfr/lib/win32/libmpfr-4.dll new file mode 100644 index 0000000..73a0c80 Binary files /dev/null and b/deps/+MPFR/mpfr/lib/win32/libmpfr-4.dll differ diff --git a/deps/+MPFR/mpfr/lib/win32/libmpfr-4.lib b/deps/+MPFR/mpfr/lib/win32/libmpfr-4.lib new file mode 100644 index 0000000..7b0ad8d Binary files /dev/null and b/deps/+MPFR/mpfr/lib/win32/libmpfr-4.lib differ diff --git a/deps/+MPFR/mpfr/lib/win64/libmpfr-4.dll b/deps/+MPFR/mpfr/lib/win64/libmpfr-4.dll new file mode 100644 index 0000000..fea68a1 Binary files /dev/null and b/deps/+MPFR/mpfr/lib/win64/libmpfr-4.dll differ diff --git a/deps/+MPFR/mpfr/lib/win64/libmpfr-4.lib b/deps/+MPFR/mpfr/lib/win64/libmpfr-4.lib new file mode 100644 index 0000000..8a1263c Binary files /dev/null and b/deps/+MPFR/mpfr/lib/win64/libmpfr-4.lib differ diff --git a/deps/MPFR/mpfr/mpfr.COPYING b/deps/+MPFR/mpfr/mpfr.COPYING similarity index 100% rename from deps/MPFR/mpfr/mpfr.COPYING rename to deps/+MPFR/mpfr/mpfr.COPYING diff --git a/deps/MPFR/mpfr/mpfr.COPYING.LESSER b/deps/+MPFR/mpfr/mpfr.COPYING.LESSER similarity index 100% rename from deps/MPFR/mpfr/mpfr.COPYING.LESSER rename to deps/+MPFR/mpfr/mpfr.COPYING.LESSER diff --git a/deps/MPFR/mpfr/mpfr.README b/deps/+MPFR/mpfr/mpfr.README similarity index 100% rename from deps/MPFR/mpfr/mpfr.README rename to deps/+MPFR/mpfr/mpfr.README diff --git a/deps/NLopt/NLopt.cmake b/deps/+NLopt/NLopt.cmake similarity index 79% rename from deps/NLopt/NLopt.cmake rename to deps/+NLopt/NLopt.cmake index 6779555..aed1c19 100644 --- a/deps/NLopt/NLopt.cmake +++ b/deps/+NLopt/NLopt.cmake @@ -1,4 +1,4 @@ -qidislicer_add_cmake_project(NLopt +add_cmake_project(NLopt URL "https://github.com/stevengj/nlopt/archive/v2.5.0.tar.gz" URL_HASH SHA256=c6dd7a5701fff8ad5ebb45a3dc8e757e61d52658de3918e38bab233e7fd3b4ae CMAKE_ARGS @@ -9,7 +9,3 @@ qidislicer_add_cmake_project(NLopt -DNLOPT_SWIG:BOOL=OFF -DNLOPT_TESTS:BOOL=OFF ) - -if (MSVC) - add_debug_dep(dep_NLopt) -endif () diff --git a/deps/NanoSVG/NanoSVG.cmake b/deps/+NanoSVG/NanoSVG.cmake similarity index 93% rename from deps/NanoSVG/NanoSVG.cmake rename to deps/+NanoSVG/NanoSVG.cmake index 9329d06..966afc4 100644 --- a/deps/NanoSVG/NanoSVG.cmake +++ b/deps/+NanoSVG/NanoSVG.cmake @@ -3,7 +3,7 @@ # for rasterizing svg files from their original size to a squared power of two texture on Windows systems using # AMD Radeon graphics cards -qidislicer_add_cmake_project(NanoSVG +add_cmake_project(NanoSVG URL https://github.com/fltk/nanosvg/archive/abcd277ea45e9098bed752cf9c6875b533c0892f.zip URL_HASH SHA256=e859938fbaee4b351bd8a8b3d3c7a75b40c36885ce00b73faa1ce0b98aa0ad34 ) \ No newline at end of file diff --git a/deps/OCCT/OCCT.cmake b/deps/+OCCT/OCCT.cmake similarity index 90% rename from deps/OCCT/OCCT.cmake rename to deps/+OCCT/OCCT.cmake index 9cf75f1..5b7743e 100644 --- a/deps/OCCT/OCCT.cmake +++ b/deps/+OCCT/OCCT.cmake @@ -1,4 +1,4 @@ -qidislicer_add_cmake_project(OCCT +add_cmake_project(OCCT #LMBBS: changed version to 7.6.2 URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V7_6_2.zip URL_HASH SHA256=c696b923593e8c18d059709717dbf155b3e72fdd283c8522047a790ec3a432c5 @@ -21,7 +21,3 @@ qidislicer_add_cmake_project(OCCT -DBUILD_MODULE_ModelingData=OFF -DBUILD_MODULE_Visualization=OFF ) - -if (MSVC) - add_debug_dep(dep_OCCT) -endif () diff --git a/deps/OCCT/occt_toolkit.cmake b/deps/+OCCT/occt_toolkit.cmake similarity index 100% rename from deps/OCCT/occt_toolkit.cmake rename to deps/+OCCT/occt_toolkit.cmake diff --git a/deps/OpenCSG/CMakeLists.txt.in b/deps/+OpenCSG/CMakeLists.txt.in similarity index 100% rename from deps/OpenCSG/CMakeLists.txt.in rename to deps/+OpenCSG/CMakeLists.txt.in diff --git a/deps/+OpenCSG/OpenCSG.cmake b/deps/+OpenCSG/OpenCSG.cmake new file mode 100644 index 0000000..9a99354 --- /dev/null +++ b/deps/+OpenCSG/OpenCSG.cmake @@ -0,0 +1,8 @@ + +add_cmake_project(OpenCSG + URL https://github.com/floriankirsch/OpenCSG/archive/refs/tags/opencsg-1-4-2-release.zip + URL_HASH SHA256=51afe0db79af8386e2027d56d685177135581e0ee82ade9d7f2caff8deab5ec5 + PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt +) + +set(DEP_OpenCSG_DEPENDS GLEW ZLIB) diff --git a/deps/OpenEXR/OpenEXR.cmake b/deps/+OpenEXR/OpenEXR.cmake similarity index 83% rename from deps/OpenEXR/OpenEXR.cmake rename to deps/+OpenEXR/OpenEXR.cmake index 9a0f092..e13f4a3 100644 --- a/deps/OpenEXR/OpenEXR.cmake +++ b/deps/+OpenEXR/OpenEXR.cmake @@ -1,8 +1,7 @@ -qidislicer_add_cmake_project(OpenEXR +add_cmake_project(OpenEXR # GIT_REPOSITORY https://github.com/openexr/openexr.git URL https://github.com/AcademySoftwareFoundation/openexr/archive/refs/tags/v2.5.5.zip URL_HASH SHA256=0307a3d7e1fa1e77e9d84d7e9a8694583fbbbfd50bdc6884e2c96b8ef6b902de - DEPENDS ${ZLIB_PKG} GIT_TAG v2.5.5 PATCH_COMMAND COMMAND ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/OpenEXR.patch CMAKE_ARGS @@ -13,6 +12,4 @@ qidislicer_add_cmake_project(OpenEXR -DOPENEXR_BUILD_UTILS:BOOL=OFF ) -if (MSVC) - add_debug_dep(dep_OpenEXR) -endif () +set(DEP_OpenEXR_DEPENDS ZLIB) diff --git a/deps/OpenEXR/OpenEXR.patch b/deps/+OpenEXR/OpenEXR.patch similarity index 100% rename from deps/OpenEXR/OpenEXR.patch rename to deps/+OpenEXR/OpenEXR.patch diff --git a/deps/OpenSSL/OpenSSL.cmake b/deps/+OpenSSL/OpenSSL.cmake similarity index 90% rename from deps/OpenSSL/OpenSSL.cmake rename to deps/+OpenSSL/OpenSSL.cmake index 347b30d..bcfbab9 100644 --- a/deps/OpenSSL/OpenSSL.cmake +++ b/deps/+OpenSSL/OpenSSL.cmake @@ -21,10 +21,10 @@ ExternalProject_Add(dep_OpenSSL EXCLUDE_FROM_ALL ON URL "https://github.com/openssl/openssl/archive/OpenSSL_1_1_0l.tar.gz" URL_HASH SHA256=e2acf0cf58d9bff2b42f2dc0aee79340c8ffe2c5e45d3ca4533dd5d4f5775b1d - DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL + DOWNLOAD_DIR ${${PROJECT_NAME}_DEP_DOWNLOAD_DIR}/OpenSSL BUILD_IN_SOURCE ON CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch} - "--prefix=${DESTDIR}/usr/local" + "--prefix=${${PROJECT_NAME}_DEP_INSTALL_PREFIX}" ${_cross_comp_prefix_line} no-shared no-ssl3-method diff --git a/deps/OpenVDB/OpenVDB.cmake b/deps/+OpenVDB/OpenVDB.cmake similarity index 62% rename from deps/OpenVDB/OpenVDB.cmake rename to deps/+OpenVDB/OpenVDB.cmake index 7316ba9..cfafa03 100644 --- a/deps/OpenVDB/OpenVDB.cmake +++ b/deps/+OpenVDB/OpenVDB.cmake @@ -7,17 +7,16 @@ else() endif() set (_openvdb_vdbprint ON) -if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") +if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm" OR NOT ${CMAKE_BUILD_TYPE} STREQUAL Release) # Build fails on raspberry pi due to missing link directive to latomic # Let's hope it will be fixed soon. set (_openvdb_vdbprint OFF) endif () -qidislicer_add_cmake_project(OpenVDB +add_cmake_project(OpenVDB # 8.2 patched URL https://github.com/tamasmeszaros/openvdb/archive/a68fd58d0e2b85f01adeb8b13d7555183ab10aa5.zip URL_HASH SHA256=f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81 - DEPENDS dep_TBB dep_Blosc dep_OpenEXR dep_Boost CMAKE_ARGS -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DOPENVDB_BUILD_PYTHON_MODULE=OFF @@ -30,15 +29,4 @@ qidislicer_add_cmake_project(OpenVDB -DDISABLE_DEPENDENCY_VERSION_CHECKS=ON # Centos6 has old zlib ) -if (MSVC) - if (${DEP_DEBUG}) - ExternalProject_Get_Property(dep_OpenVDB BINARY_DIR) - ExternalProject_Add_Step(dep_OpenVDB build_debug - DEPENDEES build - DEPENDERS install - COMMAND ${CMAKE_COMMAND} ../dep_OpenVDB -DOPENVDB_BUILD_VDB_PRINT=OFF - COMMAND msbuild /m /P:Configuration=Debug INSTALL.vcxproj - WORKING_DIRECTORY "${BINARY_DIR}" - ) - endif () -endif () \ No newline at end of file +set(DEP_OpenVDB_DEPENDS TBB Blosc OpenEXR Boost) diff --git a/deps/+PNG/CMakeLists.txt.patched b/deps/+PNG/CMakeLists.txt.patched new file mode 100644 index 0000000..c70f624 --- /dev/null +++ b/deps/+PNG/CMakeLists.txt.patched @@ -0,0 +1,989 @@ +# CMakeLists.txt + +# Copyright (C) 2007,2009-2018 Glenn Randers-Pehrson +# Written by Christian Ehrlicher, 2007 +# Revised by Roger Lowman, 2009-2010 +# Revised by Clifford Yapp, 2011-2012 +# Revised by Roger Leigh, 2016 +# Revised by Andreas Franek, 2016 + +# This code is released under the libpng license. +# For conditions of distribution and use, see the disclaimer +# and license in png.h + +cmake_minimum_required(VERSION 3.0.2) +cmake_policy(VERSION 3.0.2) + +# Set MacOSX @rpath usage globally. +if (POLICY CMP0020) + cmake_policy(SET CMP0020 NEW) +endif(POLICY CMP0020) +if (POLICY CMP0042) + cmake_policy(SET CMP0042 NEW) +endif(POLICY CMP0042) +# Use new variable expansion policy. +if (POLICY CMP0053) + cmake_policy(SET CMP0053 NEW) +endif(POLICY CMP0053) +if (POLICY CMP0054) + cmake_policy(SET CMP0054 NEW) +endif(POLICY CMP0054) + +set(CMAKE_CONFIGURATION_TYPES "Release;Debug;MinSizeRel;RelWithDebInfo") + +project(libpng C ASM) +enable_testing() + +set(PNGLIB_MAJOR 1) +set(PNGLIB_MINOR 6) +set(PNGLIB_RELEASE 35) +set(PNGLIB_NAME libpng${PNGLIB_MAJOR}${PNGLIB_MINOR}) +set(PNGLIB_VERSION ${PNGLIB_MAJOR}.${PNGLIB_MINOR}.${PNGLIB_RELEASE}) + +include(GNUInstallDirs) + +# needed packages + +#Allow users to specify location of Zlib, +# Useful if zlib is being built alongside this as a sub-project +option(PNG_BUILD_ZLIB "Custom zlib Location, else find_package is used" OFF) + +IF(NOT PNG_BUILD_ZLIB) + find_package(ZLIB REQUIRED) + include_directories(${ZLIB_INCLUDE_DIR}) +ENDIF(NOT PNG_BUILD_ZLIB) + +if(NOT WIN32) + find_library(M_LIBRARY + NAMES m + PATHS /usr/lib /usr/local/lib + ) + if(NOT M_LIBRARY) + message(STATUS "math lib 'libm' not found; floating point support disabled") + endif() +else() + # not needed on windows + set(M_LIBRARY "") +endif() + +# COMMAND LINE OPTIONS +option(PNG_SHARED "Build shared lib" ON) +option(PNG_STATIC "Build static lib" ON) +option(PNG_TESTS "Build libpng tests" ON) + +# Many more configuration options could be added here +option(PNG_FRAMEWORK "Build OS X framework" OFF) +option(PNG_DEBUG "Build with debug output" OFF) +option(PNGARG "Disable ANSI-C prototypes" OFF) + +option(PNG_HARDWARE_OPTIMIZATIONS "Enable Hardware Optimizations" ON) + + +set(PNG_PREFIX "" CACHE STRING "Prefix to add to the API function names") +set(DFA_XTRA "" CACHE FILEPATH "File containing extra configuration settings") + +# CMake currently sets CMAKE_SYSTEM_PROCESSOR to one of x86_64 or arm64 on macOS, +# based upon the OS architecture, not the target architecture. As such, we need +# to check CMAKE_OSX_ARCHITECTURES to identify which hardware-specific flags to +# enable. Note that this will fail if you attempt to build a universal binary in +# a single CMake invocation. +if (APPLE AND CMAKE_OSX_ARCHITECTURES) + set(TARGET_ARCH ${CMAKE_OSX_ARCHITECTURES}) +else() + set(TARGET_ARCH ${CMAKE_SYSTEM_PROCESSOR}) +endif() + +if(PNG_HARDWARE_OPTIMIZATIONS) + +# Set definitions and sources for ARM. +if(TARGET_ARCH MATCHES "^arm" OR + TARGET_ARCH MATCHES "^aarch64") + if(TARGET_ARCH MATCHES "^arm64" OR + TARGET_ARCH MATCHES "^aarch64") + set(PNG_ARM_NEON_POSSIBLE_VALUES on off) + set(PNG_ARM_NEON "on" + CACHE STRING "Enable ARM NEON optimizations: on|off; on is default") + else() + set(PNG_ARM_NEON_POSSIBLE_VALUES check on off) + set(PNG_ARM_NEON "check" + CACHE STRING "Enable ARM NEON optimizations: check|on|off; check is default") + endif() + set_property(CACHE PNG_ARM_NEON + PROPERTY STRINGS ${PNG_ARM_NEON_POSSIBLE_VALUES}) + list(FIND PNG_ARM_NEON_POSSIBLE_VALUES ${PNG_ARM_NEON} index) + if(index EQUAL -1) + message(FATAL_ERROR + " PNG_ARM_NEON must be one of [${PNG_ARM_NEON_POSSIBLE_VALUES}]") + elseif(NOT ${PNG_ARM_NEON} STREQUAL "no") + set(libpng_arm_sources + arm/arm_init.c + arm/filter_neon.S + arm/filter_neon_intrinsics.c) + + if(${PNG_ARM_NEON} STREQUAL "on") + add_definitions(-DPNG_ARM_NEON_OPT=2) + elseif(${PNG_ARM_NEON} STREQUAL "check") + add_definitions(-DPNG_ARM_NEON_CHECK_SUPPORTED) + endif() + else() + add_definitions(-DPNG_ARM_NEON_OPT=0) + endif() +endif() + +# Set definitions and sources for PowerPC. +if(TARGET_ARCH MATCHES "^powerpc*" OR + TARGET_ARCH MATCHES "^ppc64*") + set(PNG_POWERPC_VSX_POSSIBLE_VALUES on off) + set(PNG_POWERPC_VSX "on" CACHE STRING "Enable POWERPC VSX optimizations: + off: disable the optimizations.") + set_property(CACHE PNG_POWERPC_VSX PROPERTY STRINGS + ${PNG_POWERPC_VSX_POSSIBLE_VALUES}) + list(FIND PNG_POWERPC_VSX_POSSIBLE_VALUES ${PNG_POWERPC_VSX} index) + if(index EQUAL -1) + message(FATAL_ERROR + " PNG_POWERPC_VSX must be one of [${PNG_POWERPC_VSX_POSSIBLE_VALUES}]") + elseif(NOT ${PNG_POWERPC_VSX} STREQUAL "no") + set(libpng_powerpc_sources + powerpc/powerpc_init.c + powerpc/filter_vsx_intrinsics.c) + if(${PNG_POWERPC_VSX} STREQUAL "on") + add_definitions(-DPNG_POWERPC_VSX_OPT=2) + endif() + else() + add_definitions(-DPNG_POWERPC_VSX_OPT=0) + endif() +endif() + +# Set definitions and sources for Intel. +if(TARGET_ARCH MATCHES "^i?86" OR + TARGET_ARCH MATCHES "^x86_64*") + set(PNG_INTEL_SSE_POSSIBLE_VALUES on off) + set(PNG_INTEL_SSE "on" CACHE STRING "Enable INTEL_SSE optimizations: + off: disable the optimizations") + set_property(CACHE PNG_INTEL_SSE PROPERTY STRINGS + ${PNG_INTEL_SSE_POSSIBLE_VALUES}) + list(FIND PNG_INTEL_SSE_POSSIBLE_VALUES ${PNG_INTEL_SSE} index) + if(index EQUAL -1) + message(FATAL_ERROR + " PNG_INTEL_SSE must be one of [${PNG_INTEL_SSE_POSSIBLE_VALUES}]") + elseif(NOT ${PNG_INTEL_SSE} STREQUAL "no") + set(libpng_intel_sources + intel/intel_init.c + intel/filter_sse2_intrinsics.c) + if(${PNG_INTEL_SSE} STREQUAL "on") + add_definitions(-DPNG_INTEL_SSE_OPT=1) + endif() + else() + add_definitions(-DPNG_INTEL_SSE_OPT=0) + endif() +endif() + +# Set definitions and sources for MIPS. +if(TARGET_ARCH MATCHES "mipsel*" OR + TARGET_ARCH MATCHES "mips64el*") + set(PNG_MIPS_MSA_POSSIBLE_VALUES on off) + set(PNG_MIPS_MSA "on" CACHE STRING "Enable MIPS_MSA optimizations: + off: disable the optimizations") + set_property(CACHE PNG_MIPS_MSA PROPERTY STRINGS + ${PNG_MIPS_MSA_POSSIBLE_VALUES}) + list(FIND PNG_MIPS_MSA_POSSIBLE_VALUES ${PNG_MIPS_MSA} index) + if(index EQUAL -1) + message(FATAL_ERROR + " PNG_MIPS_MSA must be one of [${PNG_MIPS_MSA_POSSIBLE_VALUES}]") + elseif(NOT ${PNG_MIPS_MSA} STREQUAL "no") + set(libpng_mips_sources + mips/mips_init.c + mips/filter_msa_intrinsics.c) + if(${PNG_MIPS_MSA} STREQUAL "on") + add_definitions(-DPNG_MIPS_MSA_OPT=2) + endif() + else() + add_definitions(-DPNG_MIPS_MSA_OPT=0) + endif() +endif() + +else(PNG_HARDWARE_OPTIMIZATIONS) + +# Set definitions and sources for ARM. +if(TARGET_ARCH MATCHES "^arm" OR + TARGET_ARCH MATCHES "^aarch64") + add_definitions(-DPNG_ARM_NEON_OPT=0) +endif() + +# Set definitions and sources for PowerPC. +if(TARGET_ARCH MATCHES "^powerpc*" OR + TARGET_ARCH MATCHES "^ppc64*") + add_definitions(-DPNG_POWERPC_VSX_OPT=0) +endif() + +# Set definitions and sources for Intel. +if(TARGET_ARCH MATCHES "^i?86" OR + TARGET_ARCH MATCHES "^x86_64*") + add_definitions(-DPNG_INTEL_SSE_OPT=0) +endif() + +# Set definitions and sources for MIPS. +if(TARGET_ARCH MATCHES "mipsel*" OR + TARGET_ARCH MATCHES "mips64el*") + add_definitions(-DPNG_MIPS_MSA_OPT=0) +endif() + +endif(PNG_HARDWARE_OPTIMIZATIONS) + +# SET LIBNAME +set(PNG_LIB_NAME png${PNGLIB_MAJOR}${PNGLIB_MINOR}) + +# to distinguish between debug and release lib +set(CMAKE_DEBUG_POSTFIX "d") + +include(CheckCSourceCompiles) +option(ld-version-script "Enable linker version script" ON) +if(ld-version-script AND NOT APPLE) + # Check if LD supports linker scripts. + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/conftest.map" "VERS_1 { + global: sym; + local: *; +}; + +VERS_2 { + global: sym2; + main; +} VERS_1; +") + set(CMAKE_REQUIRED_FLAGS_SAVE ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS} "-Wl,--version-script='${CMAKE_CURRENT_BINARY_DIR}/conftest.map'") + check_c_source_compiles("void sym(void) {} +void sym2(void) {} +int main(void) {return 0;} +" HAVE_LD_VERSION_SCRIPT) + if(NOT HAVE_LD_VERSION_SCRIPT) + set(CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS_SAVE} "-Wl,-M -Wl,${CMAKE_CURRENT_BINARY_DIR}/conftest.map") + check_c_source_compiles("void sym(void) {} +void sym2(void) {} +int main(void) {return 0;} +" HAVE_SOLARIS_LD_VERSION_SCRIPT) + endif() + set(CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS_SAVE}) + file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/conftest.map") +endif() + +# Find symbol prefix. Likely obsolete and unnecessary with recent +# toolchains (it's not done in many other projects). +function(symbol_prefix) + set(SYMBOL_PREFIX) + + execute_process(COMMAND "${CMAKE_C_COMPILER}" "-E" "-" + INPUT_FILE /dev/null + OUTPUT_VARIABLE OUT + RESULT_VARIABLE STATUS) + + if(CPP_FAIL) + message(WARNING "Failed to run the C preprocessor") + endif() + + string(REPLACE "\n" ";" OUT "${OUT}") + foreach(line ${OUT}) + string(REGEX MATCH "^PREFIX=" found_match "${line}") + if(found_match) + STRING(REGEX REPLACE "^PREFIX=(.*\)" "\\1" prefix "${line}") + string(REGEX MATCH "__USER_LABEL_PREFIX__" found_match "${prefix}") + if(found_match) + STRING(REGEX REPLACE "(.*)__USER_LABEL_PREFIX__(.*)" "\\1\\2" prefix "${prefix}") + endif() + set(SYMBOL_PREFIX "${prefix}") + endif() + endforeach() + + message(STATUS "Symbol prefix: ${SYMBOL_PREFIX}") + set(SYMBOL_PREFIX "${SYMBOL_PREFIX}" PARENT_SCOPE) +endfunction() + +if(UNIX) + symbol_prefix() +endif() + +find_program(AWK NAMES gawk awk) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +if(NOT AWK OR ANDROID) + # No awk available to generate sources; use pre-built pnglibconf.h + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt + ${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.h) + add_custom_target(genfiles) # Dummy +else() + include(CMakeParseArguments) + # Generate .chk from .out with awk + # generate_chk(INPUT inputfile OUTPUT outputfile [DEPENDS dep1 [dep2...]]) + function(generate_chk) + set(options) + set(oneValueArgs INPUT OUTPUT) + set(multiValueArgs DEPENDS) + cmake_parse_arguments(_GC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if (NOT _GC_INPUT) + message(FATAL_ERROR "Invalid arguments. generate_out requires input.") + endif() + if (NOT _GC_OUTPUT) + message(FATAL_ERROR "Invalid arguments. generate_out requires output.") + endif() + + add_custom_command(OUTPUT "${_GC_OUTPUT}" + COMMAND "${CMAKE_COMMAND}" + "-DINPUT=${_GC_INPUT}" + "-DOUTPUT=${_GC_OUTPUT}" + -P "${CMAKE_CURRENT_BINARY_DIR}/scripts/genchk.cmake" + DEPENDS "${_GC_INPUT}" ${_GC_DEPENDS} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + endfunction() + + # Generate .out from .c with awk + # generate_out(INPUT inputfile OUTPUT outputfile [DEPENDS dep1 [dep2...]]) + function(generate_out) + set(options) + set(oneValueArgs INPUT OUTPUT) + set(multiValueArgs DEPENDS) + cmake_parse_arguments(_GO "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if (NOT _GO_INPUT) + message(FATAL_ERROR "Invalid arguments. generate_out requires input.") + endif() + if (NOT _GO_OUTPUT) + message(FATAL_ERROR "Invalid arguments. generate_out requires output.") + endif() + + add_custom_command(OUTPUT "${_GO_OUTPUT}" + COMMAND "${CMAKE_COMMAND}" + "-DINPUT=${_GO_INPUT}" + "-DOUTPUT=${_GO_OUTPUT}" + -P "${CMAKE_CURRENT_BINARY_DIR}/scripts/genout.cmake" + DEPENDS "${_GO_INPUT}" ${_GO_DEPENDS} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + endfunction() + + # Generate specific source file with awk + # generate_source(OUTPUT outputfile [DEPENDS dep1 [dep2...]]) + function(generate_source) + set(options) + set(oneValueArgs OUTPUT) + set(multiValueArgs DEPENDS) + cmake_parse_arguments(_GSO "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if (NOT _GSO_OUTPUT) + message(FATAL_ERROR "Invalid arguments. generate_source requires output.") + endif() + + add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${_GSO_OUTPUT}" + COMMAND "${CMAKE_COMMAND}" + "-DOUTPUT=${_GSO_OUTPUT}" + -P "${CMAKE_CURRENT_BINARY_DIR}/scripts/gensrc.cmake" + DEPENDS ${_GSO_DEPENDS} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + endfunction() + + # Copy file + function(generate_copy source destination) + add_custom_command(OUTPUT "${destination}" + COMMAND "${CMAKE_COMMAND}" -E remove "${destination}" + COMMAND "${CMAKE_COMMAND}" -E copy "${source}" + "${destination}" + DEPENDS "${source}") + endfunction() + + # Generate scripts/pnglibconf.h + generate_source(OUTPUT "scripts/pnglibconf.c" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pnglibconf.dfa" + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/options.awk" + "${CMAKE_CURRENT_SOURCE_DIR}/pngconf.h") + + # Generate pnglibconf.c + generate_source(OUTPUT "pnglibconf.c" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pnglibconf.dfa" + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/options.awk" + "${CMAKE_CURRENT_SOURCE_DIR}/pngconf.h") + + if(PNG_PREFIX) + set(PNGLIBCONF_H_EXTRA_DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/scripts/prefix.out" + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/macro.lst") + set(PNGPREFIX_H_EXTRA_DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/scripts/intprefix.out") + endif() + + generate_out(INPUT "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.c" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.out") + + # Generate pnglibconf.h + generate_source(OUTPUT "pnglibconf.h" + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.out" + ${PNGLIBCONF_H_EXTRA_DEPENDS}) + + generate_out(INPUT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/intprefix.c" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/scripts/intprefix.out" + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.h") + + generate_out(INPUT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/prefix.c" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/scripts/prefix.out" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/png.h" + "${CMAKE_CURRENT_SOURCE_DIR}/pngconf.h" + "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.out") + + # Generate pngprefix.h + generate_source(OUTPUT "pngprefix.h" + DEPENDS ${PNGPREFIX_H_EXTRA_DEPENDS}) + + generate_out(INPUT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/sym.c" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/scripts/sym.out" + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.h") + + generate_out(INPUT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/symbols.c" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/scripts/symbols.out" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/png.h" + "${CMAKE_CURRENT_SOURCE_DIR}/pngconf.h" + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt") + + generate_out(INPUT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/vers.c" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/scripts/vers.out" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/png.h" + "${CMAKE_CURRENT_SOURCE_DIR}/pngconf.h" + "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.h") + + generate_chk(INPUT "${CMAKE_CURRENT_BINARY_DIR}/scripts/symbols.out" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/scripts/symbols.chk" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/checksym.awk" + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/symbols.def") + + add_custom_target(symbol-check DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/scripts/symbols.chk") + + generate_copy("${CMAKE_CURRENT_BINARY_DIR}/scripts/sym.out" + "${CMAKE_CURRENT_BINARY_DIR}/libpng.sym") + generate_copy("${CMAKE_CURRENT_BINARY_DIR}/scripts/vers.out" + "${CMAKE_CURRENT_BINARY_DIR}/libpng.vers") + + add_custom_target(genvers DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/libpng.vers") + add_custom_target(gensym DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/libpng.sym") + + add_custom_target("genprebuilt" + COMMAND "${CMAKE_COMMAND}" + "-DOUTPUT=scripts/pnglibconf.h.prebuilt" + -P "${CMAKE_CURRENT_BINARY_DIR}/scripts/gensrc.cmake" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + + # A single target handles generation of all generated files. If + # they are depended upon separately by multiple targets, this + # confuses parallel make (it would require a separate top-level + # target for each file to track the dependencies properly). + add_custom_target(genfiles DEPENDS + "${CMAKE_CURRENT_BINARY_DIR}/libpng.sym" + "${CMAKE_CURRENT_BINARY_DIR}/libpng.vers" + "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.c" + "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.h" + "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.out" + "${CMAKE_CURRENT_BINARY_DIR}/pngprefix.h" + "${CMAKE_CURRENT_BINARY_DIR}/scripts/intprefix.out" + "${CMAKE_CURRENT_BINARY_DIR}/scripts/pnglibconf.c" + "${CMAKE_CURRENT_BINARY_DIR}/scripts/prefix.out" + "${CMAKE_CURRENT_BINARY_DIR}/scripts/sym.out" + "${CMAKE_CURRENT_BINARY_DIR}/scripts/symbols.chk" + "${CMAKE_CURRENT_BINARY_DIR}/scripts/symbols.out" + "${CMAKE_CURRENT_BINARY_DIR}/scripts/vers.out") +endif(NOT AWK OR ANDROID) + +# OUR SOURCES +set(libpng_public_hdrs + png.h + pngconf.h + "${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.h" +) +set(libpng_private_hdrs + pngpriv.h + pngdebug.h + pnginfo.h + pngstruct.h +) +if(AWK AND NOT ANDROID) + list(APPEND libpng_private_hdrs "${CMAKE_CURRENT_BINARY_DIR}/pngprefix.h") +endif() +set(libpng_sources + ${libpng_public_hdrs} + ${libpng_private_hdrs} + png.c + pngerror.c + pngget.c + pngmem.c + pngpread.c + pngread.c + pngrio.c + pngrtran.c + pngrutil.c + pngset.c + pngtrans.c + pngwio.c + pngwrite.c + pngwtran.c + pngwutil.c + ${libpng_arm_sources} + ${libpng_intel_sources} + ${libpng_mips_sources} + ${libpng_powerpc_sources} +) +set(pngtest_sources + pngtest.c +) +set(pngvalid_sources + contrib/libtests/pngvalid.c +) +set(pngstest_sources + contrib/libtests/pngstest.c +) +set(pngunknown_sources + contrib/libtests/pngunknown.c +) +set(pngimage_sources + contrib/libtests/pngimage.c +) +set(pngfix_sources + contrib/tools/pngfix.c +) +set(png_fix_itxt_sources + contrib/tools/png-fix-itxt.c +) + +if(MSVC) + add_definitions(-D_CRT_SECURE_NO_DEPRECATE) +endif(MSVC) + +if(PNG_DEBUG) + add_definitions(-DPNG_DEBUG) +endif() + +# NOW BUILD OUR TARGET +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${ZLIB_INCLUDE_DIR}) + +unset(PNG_LIB_TARGETS) + +if(PNG_SHARED) + add_library(png SHARED ${libpng_sources}) + set(PNG_LIB_TARGETS png) + set_target_properties(png PROPERTIES OUTPUT_NAME ${PNG_LIB_NAME}) + add_dependencies(png genfiles) + if(MSVC) + # msvc does not append 'lib' - do it here to have consistent name + set_target_properties(png PROPERTIES PREFIX "lib") + set_target_properties(png PROPERTIES IMPORT_PREFIX "lib") + endif() + target_link_libraries(png ${ZLIB_LIBRARY} ${M_LIBRARY}) + + if(UNIX AND AWK) + if(HAVE_LD_VERSION_SCRIPT) + set_target_properties(png PROPERTIES LINK_FLAGS + "-Wl,--version-script='${CMAKE_CURRENT_BINARY_DIR}/libpng.vers'") + elseif(HAVE_SOLARIS_LD_VERSION_SCRIPT) + set_target_properties(png PROPERTIES LINK_FLAGS + "-Wl,-M -Wl,'${CMAKE_CURRENT_BINARY_DIR}/libpng.vers'") + endif() + endif() +endif() + +if(PNG_STATIC) + # does not work without changing name + set(PNG_LIB_NAME_STATIC png_static) + add_library(png_static STATIC ${libpng_sources}) + add_dependencies(png_static genfiles) + # MSVC doesn't use a different file extension for shared vs. static + # libs. We are able to change OUTPUT_NAME to remove the _static + # for all other platforms. + if(NOT MSVC) + set_target_properties(png_static PROPERTIES + OUTPUT_NAME "${PNG_LIB_NAME}" + CLEAN_DIRECT_OUTPUT 1) + else() + set_target_properties(png_static PROPERTIES + OUTPUT_NAME "${PNG_LIB_NAME}_static" + CLEAN_DIRECT_OUTPUT 1) + endif() + list(APPEND PNG_LIB_TARGETS png_static) + if(MSVC) + # msvc does not append 'lib' - do it here to have consistent name + set_target_properties(png_static PROPERTIES PREFIX "lib") + endif() + target_link_libraries(png_static ${ZLIB_LIBRARY} ${M_LIBRARY}) +endif() + +if(PNG_FRAMEWORK) + set(PNG_LIB_NAME_FRAMEWORK png_framework) + add_library(png_framework SHARED ${libpng_sources}) + add_dependencies(png_framework genfiles) + list(APPEND PNG_LIB_TARGETS png_framework) + set_target_properties(png_framework PROPERTIES + FRAMEWORK TRUE + FRAMEWORK_VERSION ${PNGLIB_VERSION} + MACOSX_FRAMEWORK_SHORT_VERSION_STRING ${PNGLIB_MAJOR}.${PNGLIB_MINOR} + MACOSX_FRAMEWORK_BUNDLE_VERSION ${PNGLIB_VERSION} + MACOSX_FRAMEWORK_IDENTIFIER org.libpng.libpng + XCODE_ATTRIBUTE_INSTALL_PATH "@rpath" + PUBLIC_HEADER "${libpng_public_hdrs}" + OUTPUT_NAME png) + target_link_libraries(png_framework ${ZLIB_LIBRARY} ${M_LIBRARY}) +endif() + +if(NOT PNG_LIB_TARGETS) + message(SEND_ERROR + "No library variant selected to build. " + "Please enable at least one of the following options: " + " PNG_STATIC, PNG_SHARED, PNG_FRAMEWORK") +endif() + +if(PNG_SHARED AND WIN32) + set_target_properties(png PROPERTIES DEFINE_SYMBOL PNG_BUILD_DLL) +endif() + +function(png_add_test) + set(options) + set(oneValueArgs NAME COMMAND) + set(multiValueArgs OPTIONS FILES) + cmake_parse_arguments(_PAT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if (NOT _PAT_NAME) + message(FATAL_ERROR "Invalid arguments. png_add_test requires name.") + endif() + if (NOT _PAT_COMMAND) + message(FATAL_ERROR "Invalid arguments. png_add_test requires command.") + endif() + + set(TEST_OPTIONS "${_PAT_OPTIONS}") + set(TEST_FILES "${_PAT_FILES}") + + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/scripts/test.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/tests/${_PAT_NAME}.cmake" @ONLY) + if(CMAKE_MAJOR_VERSION GREATER 2) # have generator expressions + add_test(NAME "${_PAT_NAME}" + COMMAND "${CMAKE_COMMAND}" + "-DLIBPNG=$" + "-DTEST_COMMAND=$" + -P "${CMAKE_CURRENT_BINARY_DIR}/tests/${_PAT_NAME}.cmake") + else() # old 2.x add_test; limited and won't work well on Windows + # Note LIBPNG is a dummy value as there are no generator expressions + add_test("${_PAT_NAME}" "${CMAKE_COMMAND}" + "-DLIBPNG=${CMAKE_CURRENT_BINARY_DIR}/libpng.so" + "-DTEST_COMMAND=./${_PAT_COMMAND}" + -P "${CMAKE_CURRENT_BINARY_DIR}/tests/${_PAT_NAME}.cmake") + endif() +endfunction() + +if(PNG_TESTS AND PNG_SHARED) + # Find test PNG files by globbing, but sort lists to ensure + # consistency between different filesystems. + file(GLOB PNGSUITE_PNGS "${CMAKE_CURRENT_SOURCE_DIR}/contrib/pngsuite/*.png") + list(SORT PNGSUITE_PNGS) + file(GLOB TEST_PNGS "${CMAKE_CURRENT_SOURCE_DIR}/contrib/testpngs/*.png") + list(SORT TEST_PNGS) + + set(PNGTEST_PNG "${CMAKE_CURRENT_SOURCE_DIR}/pngtest.png") + + add_executable(pngtest ${pngtest_sources}) + target_link_libraries(pngtest png) + + png_add_test(NAME pngtest COMMAND pngtest FILES "${PNGTEST_PNG}") + + add_executable(pngvalid ${pngvalid_sources}) + target_link_libraries(pngvalid png) + + png_add_test(NAME pngvalid-gamma-16-to-8 + COMMAND pngvalid OPTIONS --gamma-16-to-8) + png_add_test(NAME pngvalid-gamma-alpha-mode + COMMAND pngvalid OPTIONS --gamma-alpha-mode) + png_add_test(NAME pngvalid-gamma-background + COMMAND pngvalid OPTIONS --gamma-background) + png_add_test(NAME pngvalid-gamma-expand16-alpha-mode + COMMAND pngvalid OPTIONS --gamma-alpha-mode --expand16) + png_add_test(NAME pngvalid-gamma-expand16-background + COMMAND pngvalid OPTIONS --gamma-background --expand16) + png_add_test(NAME pngvalid-gamma-expand16-transform + COMMAND pngvalid OPTIONS --gamma-transform --expand16) + png_add_test(NAME pngvalid-gamma-sbit + COMMAND pngvalid OPTIONS --gamma-sbit) + png_add_test(NAME pngvalid-gamma-threshold + COMMAND pngvalid OPTIONS --gamma-threshold) + png_add_test(NAME pngvalid-gamma-transform + COMMAND pngvalid OPTIONS --gamma-transform) + png_add_test(NAME pngvalid-progressive-interlace-standard + COMMAND pngvalid OPTIONS --standard --progressive-read --interlace) + png_add_test(NAME pngvalid-progressive-size + COMMAND pngvalid OPTIONS --size --progressive-read) + png_add_test(NAME pngvalid-progressive-standard + COMMAND pngvalid OPTIONS --standard --progressive-read) + png_add_test(NAME pngvalid-standard + COMMAND pngvalid OPTIONS --standard) + png_add_test(NAME pngvalid-transform + COMMAND pngvalid OPTIONS --transform) + + add_executable(pngstest ${pngstest_sources}) + target_link_libraries(pngstest png) + + foreach(gamma_type 1.8 linear none sRGB) + foreach(alpha_type none alpha) + set(PNGSTEST_FILES) + foreach(test_png ${TEST_PNGS}) + string(REGEX MATCH ".*-linear[-.].*" TEST_PNG_LINEAR "${test_png}") + string(REGEX MATCH ".*-sRGB[-.].*" TEST_PNG_SRGB "${test_png}") + string(REGEX MATCH ".*-1.8[-.].*" TEST_PNG_G18 "${test_png}") + string(REGEX MATCH ".*-alpha-.*" TEST_PNG_ALPHA "${test_png}") + + set(TEST_PNG_VALID TRUE) + + if(TEST_PNG_ALPHA) + if (NOT "${alpha_type}" STREQUAL "alpha") + set(TEST_PNG_VALID FALSE) + endif() + else() + if ("${alpha_type}" STREQUAL "alpha") + set(TEST_PNG_VALID FALSE) + endif() + endif() + + if(TEST_PNG_LINEAR) + if(NOT "${gamma_type}" STREQUAL "linear") + set(TEST_PNG_VALID FALSE) + endif() + elseif(TEST_PNG_SRGB) + if(NOT "${gamma_type}" STREQUAL "sRGB") + set(TEST_PNG_VALID FALSE) + endif() + elseif(TEST_PNG_G18) + if(NOT "${gamma_type}" STREQUAL "1.8") + set(TEST_PNG_VALID FALSE) + endif() + else() + if(NOT "${gamma_type}" STREQUAL "none") + set(TEST_PNG_VALID FALSE) + endif() + endif() + + if(TEST_PNG_VALID) + list(APPEND PNGSTEST_FILES "${test_png}") + endif() + endforeach() + # Should already be sorted, but sort anyway to be certain. + list(SORT PNGSTEST_FILES) + png_add_test(NAME pngstest-${gamma_type}-${alpha_type} + COMMAND pngstest + OPTIONS --tmpfile "${gamma_type}-${alpha_type}-" --log + FILES ${PNGSTEST_FILES}) + endforeach() + endforeach() + + add_executable(pngunknown ${pngunknown_sources}) + target_link_libraries(pngunknown png) + + png_add_test(NAME pngunknown-discard COMMAND pngunknown OPTIONS --strict default=discard FILES "${PNGTEST_PNG}") + png_add_test(NAME pngunknown-IDAT COMMAND pngunknown OPTIONS --strict default=discard IDAT=save FILES "${PNGTEST_PNG}") + png_add_test(NAME pngunknown-if-safe COMMAND pngunknown OPTIONS --strict default=if-safe FILES "${PNGTEST_PNG}") + png_add_test(NAME pngunknown-sAPI COMMAND pngunknown OPTIONS --strict bKGD=save cHRM=save gAMA=save all=discard iCCP=save sBIT=save sRGB=save FILES "${PNGTEST_PNG}") + png_add_test(NAME pngunknown-save COMMAND pngunknown OPTIONS --strict default=save FILES "${PNGTEST_PNG}") + png_add_test(NAME pngunknown-sTER COMMAND pngunknown OPTIONS --strict sTER=if-safe FILES "${PNGTEST_PNG}") + png_add_test(NAME pngunknown-vpAg COMMAND pngunknown OPTIONS --strict vpAg=if-safe FILES "${PNGTEST_PNG}") + + add_executable(pngimage ${pngimage_sources}) + target_link_libraries(pngimage png) + + png_add_test(NAME pngimage-quick COMMAND pngimage OPTIONS --list-combos --log FILES ${PNGSUITE_PNGS}) + png_add_test(NAME pngimage-full COMMAND pngimage OPTIONS --exhaustive --list-combos --log FILES ${PNGSUITE_PNGS}) +endif() + +if(PNG_SHARED) + add_executable(pngfix ${pngfix_sources}) + target_link_libraries(pngfix png) + set(PNG_BIN_TARGETS pngfix) + + add_executable(png-fix-itxt ${png_fix_itxt_sources}) + target_link_libraries(png-fix-itxt ${ZLIB_LIBRARY} ${M_LIBRARY}) + list(APPEND PNG_BIN_TARGETS png-fix-itxt) +endif() + +# Set a variable with CMake code which: +# Creates a symlink from src to dest (if possible) or alternatively +# copies if different. +include(CMakeParseArguments) + +function(CREATE_SYMLINK DEST_FILE) + + cmake_parse_arguments(S "" "FILE;TARGET" "" ${ARGN}) + + if(NOT S_TARGET AND NOT S_FILE) + message(FATAL_ERROR "Specify either a TARGET or a FILE for CREATE_SYMLINK to link to.") + endif(NOT S_TARGET AND NOT S_FILE) + + if(S_TARGET AND S_FILE) + message(FATAL_ERROR "CREATE_SYMLINK called with both source file ${S_FILE} and build target ${S_TARGET} arguments - can only handle 1 type per call.") + endif(S_TARGET AND S_FILE) + + if(S_FILE) + # If we don't need to symlink something that's coming from a build target, + # we can go ahead and symlink/copy at configure time. + + if(CMAKE_HOST_WIN32 AND NOT CYGWIN AND NOT MSYS) + execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy_if_different ${S_FILE} ${DEST_FILE} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + ) + else(CMAKE_HOST_WIN32 AND NOT CYGWIN AND NOT MSYS) + execute_process( + COMMAND ${CMAKE_COMMAND} -E create_symlink ${S_FILE} ${DEST_FILE} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + ) + endif(CMAKE_HOST_WIN32 AND NOT CYGWIN AND NOT MSYS) + endif(S_FILE) + + if(S_TARGET) + # We need to use generator expressions, which can be a bit tricky, so for + # simplicity make the symlink a POST_BUILD step and use the TARGET + # signature of add_custom_command. + + if(CMAKE_HOST_WIN32 AND NOT CYGWIN AND NOT MSYS) + add_custom_command(TARGET ${S_TARGET} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different $ $/${DEST_FILE} + ) + else(CMAKE_HOST_WIN32 AND NOT CYGWIN AND NOT MSYS) + add_custom_command(TARGET ${S_TARGET} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E create_symlink $ $/${DEST_FILE} + ) + endif(CMAKE_HOST_WIN32 AND NOT CYGWIN AND NOT MSYS) + + endif(S_TARGET) + +endfunction() + +# Create source generation scripts. +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/genchk.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/scripts/genchk.cmake @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/genout.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/scripts/genout.cmake @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/gensrc.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/scripts/gensrc.cmake @ONLY) + + +# libpng is a library so default to 'lib' +if(NOT DEFINED CMAKE_INSTALL_LIBDIR) + set(CMAKE_INSTALL_LIBDIR lib) +endif(NOT DEFINED CMAKE_INSTALL_LIBDIR) + +# CREATE PKGCONFIG FILES +# we use the same files like ./configure, so we have to set its vars +# Only do this on Windows for Cygwin - the files don't make much sense outside +# a UNIX look alike +if(NOT WIN32 OR CYGWIN OR MINGW) + set(prefix ${CMAKE_INSTALL_PREFIX}) + set(exec_prefix ${CMAKE_INSTALL_PREFIX}) + set(libdir ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}) + set(includedir ${CMAKE_INSTALL_PREFIX}/include) + set(LIBS "-lz -lm") + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/libpng.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/${PNGLIB_NAME}.pc @ONLY) + CREATE_SYMLINK(libpng.pc FILE ${PNGLIB_NAME}.pc) + + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/libpng-config.in + ${CMAKE_CURRENT_BINARY_DIR}/${PNGLIB_NAME}-config @ONLY) + CREATE_SYMLINK(libpng-config FILE ${PNGLIB_NAME}-config) +endif(NOT WIN32 OR CYGWIN OR MINGW) + +# SET UP LINKS +if(PNG_SHARED) + set_target_properties(png PROPERTIES +# VERSION 16.${PNGLIB_RELEASE}.1.6.35 + VERSION 16.${PNGLIB_RELEASE}.0 + SOVERSION 16 + CLEAN_DIRECT_OUTPUT 1) +endif() + +# If CMake > 2.4.x, we set a variable used below to export +# targets to an export file. +# TODO: Use VERSION_GREATER after our cmake_minimum_required >= 2.6.2 +if(CMAKE_MAJOR_VERSION GREATER 1 AND CMAKE_MINOR_VERSION GREATER 4) + set(PNG_EXPORT_RULE EXPORT libpng) +elseif(CMAKE_MAJOR_VERSION GREATER 2) # future proof + set(PNG_EXPORT_RULE EXPORT libpng) +endif() + +# INSTALL +if(NOT SKIP_INSTALL_LIBRARIES AND NOT SKIP_INSTALL_ALL ) + install(TARGETS ${PNG_LIB_TARGETS} + ${PNG_EXPORT_RULE} + RUNTIME DESTINATION bin + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + FRAMEWORK DESTINATION ${CMAKE_INSTALL_LIBDIR}) + + if(PNG_SHARED) + # Create a symlink for libpng.dll.a => libpng16.dll.a on Cygwin + if(CYGWIN OR MINGW) + CREATE_SYMLINK(libpng${CMAKE_IMPORT_LIBRARY_SUFFIX} TARGET png) + install(FILES $/libpng${CMAKE_IMPORT_LIBRARY_SUFFIX} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + endif(CYGWIN OR MINGW) + + if(NOT WIN32) + CREATE_SYMLINK(libpng${CMAKE_SHARED_LIBRARY_SUFFIX} TARGET png) + install(FILES $/libpng${CMAKE_SHARED_LIBRARY_SUFFIX} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + endif(NOT WIN32) + endif(PNG_SHARED) + + if(PNG_STATIC) + if(NOT WIN32 OR CYGWIN OR MINGW) + CREATE_SYMLINK( libpng${CMAKE_STATIC_LIBRARY_SUFFIX} TARGET png_static) + install(FILES $/libpng${CMAKE_STATIC_LIBRARY_SUFFIX} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + endif(NOT WIN32 OR CYGWIN OR MINGW) + endif() +endif() + +if(NOT SKIP_INSTALL_HEADERS AND NOT SKIP_INSTALL_ALL ) + install(FILES ${libpng_public_hdrs} DESTINATION include) + install(FILES ${libpng_public_hdrs} DESTINATION include/${PNGLIB_NAME}) +endif() +if(NOT SKIP_INSTALL_EXECUTABLES AND NOT SKIP_INSTALL_ALL ) + if(NOT WIN32 OR CYGWIN OR MINGW) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/libpng-config DESTINATION bin) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${PNGLIB_NAME}-config + DESTINATION bin) + endif(NOT WIN32 OR CYGWIN OR MINGW) +endif() + +if(NOT SKIP_INSTALL_PROGRAMS AND NOT SKIP_INSTALL_ALL ) + install(TARGETS ${PNG_BIN_TARGETS} + RUNTIME DESTINATION bin) +endif() + +if(NOT SKIP_INSTALL_FILES AND NOT SKIP_INSTALL_ALL ) + # Install man pages + if(NOT PNG_MAN_DIR) + set(PNG_MAN_DIR "share/man") + endif() + install(FILES libpng.3 libpngpf.3 DESTINATION ${PNG_MAN_DIR}/man3) + install(FILES png.5 DESTINATION ${PNG_MAN_DIR}/man5) + # Install pkg-config files + if(NOT CMAKE_HOST_WIN32 OR CYGWIN OR MINGW) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libpng.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/libpng-config + DESTINATION bin) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PNGLIB_NAME}.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${PNGLIB_NAME}-config + DESTINATION bin) + endif(NOT CMAKE_HOST_WIN32 OR CYGWIN OR MINGW) +endif() + +# On versions of CMake that support it, create an export file CMake +# users can include() to import our targets +if(PNG_EXPORT_RULE AND NOT SKIP_INSTALL_EXPORT AND NOT SKIP_INSTALL_ALL ) + install(EXPORT libpng DESTINATION lib/libpng FILE lib${PNG_LIB_NAME}.cmake) +endif() + +# what's with libpng-manual.txt and all the extra files? + +# UNINSTALL +# do we need this? + +# DIST +# do we need this? + +# to create msvc import lib for mingw compiled shared lib +# pexports libpng.dll > libpng.def +# lib /def:libpng.def /machine:x86 diff --git a/deps/PNG/PNG.cmake b/deps/+PNG/PNG.cmake similarity index 54% rename from deps/PNG/PNG.cmake rename to deps/+PNG/PNG.cmake index 6577eec..edbbeb9 100644 --- a/deps/PNG/PNG.cmake +++ b/deps/+PNG/PNG.cmake @@ -1,31 +1,27 @@ if (APPLE) # Only disable NEON extension for Apple ARM builds, leave it enabled for Raspberry PI. - set(_disable_neon_extension "-DPNG_ARM_NEON=off") + set(_disable_neon_extension "-DPNG_ARM_NEON:STRING=off") else () set(_disable_neon_extension "") endif () -set(_patch_step "") +set(_patch_cmd PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.patched CMakeLists.txt) + if (APPLE) - set(_patch_step PATCH_COMMAND ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/PNG.patch) + set(_patch_cmd ${_patch_cmd} && ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/PNG.patch) endif () -qidislicer_add_cmake_project(PNG - # GIT_REPOSITORY https://github.com/glennrp/libpng.git - # GIT_TAG v1.6.35 +add_cmake_project(PNG URL https://github.com/glennrp/libpng/archive/refs/tags/v1.6.35.zip URL_HASH SHA256=3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f - DEPENDS ${ZLIB_PKG} - "${_patch_step}" + PATCH_COMMAND "${_patch_cmd}" CMAKE_ARGS -DPNG_SHARED=OFF -DPNG_STATIC=ON -DPNG_PREFIX=qidislicer_ -DPNG_TESTS=OFF - -DDISABLE_DEPENDENCY_TRACKING=OFF + -DPNG_EXECUTABLES=OFF ${_disable_neon_extension} ) -if (MSVC) - add_debug_dep(dep_PNG) -endif () +set(DEP_PNG_DEPENDS ZLIB) diff --git a/deps/PNG/PNG.patch b/deps/+PNG/PNG.patch similarity index 100% rename from deps/PNG/PNG.patch rename to deps/+PNG/PNG.patch diff --git a/deps/+Qhull/Qhull.cmake b/deps/+Qhull/Qhull.cmake new file mode 100644 index 0000000..011f58e --- /dev/null +++ b/deps/+Qhull/Qhull.cmake @@ -0,0 +1,19 @@ +include(GNUInstallDirs) + +set(_qhull_static_libs "-DBUILD_STATIC_LIBS:BOOL=ON") +set(_qhull_shared_libs "-DBUILD_SHARED_LIBS:BOOL=OFF") +if (BUILD_SHARED_LIBS) + set(_qhull_static_libs "-DBUILD_STATIC_LIBS:BOOL=OFF") + set(_qhull_shared_libs "-DBUILD_SHARED_LIBS:BOOL=ON") +endif () + +add_cmake_project(Qhull + URL "https://github.com/qhull/qhull/archive/refs/tags/v8.1-alpha3.zip" + URL_HASH SHA256=7bd9b5ffae01e69c2ead52f9a9b688af6c65f9a1da05da0a170fa20d81404c06 + CMAKE_ARGS + -DINCLUDE_INSTALL_DIR=${CMAKE_INSTALL_INCLUDEDIR} + -DBUILD_APPLICATIONS:BOOL=OFF + ${_qhull_shared_libs} + ${_qhull_static_libs} + -DQHULL_ENABLE_TESTING:BOOL=OFF +) diff --git a/deps/TBB/TBB.cmake b/deps/+TBB/TBB.cmake similarity index 73% rename from deps/TBB/TBB.cmake rename to deps/+TBB/TBB.cmake index 042f5f0..95ac76b 100644 --- a/deps/TBB/TBB.cmake +++ b/deps/+TBB/TBB.cmake @@ -1,16 +1,12 @@ -qidislicer_add_cmake_project( +add_cmake_project( TBB URL "https://github.com/oneapi-src/oneTBB/archive/refs/tags/v2021.5.0.zip" URL_HASH SHA256=83ea786c964a384dd72534f9854b419716f412f9d43c0be88d41874763e7bb47 CMAKE_ARGS - -DTBB_BUILD_SHARED=OFF + -DTBB_BUILD_SHARED=${BUILD_SHARED_LIBS} -DTBB_TEST=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_DEBUG_POSTFIX=_debug ) -if (MSVC) - add_debug_dep(dep_TBB) -endif () - diff --git a/deps/+TIFF/TIFF.cmake b/deps/+TIFF/TIFF.cmake new file mode 100644 index 0000000..9281dd5 --- /dev/null +++ b/deps/+TIFF/TIFF.cmake @@ -0,0 +1,15 @@ +add_cmake_project(TIFF + URL https://gitlab.com/libtiff/libtiff/-/archive/v4.6.0/libtiff-v4.6.0.zip + URL_HASH SHA256=5d652432123223338a6ee642a6499d98ebc5a702f8a065571e1001d4c08c37e6 + CMAKE_ARGS + -Dtiff-tools:BOOL=OFF + -Dtiff-tests:BOOL=OFF + -Dlzma:BOOL=OFF + -Dwebp:BOOL=OFF + -Djbig:BOOL=OFF + -Dzstd:BOOL=OFF + -Dpixarlog:BOOL=OFF + -Dlibdeflate:BOOL=OFF +) + +set(DEP_TIFF_DEPENDS ZLIB PNG JPEG OpenGL) diff --git a/deps/WebView2/include/WebView2.h b/deps/+WebView2/include/WebView2.h similarity index 100% rename from deps/WebView2/include/WebView2.h rename to deps/+WebView2/include/WebView2.h diff --git a/deps/WebView2/include/WebView2EnvironmentOptions.h b/deps/+WebView2/include/WebView2EnvironmentOptions.h similarity index 100% rename from deps/WebView2/include/WebView2EnvironmentOptions.h rename to deps/+WebView2/include/WebView2EnvironmentOptions.h diff --git a/deps/WebView2/lib/win32/WebView2Loader.dll b/deps/+WebView2/lib/win32/WebView2Loader.dll similarity index 100% rename from deps/WebView2/lib/win32/WebView2Loader.dll rename to deps/+WebView2/lib/win32/WebView2Loader.dll diff --git a/deps/WebView2/lib/win32/WebView2Loader.dll.lib b/deps/+WebView2/lib/win32/WebView2Loader.dll.lib similarity index 100% rename from deps/WebView2/lib/win32/WebView2Loader.dll.lib rename to deps/+WebView2/lib/win32/WebView2Loader.dll.lib diff --git a/deps/WebView2/lib/win32/WebView2LoaderStatic.lib b/deps/+WebView2/lib/win32/WebView2LoaderStatic.lib similarity index 100% rename from deps/WebView2/lib/win32/WebView2LoaderStatic.lib rename to deps/+WebView2/lib/win32/WebView2LoaderStatic.lib diff --git a/deps/WebView2/lib/win64/WebView2Loader.dll b/deps/+WebView2/lib/win64/WebView2Loader.dll similarity index 100% rename from deps/WebView2/lib/win64/WebView2Loader.dll rename to deps/+WebView2/lib/win64/WebView2Loader.dll diff --git a/deps/WebView2/lib/win64/WebView2Loader.dll.lib b/deps/+WebView2/lib/win64/WebView2Loader.dll.lib similarity index 100% rename from deps/WebView2/lib/win64/WebView2Loader.dll.lib rename to deps/+WebView2/lib/win64/WebView2Loader.dll.lib diff --git a/deps/WebView2/lib/win64/WebView2LoaderStatic.lib b/deps/+WebView2/lib/win64/WebView2LoaderStatic.lib similarity index 100% rename from deps/WebView2/lib/win64/WebView2LoaderStatic.lib rename to deps/+WebView2/lib/win64/WebView2LoaderStatic.lib diff --git a/deps/ZLIB/0001-Respect-BUILD_SHARED_LIBS.patch b/deps/+ZLIB/0001-Respect-BUILD_SHARED_LIBS.patch similarity index 100% rename from deps/ZLIB/0001-Respect-BUILD_SHARED_LIBS.patch rename to deps/+ZLIB/0001-Respect-BUILD_SHARED_LIBS.patch diff --git a/deps/ZLIB/ZLIB.cmake b/deps/+ZLIB/ZLIB.cmake similarity index 77% rename from deps/ZLIB/ZLIB.cmake rename to deps/+ZLIB/ZLIB.cmake index 35a8588..60ba8c7 100644 --- a/deps/ZLIB/ZLIB.cmake +++ b/deps/+ZLIB/ZLIB.cmake @@ -1,6 +1,4 @@ -qidislicer_add_cmake_project(ZLIB - # GIT_REPOSITORY https://github.com/madler/zlib.git - # GIT_TAG v1.2.11 +add_cmake_project(ZLIB URL https://github.com/madler/zlib/archive/refs/tags/v1.2.11.zip URL_HASH SHA256=f5cc4ab910db99b2bdbba39ebbdc225ffc2aa04b4057bc2817f1b94b6978cfc3 PATCH_COMMAND ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/0001-Respect-BUILD_SHARED_LIBS.patch diff --git a/deps/+heatshrink/CMakeLists.txt b/deps/+heatshrink/CMakeLists.txt new file mode 100644 index 0000000..f304a48 --- /dev/null +++ b/deps/+heatshrink/CMakeLists.txt @@ -0,0 +1,93 @@ +cmake_minimum_required(VERSION 3.10) + +project(heatshrink LANGUAGES C VERSION 0.4.1) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + +add_library(${PROJECT_NAME} heatshrink_decoder.c heatshrink_encoder.c) +add_library(${PROJECT_NAME}_dynalloc heatshrink_decoder.c heatshrink_encoder.c) + +find_library(MATH_LIBRARY m) # Business as usual +if(MATH_LIBRARY) + target_link_libraries(${PROJECT_NAME} PUBLIC ${MATH_LIBRARY}) +endif() + +target_include_directories(${PROJECT_NAME} PUBLIC $) +target_include_directories(${PROJECT_NAME}_dynalloc PUBLIC $) + +target_compile_definitions(${PROJECT_NAME} PUBLIC HEATSHRINK_DYNAMIC_ALLOC=0) +target_compile_definitions(${PROJECT_NAME}_dynalloc PUBLIC HEATSHRINK_DYNAMIC_ALLOC=1) + +if (UNIX) + add_executable(${PROJECT_NAME}_cmd heatshrink.c) + target_link_libraries(${PROJECT_NAME}_cmd ${PROJECT_NAME}_dynalloc) + set_target_properties(${PROJECT_NAME}_cmd PROPERTIES OUTPUT_NAME ${PROJECT_NAME}) +endif () + +foreach (tgt ${PROJECT_NAME} ${PROJECT_NAME}_dynalloc) + set_target_properties(${tgt} + PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION}) +endforeach() + +# Installation and export: + +include(CMakePackageConfigHelpers) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY AnyNewerVersion +) + +set(_exported_targets ${PROJECT_NAME} ${PROJECT_NAME}_dynalloc) +if (UNIX) + list(APPEND _exported_targets ${PROJECT_NAME}_cmd) +endif () + +install(TARGETS ${_exported_targets} + EXPORT ${PROJECT_NAME}Targets +) + +export(EXPORT ${PROJECT_NAME}Targets + FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Targets.cmake" + NAMESPACE ${PROJECT_NAME}:: +) + +include(GNUInstallDirs) +set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + INSTALL_DESTINATION ${ConfigPackageLocation} +) + +install( + FILES + heatshrink_common.h + heatshrink_config.h + heatshrink_encoder.h + heatshrink_decoder.h + DESTINATION + include/${PROJECT_NAME} + ) + +install(EXPORT ${PROJECT_NAME}Targets + FILE + ${PROJECT_NAME}Targets.cmake + NAMESPACE + ${PROJECT_NAME}:: + DESTINATION + ${ConfigPackageLocation} +) + +install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + DESTINATION + ${ConfigPackageLocation} +) \ No newline at end of file diff --git a/deps/+heatshrink/Config.cmake.in b/deps/+heatshrink/Config.cmake.in new file mode 100644 index 0000000..7ace8c5 --- /dev/null +++ b/deps/+heatshrink/Config.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +if (EXISTS ${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake) + include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +endif () \ No newline at end of file diff --git a/deps/+heatshrink/heatshrink.cmake b/deps/+heatshrink/heatshrink.cmake new file mode 100644 index 0000000..08917a6 --- /dev/null +++ b/deps/+heatshrink/heatshrink.cmake @@ -0,0 +1,6 @@ +add_cmake_project(heatshrink + URL https://github.com/atomicobject/heatshrink/archive/refs/tags/v0.4.1.zip + URL_HASH SHA256=2e2db2366bdf36cb450f0b3229467cbc6ea81a8c690723e4227b0b46f92584fe + PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt ./CMakeLists.txt && + ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/Config.cmake.in ./Config.cmake.in +) \ No newline at end of file diff --git a/deps/wxWidgets/0001-wxWidget-fix.patch b/deps/+wxWidgets/0001-wxWidget-fix.patch similarity index 100% rename from deps/wxWidgets/0001-wxWidget-fix.patch rename to deps/+wxWidgets/0001-wxWidget-fix.patch diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/+wxWidgets/wxWidgets.cmake similarity index 86% rename from deps/wxWidgets/wxWidgets.cmake rename to deps/+wxWidgets/wxWidgets.cmake index 2ab56e3..a8385ec 100644 --- a/deps/wxWidgets/wxWidgets.cmake +++ b/deps/+wxWidgets/wxWidgets.cmake @@ -2,6 +2,8 @@ set(_wx_git_tag v3.2.0) set(_wx_toolkit "") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + option(DEP_WX_GTK3 "Build wxWidgets for GTK3 instead of GTK2" OFF) + set(_gtk_ver 2) if (DEP_WX_GTK3) set(_gtk_ver 3) @@ -29,18 +31,13 @@ if (UNIX AND NOT APPLE) # wxWidgets will not use char as the underlying type for set (_unicode_utf8 ON) endif() -qidislicer_add_cmake_project(wxWidgets +add_cmake_project(wxWidgets URL https://github.com/prusa3d/wxWidgets/archive/78aa2dc0ea7ce99dc19adc1140f74c3e2e3f3a26.zip URL_HASH SHA256=94b7d972373503e380e5a8b0ca63b1ccb956da4006402298dd89a0c5c7041b1e - #PATCH_COMMAND ${_patch_cmd} - DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} dep_TIFF dep_JPEG dep_NanoSVG CMAKE_ARGS + "-DCMAKE_DEBUG_POSTFIX:STRING=" -DwxBUILD_PRECOMP=ON ${_wx_toolkit} - "-DCMAKE_DEBUG_POSTFIX:STRING=" - -DwxBUILD_DEBUG_LEVEL=0 - -DwxBUILD_SAMPLES=OFF - -DwxBUILD_SHARED=OFF -DwxUSE_MEDIACTRL=ON -DwxUSE_DETECT_SM=OFF -DwxUSE_UNICODE=ON @@ -67,6 +64,4 @@ qidislicer_add_cmake_project(wxWidgets -DwxUSE_WEBREQUEST=OFF ) -if (MSVC) - add_debug_dep(dep_wxWidgets) -endif () \ No newline at end of file +set(DEP_wxWidgets_DEPENDS ZLIB PNG EXPAT TIFF JPEG NanoSVG) diff --git a/deps/Blosc/Blosc.cmake b/deps/Blosc/Blosc.cmake deleted file mode 100644 index 62a7c82..0000000 --- a/deps/Blosc/Blosc.cmake +++ /dev/null @@ -1,28 +0,0 @@ -if(BUILD_SHARED_LIBS) - set(_build_shared ON) - set(_build_static OFF) -else() - set(_build_shared OFF) - set(_build_static ON) -endif() - -qidislicer_add_cmake_project(Blosc - #URL https://github.com/Blosc/c-blosc/archive/refs/tags/v1.17.0.zip - #URL_HASH SHA256=7463a1df566704f212263312717ab2c36b45d45cba6cd0dccebf91b2cc4b4da9 - URL https://github.com/tamasmeszaros/c-blosc/archive/refs/heads/v1.17.0_tm.zip - URL_HASH SHA256=dcb48bf43a672fa3de6a4b1de2c4c238709dad5893d1e097b8374ad84b1fc3b3 - DEPENDS ${ZLIB_PKG} - # Patching upstream does not work this way with git version 2.28 installed on mac worker - # PATCH_COMMAND ${GIT_EXECUTABLE} apply --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/blosc-mods.patch - CMAKE_ARGS - -DCMAKE_POSITION_INDEPENDENT_CODE=ON - -DBUILD_SHARED=${_build_shared} - -DBUILD_STATIC=${_build_static} - -DBUILD_TESTS=OFF - -DBUILD_BENCHMARKS=OFF - -DPREFER_EXTERNAL_ZLIB=ON -) - -if (MSVC) - add_debug_dep(dep_Blosc) -endif () \ No newline at end of file diff --git a/deps/Blosc/blosc-mods.patch b/deps/Blosc/blosc-mods.patch deleted file mode 100644 index 9b1b9cb..0000000 --- a/deps/Blosc/blosc-mods.patch +++ /dev/null @@ -1,469 +0,0 @@ -From 7cf6c014a36f1712efbdbe9bc52d2d4922b54673 Mon Sep 17 00:00:00 2001 -From: tamasmeszaros -Date: Wed, 30 Oct 2019 12:54:52 +0100 -Subject: [PATCH] Blosc 1.17 fixes and cmake config script - -Signed-off-by: tamasmeszaros ---- - CMakeLists.txt | 105 +++++++++++++++++----------------- - blosc/CMakeLists.txt | 118 +++++++++------------------------------ - cmake/FindLZ4.cmake | 6 +- - cmake/FindSnappy.cmake | 8 ++- - cmake/FindZstd.cmake | 8 ++- - cmake_config.cmake.in | 24 ++++++++ - internal-complibs/CMakeLists.txt | 35 ++++++++++++ - 7 files changed, 157 insertions(+), 147 deletions(-) - create mode 100644 cmake_config.cmake.in - create mode 100644 internal-complibs/CMakeLists.txt - -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 59d9fab..e9134c2 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -71,7 +71,7 @@ - # DEV: static includes blosc.a and blosc.h - - --cmake_minimum_required(VERSION 2.8.12) -+cmake_minimum_required(VERSION 3.1) # Threads::Threads target available from 3.1 - if (NOT CMAKE_VERSION VERSION_LESS 3.3) - cmake_policy(SET CMP0063 NEW) - endif() -@@ -124,55 +124,30 @@ option(PREFER_EXTERNAL_ZSTD - - set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") - -- --if(NOT DEACTIVATE_LZ4) -- if(PREFER_EXTERNAL_LZ4) -- find_package(LZ4) -- else() -- message(STATUS "Using LZ4 internal sources.") -- endif(PREFER_EXTERNAL_LZ4) -- # HAVE_LZ4 will be set to true because even if the library is -- # not found, we will use the included sources for it -- set(HAVE_LZ4 TRUE) --endif(NOT DEACTIVATE_LZ4) -- --if(NOT DEACTIVATE_SNAPPY) -- if(PREFER_EXTERNAL_SNAPPY) -- find_package(Snappy) -- else() -- message(STATUS "Using Snappy internal sources.") -- endif(PREFER_EXTERNAL_SNAPPY) -- # HAVE_SNAPPY will be set to true because even if the library is not found, -- # we will use the included sources for it -- set(HAVE_SNAPPY TRUE) --endif(NOT DEACTIVATE_SNAPPY) -- --if(NOT DEACTIVATE_ZLIB) -- # import the ZLIB_ROOT environment variable to help finding the zlib library -- if(PREFER_EXTERNAL_ZLIB) -- set(ZLIB_ROOT $ENV{ZLIB_ROOT}) -- find_package(ZLIB) -- if (NOT ZLIB_FOUND ) -- message(STATUS "No zlib found. Using internal sources.") -- endif (NOT ZLIB_FOUND ) -- else() -- message(STATUS "Using zlib internal sources.") -- endif(PREFER_EXTERNAL_ZLIB) -- # HAVE_ZLIB will be set to true because even if the library is not found, -- # we will use the included sources for it -- set(HAVE_ZLIB TRUE) --endif(NOT DEACTIVATE_ZLIB) -- --if (NOT DEACTIVATE_ZSTD) -- if (PREFER_EXTERNAL_ZSTD) -- find_package(Zstd) -- else () -- message(STATUS "Using ZSTD internal sources.") -- endif (PREFER_EXTERNAL_ZSTD) -- # HAVE_ZSTD will be set to true because even if the library is -- # not found, we will use the included sources for it -- set(HAVE_ZSTD TRUE) --endif (NOT DEACTIVATE_ZSTD) -+set(LIBS "") -+macro(use_package _pkg _tgt) -+ string(TOUPPER ${_pkg} _PKG) -+ if(NOT DEACTIVATE_${_PKG}) -+ if(PREFER_EXTERNAL_${_PKG}) -+ find_package(${_pkg}) -+ if (NOT ${_pkg}_FOUND ) -+ message(STATUS "No ${_pkg} found. Using internal sources.") -+ endif() -+ else() -+ message(STATUS "Using ${_pkg} internal sources.") -+ endif(PREFER_EXTERNAL_${_PKG}) -+ # HAVE_${_pkg} will be set to true because even if the library is -+ # not found, we will use the included sources for it -+ set(HAVE_${_PKG} TRUE) -+ list(APPEND LIBS ${_pkg}::${_tgt}) -+ endif(NOT DEACTIVATE_${_PKG}) -+endmacro() -+ -+set(ZLIB_ROOT $ENV{ZLIB_ROOT}) -+use_package(ZLIB ZLIB) -+use_package(LZ4 LZ4) -+use_package(Snappy snappy) -+use_package(Zstd Zstd) - - # create the config.h file - configure_file ("blosc/config.h.in" "blosc/config.h" ) -@@ -316,6 +291,7 @@ endif() - - - # subdirectories -+add_subdirectory(internal-complibs) - add_subdirectory(blosc) - - if(BUILD_TESTS) -@@ -328,7 +304,6 @@ if(BUILD_BENCHMARKS) - add_subdirectory(bench) - endif(BUILD_BENCHMARKS) - -- - # uninstall target - if (BLOSC_INSTALL) - configure_file( -@@ -338,10 +313,38 @@ if (BLOSC_INSTALL) - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/blosc.pc" - DESTINATION lib/pkgconfig COMPONENT DEV) - -+ configure_file( -+ "${CMAKE_CURRENT_SOURCE_DIR}/cmake_config.cmake.in" -+ "${CMAKE_CURRENT_BINARY_DIR}/cmakeexports/BloscConfig.cmake" -+ @ONLY) -+ - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" - IMMEDIATE @ONLY) -+ -+ include(CMakePackageConfigHelpers) -+ write_basic_package_version_file( -+ "${CMAKE_CURRENT_BINARY_DIR}/cmakeexports/BloscConfigVersion.cmake" -+ VERSION ${BLOSC_VERSION_MAJOR}.${BLOSC_VERSION_MINOR}.${BLOSC_VERSION_PATCH} -+ COMPATIBILITY AnyNewerVersion -+ ) -+ -+ export(EXPORT BloscTargets -+ FILE "${CMAKE_CURRENT_BINARY_DIR}/cmakeexports/BloscTargets.cmake" -+ NAMESPACE Blosc::) -+ -+ install(EXPORT BloscTargets -+ FILE BloscTargets.cmake -+ NAMESPACE Blosc:: -+ DESTINATION lib/cmake/Blosc -+ EXPORT_LINK_INTERFACE_LIBRARIES) -+ -+ install(FILES -+ "${CMAKE_CURRENT_BINARY_DIR}/cmakeexports/BloscConfig.cmake" -+ "${CMAKE_CURRENT_BINARY_DIR}/cmakeexports/BloscConfigVersion.cmake" -+ DESTINATION lib/cmake/Blosc COMPONENT DEV) -+ - add_custom_target(uninstall - COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) - endif() -diff --git a/blosc/CMakeLists.txt b/blosc/CMakeLists.txt -index 1d1bebe..f554abe 100644 ---- a/blosc/CMakeLists.txt -+++ b/blosc/CMakeLists.txt -@@ -1,52 +1,11 @@ - # a simple way to detect that we are using CMAKE - add_definitions(-DUSING_CMAKE) - --set(INTERNAL_LIBS ${PROJECT_SOURCE_DIR}/internal-complibs) -- - # Hide symbols by default unless they're specifically exported. - # This makes it easier to keep the set of exported symbols the - # same across all compilers/platforms. - set(CMAKE_C_VISIBILITY_PRESET hidden) - --# includes --set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}) --if(NOT DEACTIVATE_LZ4) -- if (LZ4_FOUND) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${LZ4_INCLUDE_DIR}) -- else(LZ4_FOUND) -- set(LZ4_LOCAL_DIR ${INTERNAL_LIBS}/lz4-1.9.1) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${LZ4_LOCAL_DIR}) -- endif(LZ4_FOUND) --endif(NOT DEACTIVATE_LZ4) -- --if(NOT DEACTIVATE_SNAPPY) -- if (SNAPPY_FOUND) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${SNAPPY_INCLUDE_DIR}) -- else(SNAPPY_FOUND) -- set(SNAPPY_LOCAL_DIR ${INTERNAL_LIBS}/snappy-1.1.1) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${SNAPPY_LOCAL_DIR}) -- endif(SNAPPY_FOUND) --endif(NOT DEACTIVATE_SNAPPY) -- --if(NOT DEACTIVATE_ZLIB) -- if (ZLIB_FOUND) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIR}) -- else(ZLIB_FOUND) -- set(ZLIB_LOCAL_DIR ${INTERNAL_LIBS}/zlib-1.2.8) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${ZLIB_LOCAL_DIR}) -- endif(ZLIB_FOUND) --endif(NOT DEACTIVATE_ZLIB) -- --if (NOT DEACTIVATE_ZSTD) -- if (ZSTD_FOUND) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${ZSTD_INCLUDE_DIR}) -- else (ZSTD_FOUND) -- set(ZSTD_LOCAL_DIR ${INTERNAL_LIBS}/zstd-1.4.1) -- set(BLOSC_INCLUDE_DIRS ${BLOSC_INCLUDE_DIRS} ${ZSTD_LOCAL_DIR} ${ZSTD_LOCAL_DIR}/common) -- endif (ZSTD_FOUND) --endif (NOT DEACTIVATE_ZSTD) -- --include_directories(${BLOSC_INCLUDE_DIRS}) - - # library sources - set(SOURCES blosc.c blosclz.c fastcopy.c shuffle-generic.c bitshuffle-generic.c -@@ -73,53 +32,13 @@ if(WIN32) - message(STATUS "using the internal pthread library for win32 systems.") - set(SOURCES ${SOURCES} win32/pthread.c) - else(NOT Threads_FOUND) -- set(LIBS ${LIBS} ${CMAKE_THREAD_LIBS_INIT}) -+ list(APPEND LIBS Threads::Threads) - endif(NOT Threads_FOUND) - else(WIN32) - find_package(Threads REQUIRED) -- set(LIBS ${LIBS} ${CMAKE_THREAD_LIBS_INIT}) -+ list(APPEND LIBS Threads::Threads) - endif(WIN32) - --if(NOT DEACTIVATE_LZ4) -- if(LZ4_FOUND) -- set(LIBS ${LIBS} ${LZ4_LIBRARY}) -- else(LZ4_FOUND) -- file(GLOB LZ4_FILES ${LZ4_LOCAL_DIR}/*.c) -- set(SOURCES ${SOURCES} ${LZ4_FILES}) -- endif(LZ4_FOUND) --endif(NOT DEACTIVATE_LZ4) -- --if(NOT DEACTIVATE_SNAPPY) -- if(SNAPPY_FOUND) -- set(LIBS ${LIBS} ${SNAPPY_LIBRARY}) -- else(SNAPPY_FOUND) -- file(GLOB SNAPPY_FILES ${SNAPPY_LOCAL_DIR}/*.cc) -- set(SOURCES ${SOURCES} ${SNAPPY_FILES}) -- endif(SNAPPY_FOUND) --endif(NOT DEACTIVATE_SNAPPY) -- --if(NOT DEACTIVATE_ZLIB) -- if(ZLIB_FOUND) -- set(LIBS ${LIBS} ${ZLIB_LIBRARY}) -- else(ZLIB_FOUND) -- file(GLOB ZLIB_FILES ${ZLIB_LOCAL_DIR}/*.c) -- set(SOURCES ${SOURCES} ${ZLIB_FILES}) -- endif(ZLIB_FOUND) --endif(NOT DEACTIVATE_ZLIB) -- --if (NOT DEACTIVATE_ZSTD) -- if (ZSTD_FOUND) -- set(LIBS ${LIBS} ${ZSTD_LIBRARY}) -- else (ZSTD_FOUND) -- file(GLOB ZSTD_FILES -- ${ZSTD_LOCAL_DIR}/common/*.c -- ${ZSTD_LOCAL_DIR}/compress/*.c -- ${ZSTD_LOCAL_DIR}/decompress/*.c) -- set(SOURCES ${SOURCES} ${ZSTD_FILES}) -- endif (ZSTD_FOUND) --endif (NOT DEACTIVATE_ZSTD) -- -- - # targets - if (BUILD_SHARED) - add_library(blosc_shared SHARED ${SOURCES}) -@@ -191,14 +110,17 @@ if (BUILD_TESTS) - endif() - endif() - -+add_library(blosc INTERFACE) -+ - if (BUILD_SHARED) -- target_link_libraries(blosc_shared ${LIBS}) -- target_include_directories(blosc_shared PUBLIC ${BLOSC_INCLUDE_DIRS}) -+ target_link_libraries(blosc_shared PRIVATE ${LIBS}) -+ target_include_directories(blosc_shared PUBLIC $) -+ target_link_libraries(blosc INTERFACE blosc_shared) - endif() - - if (BUILD_TESTS) -- target_link_libraries(blosc_shared_testing ${LIBS}) -- target_include_directories(blosc_shared_testing PUBLIC ${BLOSC_INCLUDE_DIRS}) -+ target_link_libraries(blosc_shared_testing PRIVATE ${LIBS}) -+ target_include_directories(blosc_shared_testing PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - endif() - - if(BUILD_STATIC) -@@ -207,17 +129,31 @@ if(BUILD_STATIC) - if (MSVC) - set_target_properties(blosc_static PROPERTIES PREFIX lib) - endif() -- target_link_libraries(blosc_static ${LIBS}) -- target_include_directories(blosc_static PUBLIC ${BLOSC_INCLUDE_DIRS}) -+ # With the static library, cmake has to deal with transitive dependencies -+ target_link_libraries(blosc_static PRIVATE ${LIBS}) -+ target_include_directories(blosc_static PUBLIC $) -+ if (NOT BUILD_SHARED) -+ target_link_libraries(blosc INTERFACE blosc_static) -+ endif() - endif(BUILD_STATIC) - -+ - # install - if(BLOSC_INSTALL) - install(FILES blosc.h blosc-export.h DESTINATION include COMPONENT DEV) -+ set(_inst_libs "blosc") - if(BUILD_SHARED) -- install(TARGETS blosc_shared DESTINATION ${lib_dir} COMPONENT LIB) -+ list(APPEND _inst_libs blosc_shared) - endif(BUILD_SHARED) - if(BUILD_STATIC) -- install(TARGETS blosc_static DESTINATION ${lib_dir} COMPONENT DEV) -+ list(APPEND _inst_libs blosc_static) - endif(BUILD_STATIC) -+ -+ install(TARGETS ${_inst_libs} -+ EXPORT BloscTargets -+ LIBRARY DESTINATION ${lib_dir} -+ ARCHIVE DESTINATION ${lib_dir} -+ RUNTIME DESTINATION bin -+ COMPONENT DEV -+ INCLUDES DESTINATION include) - endif(BLOSC_INSTALL) -diff --git a/cmake/FindLZ4.cmake b/cmake/FindLZ4.cmake -index e581a80..05de6ef 100644 ---- a/cmake/FindLZ4.cmake -+++ b/cmake/FindLZ4.cmake -@@ -5,6 +5,10 @@ find_library(LZ4_LIBRARY NAMES lz4) - if (LZ4_INCLUDE_DIR AND LZ4_LIBRARY) - set(LZ4_FOUND TRUE) - message(STATUS "Found LZ4 library: ${LZ4_LIBRARY}") -+ add_library(LZ4::LZ4 UNKNOWN IMPORTED) -+ set_target_properties(LZ4::LZ4 PROPERTIES -+ IMPORTED_LOCATION ${LZ4_LIBRARY} -+ INTERFACE_INCLUDE_DIRECTORIES ${LZ4_INCLUDE_DIR}) - else () - message(STATUS "No LZ4 library found. Using internal sources.") --endif () -+endif () -\ No newline at end of file -diff --git a/cmake/FindSnappy.cmake b/cmake/FindSnappy.cmake -index 688d4d5..21dbee1 100644 ---- a/cmake/FindSnappy.cmake -+++ b/cmake/FindSnappy.cmake -@@ -3,8 +3,12 @@ find_path(SNAPPY_INCLUDE_DIR snappy-c.h) - find_library(SNAPPY_LIBRARY NAMES snappy) - - if (SNAPPY_INCLUDE_DIR AND SNAPPY_LIBRARY) -- set(SNAPPY_FOUND TRUE) -+ set(Snappy_FOUND TRUE) -+ add_library(Snappy::snappy UNKNOWN IMPORTED) -+ set_target_properties(Snappy::snappy PROPERTIES -+ IMPORTED_LOCATION ${SNAPPY_LIBRARY} -+ INTERFACE_INCLUDE_DIRECTORIES ${SNAPPY_INCLUDE_DIR}) - message(STATUS "Found SNAPPY library: ${SNAPPY_LIBRARY}") - else () - message(STATUS "No snappy found. Using internal sources.") --endif () -+endif () -\ No newline at end of file -diff --git a/cmake/FindZstd.cmake b/cmake/FindZstd.cmake -index 7db4bb9..cabc2f8 100644 ---- a/cmake/FindZstd.cmake -+++ b/cmake/FindZstd.cmake -@@ -3,8 +3,12 @@ find_path(ZSTD_INCLUDE_DIR zstd.h) - find_library(ZSTD_LIBRARY NAMES zstd) - - if (ZSTD_INCLUDE_DIR AND ZSTD_LIBRARY) -- set(ZSTD_FOUND TRUE) -+ set(Zstd_FOUND TRUE) -+ add_library(Zstd::Zstd UNKNOWN IMPORTED) -+ set_target_properties(Zstd::Zstd PROPERTIES -+ IMPORTED_LOCATION ${ZSTD_LIBRARY} -+ INTERFACE_INCLUDE_DIRECTORIES ${ZSTD_INCLUDE_DIR}) - message(STATUS "Found Zstd library: ${ZSTD_LIBRARY}") - else () - message(STATUS "No Zstd library found. Using internal sources.") --endif () -+endif () -\ No newline at end of file -diff --git a/cmake_config.cmake.in b/cmake_config.cmake.in -new file mode 100644 -index 0000000..0f6af24 ---- /dev/null -+++ b/cmake_config.cmake.in -@@ -0,0 +1,24 @@ -+include(CMakeFindDependencyMacro) -+ -+include("${CMAKE_CURRENT_LIST_DIR}/BloscTargets.cmake") -+ -+function(_blosc_remap_configs from_Cfg to_Cfg) -+ string(TOUPPER ${from_Cfg} from_CFG) -+ string(TOLOWER ${from_Cfg} from_cfg) -+ -+ if(NOT EXISTS ${CMAKE_CURRENT_LIST_DIR}/BloscTargets-${from_cfg}.cmake) -+ foreach(tgt IN ITEMS blosc_static blosc_shared blosc) -+ if(TARGET Blosc::${tgt}) -+ set_target_properties(Blosc::${tgt} PROPERTIES -+ MAP_IMPORTED_CONFIG_${from_CFG} ${to_Cfg}) -+ endif() -+ endforeach() -+ endif() -+endfunction() -+ -+# MSVC will try to link RelWithDebInfo or MinSizeRel target with debug config -+# if no matching installation is present which would result in link errors. -+if(MSVC) -+ _blosc_remap_configs(RelWithDebInfo Release) -+ _blosc_remap_configs(MinSizeRel Release) -+endif() -diff --git a/internal-complibs/CMakeLists.txt b/internal-complibs/CMakeLists.txt -new file mode 100644 -index 0000000..4586efa ---- /dev/null -+++ b/internal-complibs/CMakeLists.txt -@@ -0,0 +1,35 @@ -+macro(add_lib_target pkg tgt incdir files) -+ string(TOUPPER ${pkg} TGT) -+ if(NOT DEACTIVATE_${TGT} AND NOT ${pkg}_FOUND) -+ add_library(${tgt}_objs OBJECT ${files}) -+ add_library(${tgt} INTERFACE) -+ target_include_directories(${tgt}_objs PRIVATE $) -+ target_include_directories(${tgt} INTERFACE $) -+ #set_target_properties(${tgt} PROPERTIES INTERFACE_SOURCES "$") -+ set_target_properties(${tgt}_objs PROPERTIES POSITION_INDEPENDENT_CODE ON) -+ target_sources(${tgt} INTERFACE "$>") -+ add_library(${pkg}::${tgt} ALIAS ${tgt}) -+ -+ # This creates dummy (empty) interface targets in the exported config. -+ install(TARGETS ${tgt} EXPORT BloscTargets INCLUDES DESTINATION include) -+ endif() -+ unset(TGT) -+endmacro() -+ -+set(ZLIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/zlib-1.2.8) -+file(GLOB ZLIB_FILES ${ZLIB_DIR}/*.c) -+add_lib_target(ZLIB ZLIB ${ZLIB_DIR} "${ZLIB_FILES}") -+ -+set(SNAPPY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/snappy-1.1.1) -+file(GLOB SNAPPY_FILES ${SNAPPY_DIR}/*.cc) -+add_lib_target(Snappy snappy ${SNAPPY_DIR} "${SNAPPY_FILES}") -+ -+set(LZ4_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lz4-1.9.1) -+file(GLOB LZ4_FILES ${LZ4_DIR}/*.c) -+add_lib_target(LZ4 LZ4 ${LZ4_DIR} "${LZ4_FILES}") -+ -+set(ZSTD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/zstd-1.4.1) -+file(GLOB ZSTD_FILES ${ZSTD_DIR}/common/*.c ${ZSTD_DIR}/compress/*.c ${ZSTD_DIR}/decompress/*.c) -+add_lib_target(Zstd Zstd ${ZSTD_DIR} "${ZSTD_FILES}") -+target_include_directories(Zstd INTERFACE $) -+target_include_directories(Zstd_objs PRIVATE $) -\ No newline at end of file --- -2.16.2.windows.1 - diff --git a/deps/Boost/Boost.cmake b/deps/Boost/Boost.cmake deleted file mode 100644 index 2b44218..0000000 --- a/deps/Boost/Boost.cmake +++ /dev/null @@ -1,168 +0,0 @@ -include(ExternalProject) - -if (WIN32) - set(_bootstrap_cmd bootstrap.bat) - set(_build_cmd b2.exe) -else() - set(_bootstrap_cmd ./bootstrap.sh) - set(_build_cmd ./b2) -endif() - -if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - set(_boost_toolset gcc) - configure_file(${CMAKE_CURRENT_LIST_DIR}/user-config.jam boost-user-config.jam) - set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/boost-user-config.jam ./tools/build/src/tools/user-config.jam) -elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - # https://cmake.org/cmake/help/latest/variable/MSVC_VERSION.html - if (MSVC_VERSION EQUAL 1800) - # 1800 = VS 12.0 (v120 toolset) - set(_boost_toolset "msvc-12.0") - elseif (MSVC_VERSION EQUAL 1900) - # 1900 = VS 14.0 (v140 toolset) - set(_boost_toolset "msvc-14.0") - elseif (MSVC_VERSION LESS 1920) - # 1910-1919 = VS 15.0 (v141 toolset) - set(_boost_toolset "msvc-14.1") - elseif (MSVC_VERSION LESS 1930) - # 1920-1929 = VS 16.0 (v142 toolset) - set(_boost_toolset "msvc-14.2") - elseif (MSVC_VERSION LESS 1940) - # 1930-1939 = VS 17.0 (v143 toolset) - set(_boost_toolset "msvc-14.3") - else () - message(FATAL_ERROR "Unsupported MSVC version") - endif () -elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - if (WIN32) - set(_boost_toolset "clang-win") - elseif (APPLE) - set(_boost_toolset "clang") - else() - set(_boost_toolset clang) - configure_file(${CMAKE_CURRENT_LIST_DIR}/user-config.jam boost-user-config.jam) - set(_patch_command ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/boost-user-config.jam ./tools/build/src/tools/user-config.jam) - endif() -elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Intel") - set(_boost_toolset "intel") -elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") - set(_boost_toolset "clang") -endif() - -message(STATUS "Deduced boost toolset: ${_boost_toolset} based on ${CMAKE_CXX_COMPILER_ID} compiler") - -set(_libs "") -foreach(_comp ${DEP_Boost_COMPONENTS}) - list(APPEND _libs "--with-${_comp}") -endforeach() - -if (BUILD_SHARED_LIBS) - set(_link shared) -else() - set(_link static) -endif() - -set(_bits "") -if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") - set(_bits 64) -elseif ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") - set(_bits 32) -endif () - -include(ProcessorCount) -ProcessorCount(NPROC) -file(TO_NATIVE_PATH ${DESTDIR}/usr/local/ _prefix) - -set(_boost_flags "") -if (UNIX) - set(_boost_flags "cflags=-fPIC;cxxflags=-fPIC") -endif () - -if(APPLE) - set(_boost_flags - "cflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET};" - "cxxflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET};" - "mflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET};" - "mmflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET}") -endif() - -set(_boost_variants "") -if(CMAKE_BUILD_TYPE) - list(APPEND CMAKE_CONFIGURATION_TYPES ${CMAKE_BUILD_TYPE}) - list(REMOVE_DUPLICATES CMAKE_CONFIGURATION_TYPES) -endif() -list(FIND CMAKE_CONFIGURATION_TYPES "Release" _cfg_rel) -list(FIND CMAKE_CONFIGURATION_TYPES "RelWithDebInfo" _cfg_relwdeb) -list(FIND CMAKE_CONFIGURATION_TYPES "MinSizeRel" _cfg_minsizerel) -list(FIND CMAKE_CONFIGURATION_TYPES "Debug" _cfg_deb) - -if (_cfg_rel GREATER -1 OR _cfg_relwdeb GREATER -1 OR _cfg_minsizerel GREATER -1) - list(APPEND _boost_variants release) -endif() - -if ( (NOT MSVC AND _cfg_deb GREATER -1) OR (MSVC AND ${DEP_DEBUG}) ) - list(APPEND _boost_variants debug) -endif() - -if (NOT _boost_variants) - set(_boost_variants release) -endif() - -set(_boost_layout system) -if (MSVC) - set(_boost_layout versioned) -endif () - -set(_build_cmd ${_build_cmd} - ${_boost_flags} - -j${NPROC} - ${_libs} - --layout=${_boost_layout} - --debug-configuration - toolset=${_boost_toolset} - address-model=${_bits} - link=${_link} - threading=multi - boost.locale.icu=off - --disable-icu - ${_boost_variants} - stage) - -set(_install_cmd ${_build_cmd} --prefix=${_prefix} install) - -if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - # When Clang is used with enabled UndefinedBehaviorSanitizer, it produces "undefined reference to '__muloti4'" when __int128 is used. - # Because of that, UndefinedBehaviorSanitizer is disabled for those functions that use __int128. - list(APPEND _patch_command COMMAND ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/Boost.patch) -endif () - -ExternalProject_Add( - dep_Boost - URL "https://boostorg.jfrog.io/artifactory/main/release/1.78.0/source/boost_1_78_0.zip" - URL_HASH SHA256=f22143b5528e081123c3c5ed437e92f648fe69748e95fa6e2bd41484e2986cc3 - DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/Boost - CONFIGURE_COMMAND "${_bootstrap_cmd}" - PATCH_COMMAND ${_patch_command} - BUILD_COMMAND "${_build_cmd}" - BUILD_IN_SOURCE ON - INSTALL_COMMAND "${_install_cmd}" -) - -if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") - # Patch the boost::polygon library with a custom one. - ExternalProject_Add(dep_boost_polygon - EXCLUDE_FROM_ALL ON - # GIT_REPOSITORY "https://github.com/qidi3d/polygon" - # GIT_TAG qidislicer_gmp - URL https://github.com/qidi3d/polygon/archive/refs/heads/qidislicer_gmp.zip - URL_HASH SHA256=abeb9710f0a7069fb9b22181ae5c56f6066002f125db210e7ffb27032aed6824 - DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/boost_polygon - DEPENDS dep_Boost - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CMAKE_CURRENT_BINARY_DIR}/dep_boost_polygon-prefix/src/dep_boost_polygon/include/boost/polygon" - "${DESTDIR}/usr/local/include/boost/polygon" - ) - # Only override boost::Polygon Voronoi implementation with Vojtech's GMP hacks on 64bit platforms. - list(APPEND _dep_list "dep_boost_polygon") -endif () diff --git a/deps/Boost/Boost.patch b/deps/Boost/Boost.patch deleted file mode 100644 index 8c54430..0000000 --- a/deps/Boost/Boost.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff -u ../boost_1_75_0-orig/boost/rational.hpp ./boost/rational.hpp ---- ../boost_1_75_0-orig/boost/rational.hpp 2020-12-03 06:02:19.000000000 +0100 -+++ ./boost/rational.hpp 2022-01-27 16:02:27.993848905 +0100 -@@ -302,6 +302,9 @@ - return *this; - } - template -+ #if defined(__clang__) -+ __attribute__((no_sanitize("undefined"))) -+ #endif - BOOST_CXX14_CONSTEXPR typename boost::enable_if_c::value, rational&>::type operator*= (const T& i) - { - // Avoid overflow and preserve normalization -@@ -311,6 +314,9 @@ - return *this; - } - template -+ #if defined(__clang__) -+ __attribute__((no_sanitize("undefined"))) -+ #endif - BOOST_CXX14_CONSTEXPR typename boost::enable_if_c::value, rational&>::type operator/= (const T& i) - { - // Avoid repeated construction diff --git a/deps/Boost/user-config.jam b/deps/Boost/user-config.jam deleted file mode 100644 index 6d86ef8..0000000 --- a/deps/Boost/user-config.jam +++ /dev/null @@ -1 +0,0 @@ -using @_boost_toolset@ : : @CMAKE_CXX_COMPILER@ ; \ No newline at end of file diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 87beb88..073c92b 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -19,110 +19,77 @@ # WARNING: On UNIX platforms wxWidgets hardcode the destdir path into its `wx-conffig` utility, # therefore, unfortunatelly, the installation cannot be copied/moved elsewhere without re-installing wxWidgets. # +cmake_minimum_required(VERSION 3.12) +project(QIDISlicer_deps) -project(QIDISlicer-deps) -cmake_minimum_required(VERSION 3.2) +# Redefine BUILD_SHARED_LIBS with default being OFF +option(BUILD_SHARED_LIBS "Build shared libraries instead of static (experimental)" OFF) -include(ExternalProject) -include(ProcessorCount) +# List libraries to be excluded from build +set(${PROJECT_NAME}_PACKAGE_EXCLUDES "" CACHE STRING "Exclude packages matching this regex pattern") -ProcessorCount(NPROC) -if (NPROC EQUAL 0) - set(NPROC 1) +# Support legacy parameter DESTDIR +if (DESTDIR) + set(${PROJECT_NAME}_DEP_INSTALL_PREFIX ${DESTDIR}/usr/local CACHE PATH "Destination directory" FORCE) endif () -set(DESTDIR "${CMAKE_CURRENT_BINARY_DIR}/destdir" CACHE PATH "Destination directory") -set(DEP_DOWNLOAD_DIR ${CMAKE_CURRENT_BINARY_DIR} CACHE PATH "Path for downloaded source packages.") - -option(DEP_DEBUG "Build debug variants (only applicable on Windows)" ON) - -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - option(DEP_WX_GTK3 "Build wxWidgets against GTK3" OFF) -endif() - -# On developer machines, it can be enabled to speed up compilation and suppress warnings coming from IGL. -# FIXME: -# Enabling this option is not safe. IGL will compile itself with its own version of Eigen while -# Slic3r compiles with a different version which will cause runtime errors. -# option(DEP_BUILD_IGL_STATIC "Build IGL as a static library. Might cause link errors and increase binary size." OFF) - -message(STATUS "QIDISlicer deps DESTDIR: ${DESTDIR}") -message(STATUS "QIDISlicer dowload dir for source packages: ${DEP_DOWNLOAD_DIR}") -message(STATUS "QIDISlicer deps debug build: ${DEP_DEBUG}") - -find_package(Git REQUIRED) - -# The default command line for patching. Only works for newer -set(PATCH_CMD ${GIT_EXECUTABLE} apply --verbose --ignore-space-change --whitespace=fix) - -get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) - -if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) - message(STATUS "Forcing CMAKE_BUILD_TYPE to Release as it was not specified.") +# Support legacy parameter DEP_DOWNLOAD_DIR +if (DEP_DOWNLOAD_DIR) + set(${PROJECT_NAME}_DEP_DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR} CACHE PATH "Path for downloaded source packages." FORCE) endif () -function(qidislicer_add_cmake_project projectname) - cmake_parse_arguments(P_ARGS "" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN}) - - set(_configs_line -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}) - if (_is_multi OR MSVC) - set(_configs_line "") - endif () - - set(_gen "") - set(_build_j "-j${NPROC}") - if (MSVC) - set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}") - set(_build_j "/m") - if (${projectname} STREQUAL "OCCT") - set(_build_j "/m:1") - endif () - endif () - - ExternalProject_Add( - dep_${projectname} - EXCLUDE_FROM_ALL ON - INSTALL_DIR ${DESTDIR}/usr/local - DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname} - ${_gen} - CMAKE_ARGS - -DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}/usr/local - -DCMAKE_MODULE_PATH:STRING=${PROJECT_SOURCE_DIR}/../cmake/modules - -DCMAKE_PREFIX_PATH:STRING=${DESTDIR}/usr/local - -DCMAKE_DEBUG_POSTFIX:STRING=d - -DCMAKE_C_COMPILER:STRING=${CMAKE_C_COMPILER} - -DCMAKE_CXX_COMPILER:STRING=${CMAKE_CXX_COMPILER} - -DCMAKE_TOOLCHAIN_FILE:STRING=${CMAKE_TOOLCHAIN_FILE} - -DBUILD_SHARED_LIBS:BOOL=OFF - "${_configs_line}" - ${DEP_CMAKE_OPTS} - ${P_ARGS_CMAKE_ARGS} - ${P_ARGS_UNPARSED_ARGUMENTS} - BUILD_COMMAND ${CMAKE_COMMAND} --build . --config Release -- ${_build_j} - INSTALL_COMMAND ${CMAKE_COMMAND} --build . --target install --config Release - ) - -endfunction(qidislicer_add_cmake_project) - +# Slightly controversial +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/../cmake/modules) if (MSVC) - if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") - message(STATUS "\nDetected 64-bit compiler => building 64-bit deps bundle\n") - set(DEPS_BITS 64) - include("deps-windows.cmake") - elseif ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") - message(STATUS "\nDetected 32-bit compiler => building 32-bit deps bundle\n") - set(DEPS_BITS 32) - include("deps-windows.cmake") - else () - message(FATAL_ERROR "Unable to detect architecture") - endif () -elseif (APPLE) - message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}") + option(DEP_DEBUG "Build in debug version of packages automatically" ON) +endif () + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) + cmake_policy(SET CMP0135 NEW) +endif () + +include(${PROJECT_SOURCE_DIR}/../cmake/modules/AddCMakeProject.cmake) + +macro(list_projects result curdir) + file(GLOB children RELATIVE ${curdir} ${curdir}/*) + set(dirlist "") + foreach(child ${children}) + if(IS_DIRECTORY ${curdir}/${child}) + string(REGEX MATCH "^\\+([a-zA-Z0-9]+)" is_package_dir ${child}) + if(is_package_dir AND EXISTS ${curdir}/${child}/${CMAKE_MATCH_1}.cmake) + list(APPEND dirlist ${CMAKE_MATCH_1}) + endif() + endif() + endforeach() + set(${result} ${dirlist}) +endmacro() + +function(dep_message mode msg) + if (NOT DEP_MESSAGES_WRITTEN) + message(${mode} "${msg}") + endif() +endfunction () + +# Always ON options: +if (MSVC) + if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") + dep_message(STATUS "Detected 64-bit compiler => building 64-bit deps bundle") + set(DEPS_BITS 64) + elseif ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") + dep_message(STATUS "Detected 32-bit compiler => building 32-bit deps bundle") + set(DEPS_BITS 32) + else () + dep_message(FATAL_ERROR "Unable to detect architecture!") + endif () +else () + set(DEP_CMAKE_OPTS "-DCMAKE_POSITION_INDEPENDENT_CODE=ON") +endif () + +if (APPLE) if (CMAKE_OSX_DEPLOYMENT_TARGET) set(DEP_OSX_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}") - message("OS X Deployment Target: ${DEP_OSX_TARGET}") + dep_message(STATUS "OS X Deployment Target: ${DEP_OSX_TARGET}") else () # Attempt to infer the SDK version from the CMAKE_OSX_SYSROOT, # this is done because wxWidgets need the min version explicitly set @@ -133,91 +100,198 @@ elseif (APPLE) message(FATAL_ERROR "Could not determine OS X SDK version. Please use -DCMAKE_OSX_DEPLOYMENT_TARGET=") endif () - message("OS X Deployment Target (inferred from SDK): ${DEP_OSX_TARGET}") + dep_message(STATUS "OS X Deployment Target (inferred from SDK): ${DEP_OSX_TARGET}") endif () - include("deps-macos.cmake") -elseif (MINGW) - message(STATUS "Building for MinGW...") - include("deps-mingw.cmake") -else() - include("deps-linux.cmake") -endif() - -set(ZLIB_PKG "") -if (NOT ZLIB_FOUND) - include(ZLIB/ZLIB.cmake) - set(ZLIB_PKG dep_ZLIB) -endif () -set(PNG_PKG "") -if (NOT PNG_FOUND) - include(PNG/PNG.cmake) - set(PNG_PKG dep_PNG) -endif () -set(EXPAT_PKG "") -if (NOT EXPAT_FOUND) - include(EXPAT/EXPAT.cmake) - set(EXPAT_PKG dep_EXPAT) -endif () - -set(DEP_Boost_COMPONENTS system iostreams filesystem thread log locale regex date_time) -include(Boost/Boost.cmake) - -# The order of includes respects the dependencies between libraries -include(Cereal/Cereal.cmake) -include(Qhull/Qhull.cmake) -include(GLEW/GLEW.cmake) -include(OpenCSG/OpenCSG.cmake) - -include(TBB/TBB.cmake) - -include(Blosc/Blosc.cmake) -include(OpenEXR/OpenEXR.cmake) -include(OpenVDB/OpenVDB.cmake) - -include(GMP/GMP.cmake) -include(MPFR/MPFR.cmake) -include(CGAL/CGAL.cmake) - -include(NLopt/NLopt.cmake) - -include(OpenSSL/OpenSSL.cmake) - -set(CURL_PKG "") -if (NOT CURL_FOUND) - include(CURL/CURL.cmake) - set(CURL_PKG dep_CURL) -endif () - -include(JPEG/JPEG.cmake) -include(TIFF/TIFF.cmake) -include(NanoSVG/NanoSVG.cmake) -include(wxWidgets/wxWidgets.cmake) -include(OCCT/OCCT.cmake) - -set(_dep_list - dep_Boost - dep_TBB - ${CURL_PKG} - dep_wxWidgets - dep_Cereal - dep_NLopt - dep_OpenVDB - dep_OpenCSG - dep_CGAL - dep_Qhull - dep_OCCT - ${PNG_PKG} - ${ZLIB_PKG} - ${EXPAT_PKG} + # This ensures dependencies don't use SDK features which are not available in the version specified by Deployment target + # That can happen when one uses a recent SDK but specifies an older Deployment target + set(DEP_WERRORS_SDK "-Werror=partial-availability -Werror=unguarded-availability -Werror=unguarded-availability-new") + + set(DEP_CMAKE_OPTS + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" + "-DCMAKE_OSX_SYSROOT=${CMAKE_OSX_SYSROOT}" + "-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}" + "-DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" + "-DCMAKE_CXX_FLAGS=${DEP_WERRORS_SDK}" + "-DCMAKE_C_FLAGS=${DEP_WERRORS_SDK}" + "-DCMAKE_FIND_FRAMEWORK=LAST" + "-DCMAKE_FIND_APPBUNDLE=LAST" ) +endif () -# if (NOT MSVC) - # Not working, static build has different Eigen - #list(APPEND _dep_list "dep_libigl") -# endif() +list_projects(FOUND_PACKAGES ${CMAKE_CURRENT_LIST_DIR}) -add_custom_target(deps ALL DEPENDS ${_dep_list}) +dep_message(STATUS "Found external package definitions: ${FOUND_PACKAGES}") -# Note: I'm not using any of the LOG_xxx options in ExternalProject_Add() commands -# because they seem to generate bogus build files (possibly a bug in ExternalProject). +# Current list of all required dependencies for PS (top level) +set(REQUIRED_PACKAGES + Boost + Catch2 + Cereal + CURL + EXPAT + NLopt + GLEW + TBB + Qhull + wxWidgets + OpenVDB + CGAL + OCCT + ZLIB + LibBGCode +) + +set(${PROJECT_NAME}_PLATFORM_PACKAGES "" CACHE STRING "Select packages which are provided by the platform" ) +set(SYSTEM_PROVIDED_PACKAGES OpenGL) + +if (UNIX) + # On UNIX systems (including Apple) ZLIB should be available + list(APPEND SYSTEM_PROVIDED_PACKAGES ZLIB) + if (APPLE) + # Deal with CURL on Apple (See issue #5984 on GH): + # Mac SDK should include CURL from at least version 10.12 + list(APPEND SYSTEM_PROVIDED_PACKAGES CURL) + endif () +endif () + + +list(APPEND SYSTEM_PROVIDED_PACKAGES ${${PROJECT_NAME}_PLATFORM_PACKAGES}) +list(REMOVE_DUPLICATES SYSTEM_PROVIDED_PACKAGES) + +include(CMakeDependentOption) +option(${PROJECT_NAME}_SELECT_ALL "Choose all external projects to be built." ON) + +find_package(Git REQUIRED) + +# The default command line for patching. Only works for newer +set(PATCH_CMD ${GIT_EXECUTABLE} apply --verbose --ignore-space-change --whitespace=fix) + +# all required package targets that have existing definitions will be gathered here +set(DEPS_TO_BUILD "") +set(_build_list "") +set(_build_list_toplevel "") +set(_checked_list "") + +# function to check if a package ought to be provided by the platform can really be found +function (check_system_package pkg checked_list) + if (NOT ${pkg} IN_LIST ${checked_list}) + find_package(${pkg}) + if (NOT ${pkg}_FOUND) + dep_message(WARNING "No ${pkg} found in system altough marked as system provided. This might cause trouble building the dependencies on this platform") + endif () + list(APPEND ${checked_list} ${pkg}) + set (${checked_list} ${${checked_list}} PARENT_SCOPE) + endif () +endfunction() + +# Go through all the found package definition folders and filter them according to the provided cache options +set(SUPPORTED_PACKAGES "") +foreach (pkg ${FOUND_PACKAGES}) + cmake_dependent_option(${PROJECT_NAME}_SELECT_${pkg} "Select package ${pkg} to be built." OFF "NOT ${PROJECT_NAME}_SELECT_ALL" OFF) + if (NOT ${PROJECT_NAME}_PACKAGE_EXCLUDES MATCHES ${pkg} AND (${PROJECT_NAME}_SELECT_ALL OR ${PROJECT_NAME}_SELECT_${pkg})) + include(+${pkg}/${pkg}.cmake) + + list(APPEND SUPPORTED_PACKAGES ${pkg}) + + if (${pkg} IN_LIST SYSTEM_PROVIDED_PACKAGES) + check_system_package(${pkg} _checked_list) + elseif (${pkg} IN_LIST REQUIRED_PACKAGES) + list(APPEND DEPS_TO_BUILD ${pkg}) + endif () + endif () +endforeach() + +# Establish dependency graph +foreach (pkg ${SUPPORTED_PACKAGES}) + if (${pkg} IN_LIST DEPS_TO_BUILD) + list(APPEND _build_list dep_${pkg}) + list(APPEND _build_list_toplevel dep_${pkg}) + endif () + foreach(deppkg ${DEP_${pkg}_DEPENDS}) + if (${deppkg} IN_LIST SYSTEM_PROVIDED_PACKAGES) + check_system_package(${deppkg} _checked_list) + elseif(TARGET dep_${deppkg}) + dep_message(STATUS "Mapping dep_${deppkg} => dep_${pkg}") + add_dependencies(dep_${pkg} dep_${deppkg}) + if (${pkg} IN_LIST REQUIRED_PACKAGES) + list(APPEND _build_list dep_${deppkg}) + endif () + endif () + endforeach() +endforeach() + +list(REMOVE_DUPLICATES _build_list) +dep_message(STATUS "Building dep targets (${CMAKE_BUILD_TYPE}): ${_build_list}") +add_custom_target(deps ALL DEPENDS ${_build_list_toplevel}) + +# Support legacy option DEP_DEBUG on MSVC to build debug libraries in the same cmake run as for CMAKE_BUILD_TYPE: +if (DEP_DEBUG AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + # MSVC has this nice feature to not be able to link release mode libs to Debug mode + # projects + + # Exclude the libraries which have no problem to link to Debug builds in + # Release mode (mostly C libraries) + set(DEP_DEBUG_EXCLUDES GMP MPFR OpenSSL NanoSVG TIFF JPEG ZLIB heatshrink) + if (UNIX) + # Making a separate debug build on Unix of wx is a nightmare + list(APPEND DEP_DEBUG_EXCLUDES wxWidgets) + endif () + + # Create the list of targets needed in debug mode + set(_build_list_dbg "") + set(_build_list_filt ${_build_list}) + list(JOIN DEP_DEBUG_EXCLUDES "|" _excl_regexp) + list(FILTER _build_list_filt EXCLUDE REGEX "${_excl_regexp}") + + foreach (t ${_build_list_filt}) + list(APPEND _build_list_dbg ${t}_debug) + endforeach() + + # Create a subdirectory for the Debug build within the current binary dir: + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_d) + execute_process( + COMMAND ${CMAKE_COMMAND} ${CMAKE_CURRENT_SOURCE_DIR} -G${CMAKE_GENERATOR} + -DCMAKE_BUILD_TYPE=Debug + -DDEP_WX_GTK3=${DEP_WX_GTK3} + -D${PROJECT_NAME}_DEP_DOWNLOAD_DIR=${${PROJECT_NAME}_DEP_DOWNLOAD_DIR} + -D${PROJECT_NAME}_DEP_INSTALL_PREFIX=${${PROJECT_NAME}_DEP_INSTALL_PREFIX} + -D${PROJECT_NAME}_PACKAGE_EXCLUDES="${_excl_regexp}" + -D${PROJECT_NAME}_SELECT_ALL=${${PROJECT_NAME}_SELECT_ALL} + -D${PROJECT_NAME}_DEP_BUILD_VERBOSE=${${PROJECT_NAME}_DEP_BUILD_VERBOSE} + -DCMAKE_DEBUG_POSTFIX=d + #TODO: forward per-package selector variables + -DDEP_MESSAGES_WRITTEN=ON + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_d + OUTPUT_QUIET + ) + + dep_message(STATUS "Building dep targets (Debug): ${_build_list_dbg}") + + # Each lib will have a dep__debug target to build only the debug counterpart + # Not part of ALL (problem with parallelization) + foreach(pkgtgt ${_build_list_filt}) + add_custom_target(${pkgtgt}_debug + COMMAND ${CMAKE_COMMAND} --build . --target ${pkgtgt} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_d + USES_TERMINAL + ) + endforeach() + + # Can be used to build all the debug libs + string(JOIN " " _build_list_filt_targets "${_build_list_filt}") + add_custom_target(deps_debug ALL + COMMAND ${CMAKE_COMMAND} --build . --target ${_build_list_filt_targets} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/_d + USES_TERMINAL + ) + + # The Release must be built before, as there are libs in this debug session which need + # the release versions of the excluded libs + add_dependencies(deps_debug deps) + +endif () + +set(DEP_MESSAGES_WRITTEN ON CACHE BOOL "") + +install(CODE "message(STATUS \"Built packages succesfully.\")") diff --git a/deps/CMakePresets.json b/deps/CMakePresets.json new file mode 100644 index 0000000..b6dc536 --- /dev/null +++ b/deps/CMakePresets.json @@ -0,0 +1,57 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "default", + "displayName": "Default Config", + "description": "Default build for any desktop OS", + "binaryDir": "${sourceDir}/build-default", + "condition": { + "type": "const", + "value": true + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "DEP_WX_GTK3": true, + "DEP_DOWNLOAD_DIR": { + "type": "PATH", + "value": "${sourceDir}/.pkg_cache" + } + } + }, + { + "name": "no-occt", + "inherits": "default", + "binaryDir": "${sourceDir}/build-no-occt", + "cacheVariables": { + "PrusaSlicer_deps_PACKAGE_EXCLUDES": "OCCT" + } + }, + { + "name": "mac_universal_x86", + "inherits": "default", + "binaryDir": "${sourceDir}/build-x86_64", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "x86_64" + } + }, + { + "name": "mac_universal_arm", + "inherits": "default", + "binaryDir": "${sourceDir}/build-arm64", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "arm64" + } + } + ] +} diff --git a/deps/GLEW/GLEW.cmake b/deps/GLEW/GLEW.cmake deleted file mode 100644 index bc36940..0000000 --- a/deps/GLEW/GLEW.cmake +++ /dev/null @@ -1,16 +0,0 @@ -# We have to check for OpenGL to compile GLEW -set(OpenGL_GL_PREFERENCE "LEGACY") # to prevent a nasty warning by cmake -find_package(OpenGL QUIET REQUIRED) - -qidislicer_add_cmake_project( - GLEW - URL https://sourceforge.net/projects/glew/files/glew/2.2.0/glew-2.2.0.zip - URL_HASH SHA256=a9046a913774395a095edcc0b0ac2d81c3aacca61787b39839b941e9be14e0d4 - SOURCE_SUBDIR build/cmake - CMAKE_ARGS - -DBUILD_UTILS=OFF -) - -if (MSVC) - add_debug_dep(dep_GLEW) -endif () diff --git a/deps/JPEG/JPEG.cmake b/deps/JPEG/JPEG.cmake deleted file mode 100644 index fb3920f..0000000 --- a/deps/JPEG/JPEG.cmake +++ /dev/null @@ -1,8 +0,0 @@ -qidislicer_add_cmake_project(JPEG - URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/2.0.6.zip - URL_HASH SHA256=017bdc33ff3a72e11301c0feb4657cb27719d7f97fa67a78ed506c594218bbf1 - DEPENDS ${ZLIB_PKG} - CMAKE_ARGS - -DENABLE_SHARED=OFF - -DENABLE_STATIC=ON -) diff --git a/deps/MPFR/MPFR.cmake b/deps/MPFR/MPFR.cmake deleted file mode 100644 index c29bb39..0000000 --- a/deps/MPFR/MPFR.cmake +++ /dev/null @@ -1,38 +0,0 @@ -set(_srcdir ${CMAKE_CURRENT_LIST_DIR}/mpfr) -set(_dstdir ${DESTDIR}/usr/local) - -if (MSVC) - set(_output ${_dstdir}/include/mpfr.h - ${_dstdir}/include/mpf2mpfr.h - ${_dstdir}/lib/libmpfr-4.lib - ${_dstdir}/bin/libmpfr-4.dll) - - add_custom_command( - OUTPUT ${_output} - COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/include/mpfr.h ${_dstdir}/include/ - COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/include/mpf2mpfr.h ${_dstdir}/include/ - COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/lib/win${DEPS_BITS}/libmpfr-4.lib ${_dstdir}/lib/ - COMMAND ${CMAKE_COMMAND} -E copy ${_srcdir}/lib/win${DEPS_BITS}/libmpfr-4.dll ${_dstdir}/bin/ - ) - - add_custom_target(dep_MPFR SOURCES ${_output}) - -else () - - set(_cross_compile_arg "") - if (CMAKE_CROSSCOMPILING) - # TOOLCHAIN_PREFIX should be defined in the toolchain file - set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX}) - endif () - - ExternalProject_Add(dep_MPFR - URL http://ftp.vim.org/ftp/gnu/mpfr/mpfr-3.1.6.tar.bz2 https://www.mpfr.org/mpfr-3.1.6/mpfr-3.1.6.tar.bz2 # mirrors are allowed - URL_HASH SHA256=cf4f4b2d80abb79e820e78c8077b6725bbbb4e8f41896783c899087be0e94068 - DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/MPFR - BUILD_IN_SOURCE ON - CONFIGURE_COMMAND env "CFLAGS=${_gmp_ccflags}" "CXXFLAGS=${_gmp_ccflags}" ./configure ${_cross_compile_arg} --prefix=${DESTDIR}/usr/local --enable-shared=no --enable-static=yes --with-gmp=${DESTDIR}/usr/local ${_gmp_build_tgt} - BUILD_COMMAND make -j - INSTALL_COMMAND make install - DEPENDS dep_GMP - ) -endif () diff --git a/deps/OpenCSG/OpenCSG.cmake b/deps/OpenCSG/OpenCSG.cmake deleted file mode 100644 index 18d6696..0000000 --- a/deps/OpenCSG/OpenCSG.cmake +++ /dev/null @@ -1,17 +0,0 @@ - -qidislicer_add_cmake_project(OpenCSG - # GIT_REPOSITORY https://github.com/floriankirsch/OpenCSG.git - # GIT_TAG 83e274457b46c9ad11a4ee599203250b1618f3b9 #v1.4.2 - URL https://github.com/floriankirsch/OpenCSG/archive/refs/tags/opencsg-1-4-2-release.zip - URL_HASH SHA256=51afe0db79af8386e2027d56d685177135581e0ee82ade9d7f2caff8deab5ec5 - PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt - DEPENDS dep_GLEW -) - -if (TARGET ${ZLIB_PKG}) - add_dependencies(dep_OpenCSG ${ZLIB_PKG}) -endif() - -if (MSVC) - add_debug_dep(dep_OpenCSG) -endif () \ No newline at end of file diff --git a/deps/Qhull/Qhull.cmake b/deps/Qhull/Qhull.cmake deleted file mode 100644 index 5d38d3a..0000000 --- a/deps/Qhull/Qhull.cmake +++ /dev/null @@ -1,11 +0,0 @@ -include(GNUInstallDirs) -qidislicer_add_cmake_project(Qhull - URL "https://github.com/qhull/qhull/archive/v8.0.1.zip" - URL_HASH SHA256=5287f5edd6a0372588f5d6640799086a4033d89d19711023ef8229dd9301d69b - CMAKE_ARGS - -DINCLUDE_INSTALL_DIR=${CMAKE_INSTALL_INCLUDEDIR} -) - -if (MSVC) - add_debug_dep(dep_Qhull) -endif () \ No newline at end of file diff --git a/deps/TIFF/TIFF.cmake b/deps/TIFF/TIFF.cmake deleted file mode 100644 index 05bbecb..0000000 --- a/deps/TIFF/TIFF.cmake +++ /dev/null @@ -1,13 +0,0 @@ -find_package(OpenGL QUIET REQUIRED) - -qidislicer_add_cmake_project(TIFF - URL https://gitlab.com/libtiff/libtiff/-/archive/v4.1.0/libtiff-v4.1.0.zip - URL_HASH SHA256=c56edfacef0a60c0de3e6489194fcb2f24c03dbb550a8a7de5938642d045bd32 - DEPENDS ${ZLIB_PKG} ${PNG_PKG} dep_JPEG - CMAKE_ARGS - -Dlzma:BOOL=OFF - -Dwebp:BOOL=OFF - -Djbig:BOOL=OFF - -Dzstd:BOOL=OFF - -Dpixarlog:BOOL=OFF -) diff --git a/deps/autobuild.cmake b/deps/autobuild.cmake new file mode 100644 index 0000000..316a5b8 --- /dev/null +++ b/deps/autobuild.cmake @@ -0,0 +1,63 @@ +if (NOT ${PROJECT_NAME}_DEPS_PRESET) + set (${PROJECT_NAME}_DEPS_PRESET "default") +endif () + +set (_output_quiet "") +if (${PROJECT_NAME}_DEPS_OUTPUT_QUIET) + set (_output_quiet OUTPUT_QUIET) +endif () + +message(STATUS "Building the dependencies with preset ${${PROJECT_NAME}_DEPS_PRESET}") + +set(_gen_arg "") +if (CMAKE_GENERATOR) + set (_gen_arg "-G${CMAKE_GENERATOR}") +endif () + +set(_build_args "") + +if (CMAKE_C_COMPILER) + list(APPEND _build_args "-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}") +endif () + +if (CMAKE_CXX_COMPILER) + list(APPEND _build_args "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}") +endif () + +if (CMAKE_TOOLCHAIN_FILE) + list(APPEND _build_args "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") +endif () + +set(_build_dir "${CMAKE_CURRENT_LIST_DIR}/build-${${PROJECT_NAME}_DEPS_PRESET}") +if (${PROJECT_NAME}_DEPS_BUILD_DIR) + set(_build_dir "${${PROJECT_NAME}_DEPS_BUILD_DIR}") +endif () + +message(STATUS "build dir = ${_build_dir}") + +execute_process( + COMMAND ${CMAKE_COMMAND} --preset ${${PROJECT_NAME}_DEPS_PRESET} "${_gen_arg}" -B ${_build_dir} ${_build_args} + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + ${_output_quiet} + ERROR_VARIABLE _deps_configure_output + RESULT_VARIABLE _deps_configure_result +) + +if (NOT _deps_configure_result EQUAL 0) + message(FATAL_ERROR "Dependency configure failed with output:\n${_deps_configure_output}") +else () + execute_process( + COMMAND ${CMAKE_COMMAND} --build . + WORKING_DIRECTORY ${_build_dir} + ${_output_quiet} + ERROR_VARIABLE _deps_build_output + RESULT_VARIABLE _deps_build_result + ) + if (NOT _deps_build_result EQUAL 0) + message(FATAL_ERROR "Dependency build failed with output:\n${_deps_build_output}") + endif () +endif () + +list(APPEND CMAKE_PREFIX_PATH ${_build_dir}/destdir/usr/local) +set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" CACHE STRING "") + diff --git a/deps/deps-linux.cmake b/deps/deps-linux.cmake deleted file mode 100644 index fbe7b71..0000000 --- a/deps/deps-linux.cmake +++ /dev/null @@ -1,10 +0,0 @@ - -set(DEP_CMAKE_OPTS "-DCMAKE_POSITION_INDEPENDENT_CODE=ON") - -include("deps-unix-common.cmake") - -# Some Linuxes may have very old libpng, so it's best to bundle it instead of relying on the system version. -# find_package(PNG QUIET) -# if (NOT PNG_FOUND) -# message(WARNING "No PNG dev package found in system, building static library. You should install the system package.") -# endif () \ No newline at end of file diff --git a/deps/deps-macos.cmake b/deps/deps-macos.cmake deleted file mode 100644 index d9e0ce3..0000000 --- a/deps/deps-macos.cmake +++ /dev/null @@ -1,96 +0,0 @@ - -# This ensures dependencies don't use SDK features which are not available in the version specified by Deployment target -# That can happen when one uses a recent SDK but specifies an older Deployment target -set(DEP_WERRORS_SDK "-Werror=partial-availability -Werror=unguarded-availability -Werror=unguarded-availability-new") - -set(DEP_CMAKE_OPTS - "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" - "-DCMAKE_OSX_SYSROOT=${CMAKE_OSX_SYSROOT}" - "-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEP_OSX_TARGET}" - "-DCMAKE_CXX_FLAGS=${DEP_WERRORS_SDK}" - "-DCMAKE_C_FLAGS=${DEP_WERRORS_SDK}" - "-DCMAKE_FIND_FRAMEWORK=LAST" - "-DCMAKE_FIND_APPBUNDLE=LAST" -) - -include("deps-unix-common.cmake") - -find_package(CURL QUIET) -if (NOT CURL_FOUND) - message(WARNING "No CURL dev package found in system, building static library. Mac SDK should include CURL from at least version 10.12. Check your SDK installation.") -endif () - - -# ExternalProject_Add(dep_boost -# EXCLUDE_FROM_ALL 1 -# URL "https://dl.bintray.com/boostorg/release/1.75.0/source/boost_1_75_0.tar.gz" -# URL_HASH SHA256=aeb26f80e80945e82ee93e5939baebdca47b9dee80a07d3144be1e1a6a66dd6a -# BUILD_IN_SOURCE 1 -# CONFIGURE_COMMAND ./bootstrap.sh -# --with-toolset=clang -# --with-libraries=system,iostreams,filesystem,thread,log,locale,regex,date_time -# "--prefix=${DESTDIR}/usr/local" -# BUILD_COMMAND ./b2 -# -j ${NPROC} -# --reconfigure -# toolset=clang -# link=static -# variant=release -# threading=multi -# boost.locale.icu=off -# --disable-icu -# "cflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET}" -# "cxxflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET}" -# "mflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET}" -# "mmflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET}" -# install -# INSTALL_COMMAND "" # b2 does that already -# ) - -# ExternalProject_Add(dep_libcurl -# EXCLUDE_FROM_ALL 1 -# URL "https://curl.haxx.se/download/curl-7.58.0.tar.gz" -# URL_HASH SHA256=cc245bf9a1a42a45df491501d97d5593392a03f7b4f07b952793518d97666115 -# BUILD_IN_SOURCE 1 -# CONFIGURE_COMMAND ./configure -# --enable-static -# --disable-shared -# "--with-ssl=${DESTDIR}/usr/local" -# --with-pic -# --enable-ipv6 -# --enable-versioned-symbols -# --enable-threaded-resolver -# --with-darwinssl -# --without-ssl # disables OpenSSL -# --disable-ldap -# --disable-ldaps -# --disable-manual -# --disable-rtsp -# --disable-dict -# --disable-telnet -# --disable-pop3 -# --disable-imap -# --disable-smb -# --disable-smtp -# --disable-gopher -# --without-gssapi -# --without-libpsl -# --without-libidn2 -# --without-gnutls -# --without-polarssl -# --without-mbedtls -# --without-cyassl -# --without-nss -# --without-axtls -# --without-brotli -# --without-libmetalink -# --without-libssh -# --without-libssh2 -# --without-librtmp -# --without-nghttp2 -# --without-zsh-functions-dir -# BUILD_COMMAND make "-j${NPROC}" -# INSTALL_COMMAND make install "DESTDIR=${DESTDIR}" -# ) - -# add_dependencies(dep_openvdb dep_boost) \ No newline at end of file diff --git a/deps/deps-mingw.cmake b/deps/deps-mingw.cmake deleted file mode 100644 index b86554f..0000000 --- a/deps/deps-mingw.cmake +++ /dev/null @@ -1,8 +0,0 @@ -set(DEP_CMAKE_OPTS "-DCMAKE_POSITION_INDEPENDENT_CODE=ON") -set(DEP_BOOST_TOOLSET "gcc") -set(DEP_BITS 64) - -find_package(Git REQUIRED) - -# TODO make sure to build tbb with -flifetime-dse=1 -include("deps-unix-common.cmake") diff --git a/deps/deps-unix-common.cmake b/deps/deps-unix-common.cmake deleted file mode 100644 index d715e81..0000000 --- a/deps/deps-unix-common.cmake +++ /dev/null @@ -1,19 +0,0 @@ - -# The unix common part expects DEP_CMAKE_OPTS to be set - -if (MINGW) - set(TBB_MINGW_WORKAROUND "-flifetime-dse=1") -else () - set(TBB_MINGW_WORKAROUND "") -endif () - -find_package(ZLIB QUIET) -if (NOT ZLIB_FOUND) - message(WARNING "No ZLIB dev package found in system, building static library. You should install the system package.") -endif () - -# TODO Evaluate expat modifications in the bundled version and test with system versions in various distros and OSX SDKs -# find_package(EXPAT QUIET) -# if (NOT EXPAT_FOUND) -# message(WARNING "No EXPAT dev package found in system, building static library. Consider installing the system package.") -# endif () diff --git a/deps/deps-windows.cmake b/deps/deps-windows.cmake deleted file mode 100644 index aba6fca..0000000 --- a/deps/deps-windows.cmake +++ /dev/null @@ -1,68 +0,0 @@ -# https://cmake.org/cmake/help/latest/variable/MSVC_VERSION.html -if (MSVC_VERSION EQUAL 1800) -# 1800 = VS 12.0 (v120 toolset) - set(DEP_VS_VER "12") - set(DEP_BOOST_TOOLSET "msvc-12.0") -elseif (MSVC_VERSION EQUAL 1900) -# 1900 = VS 14.0 (v140 toolset) - set(DEP_VS_VER "14") - set(DEP_BOOST_TOOLSET "msvc-14.0") -elseif (MSVC_VERSION LESS 1920) -# 1910-1919 = VS 15.0 (v141 toolset) - set(DEP_VS_VER "15") - set(DEP_BOOST_TOOLSET "msvc-14.1") -elseif (MSVC_VERSION LESS 1930) -# 1920-1929 = VS 16.0 (v142 toolset) - set(DEP_VS_VER "16") - set(DEP_BOOST_TOOLSET "msvc-14.2") -elseif (MSVC_VERSION LESS 1940) -# 1930-1939 = VS 17.0 (v143 toolset) - set(DEP_VS_VER "17") - set(DEP_BOOST_TOOLSET "msvc-14.3") -else () - message(FATAL_ERROR "Unsupported MSVC version") -endif () - -if (CMAKE_CXX_COMPILER_ID STREQUAL Clang) - set(DEP_BOOST_TOOLSET "clang-win") -endif () - -if (${DEPS_BITS} EQUAL 32) - set(DEP_MSVC_GEN "Visual Studio ${DEP_VS_VER}") - set(DEP_PLATFORM "Win32") -else () - if (DEP_VS_VER LESS 16) - set(DEP_MSVC_GEN "Visual Studio ${DEP_VS_VER} Win64") - else () - set(DEP_MSVC_GEN "Visual Studio ${DEP_VS_VER}") - endif () - set(DEP_PLATFORM "x64") -endif () - -if (${DEP_DEBUG}) - set(DEP_BOOST_DEBUG "debug") -else () - set(DEP_BOOST_DEBUG "") -endif () - -macro(add_debug_dep _dep) -if (${DEP_DEBUG}) - ExternalProject_Get_Property(${_dep} BINARY_DIR) - ExternalProject_Add_Step(${_dep} build_debug - DEPENDEES build - DEPENDERS install - COMMAND msbuild /m /P:Configuration=Debug INSTALL.vcxproj - WORKING_DIRECTORY "${BINARY_DIR}" - ) -endif () -endmacro() - -if (${DEPS_BITS} EQUAL 32) - set(DEP_WXWIDGETS_TARGET "") - set(DEP_WXWIDGETS_LIBDIR "vc_lib") -else () - set(DEP_WXWIDGETS_TARGET "TARGET_CPU=X64") - set(DEP_WXWIDGETS_LIBDIR "vc_x64_lib") -endif () - -find_package(Git REQUIRED) diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index 6a2161d..ae0dc85 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -15,7 +15,6 @@ our @EXPORT_OK = qw( X Y Z convex_hull - chained_path_from deg2rad rad2deg ); diff --git a/resources/data/embossed_text.obj b/resources/data/embossed_text.obj new file mode 100644 index 0000000..8f6f09f --- /dev/null +++ b/resources/data/embossed_text.obj @@ -0,0 +1,2555 @@ +v -18.02 2 -1 +v -17.5 -0.13 -1 +v -18.02 -1.94 -1 +v 5.62 0.93 -1 +v 5.93 0.58 -1 +v 5.65 0.49 -1 +v -17.5 0.33 -1 +v -15.07 -1.94 -1 +v -17.5 -1.47 -1 +v -15.31 -0.13 -1 +v -15.31 0.33 -1 +v -17.5 1.54 -1 +v -11.91 0.85 -1 +v -11.73 0.54 -1 +v -12.12 0.7 -1 +v -15.16 1.54 -1 +v -15.16 2 -1 +v -15.07 -1.47 -1 +v 6.78 -1.58 -1 +v 6.62 -1.76 -1 +v 6.37 -1.52 -1 +v 6.69 -1.14 -1 +v 6.56 -1.35 -1 +v -9.33 0.59 -1 +v -9.81 2 -1 +v -9.33 2 -1 +v 6.78 -1.94 -1 +v 7.23 -1.94 -1 +v 6.74 2 -1 +v 7.23 2 -1 +v 6.74 0.59 -1 +v 5.95 0.98 -1 +v 2.05 -1.62 -1 +v 2.5 -1.41 -1 +v 2.25 -1.79 -1 +v 5.33 0.1 -1 +v 5.24 -0.34 -1 +v 4.78 -0.1 -1 +v -6.35 -1.79 -1 +v -6.25 -1 -1 +v -6 -1.5 -1 +v 4.74 -0.51 -1 +v 5.27 -0.88 -1 +v -4.84 -1.11 -1 +v -4.74 -0.66 -1 +v -4.26 -0.84 -1 +v 6.2 0.95 -1 +v 6.5 0.83 -1 +v 6.16 0.56 -1 +v 5.09 -1.59 -1 +v 4.9 -1.29 -1 +v 5.41 -1 -1 +v 5.34 -1.82 -1 +v 5.65 -1.5 -1 +v 5.87 -1.59 -1 +v 5.65 -1.96 -1 +v 3.96 -1.69 -1 +v 3.68 -1.28 -1 +v 4.2 -1.37 -1 +v 5.98 -2 -1 +v 3.81 -1.02 -1 +v 6.44 -1.9 -1 +v 4.78 -0.92 -1 +v 4.89 0.26 -1 +v 5 0.49 -1 +v 5.46 0.31 -1 +v 5.25 0.75 -1 +v 6.38 0.47 -1 +v 6.62 0.22 -1 +v 6.76 -0.15 -1 +v 10.39 -1.52 -1 +v 10.65 -1.93 -1 +v 10.36 -1.98 -1 +v 6.78 -0.72 -1 +v 6.09 -1.61 -1 +v 6.22 -1.98 -1 +v -8.06 -1.33 -1 +v -7.92 -1.11 -1 +v -7.69 -1.6 -1 +v 3.54 0.88 -1 +v 3.45 0.47 -1 +v 3.15 0.98 -1 +v 2.49 -1.91 -1 +v 2.78 -1.57 -1 +v -9.02 -1.9 -1 +v -9.2 -1.77 -1 +v -8.9 -1.55 -1 +v -7.85 -0.13 -1 +v -7.33 -0.27 -1 +v -7.83 -0.68 -1 +v 2.35 -1.21 -1 +v -7.83 0.74 -1 +v -8.22 0.47 -1 +v -8.04 0.87 -1 +v -8.94 0.49 -1 +v -9.19 0.24 -1 +v -8.29 0.95 -1 +v -8.49 0.58 -1 +v -8.69 0.98 -1 +v 2.23 -0.95 -1 +v 1.69 -0.75 -1 +v 2.19 -0.63 -1 +v 2.21 -0.23 -1 +v -4.33 -1.16 -1 +v -4.43 -1.4 -1 +v -7.98 0.23 -1 +v 4.12 0.37 -1 +v 3.77 0 -1 +v 3.67 0.25 -1 +v -12.82 0.95 -1 +v -13.02 0.53 -1 +v -13.11 0.98 -1 +v -9.33 -0.09 -1 +v -8.54 -1.61 -1 +v -8.81 -1.98 -1 +v -13.91 -0.15 -1 +v -13.98 0.51 -1 +v -13.85 0.14 -1 +v -9.32 -1.02 -1 +v -9.18 -1.31 -1 +v -9.36 -1.58 -1 +v -9.81 -1.94 -1 +v -9.36 -1.94 -1 +v -8.42 -2 -1 +v -8.26 -1.51 -1 +v -7.42 -1.12 -1 +v -12.3 0.48 -1 +v -11.97 0.41 -1 +v -9.09 0.83 -1 +v -8.74 0.56 -1 +v -7.66 0.57 -1 +v -7.47 0.27 -1 +v -7.35 -0.81 -1 +v -7.53 -1.38 -1 +v -7.97 -1.84 -1 +v 10.58 -1.51 -1 +v 17.42 -1.96 -1 +v 17.58 -1.5 -1 +v 17.73 -1.98 -1 +v 17.47 -1.37 -1 +v 17.02 -1.64 -1 +v 16.98 -1.27 -1 +v 10.22 -1.5 -1 +v 9.62 -1.27 -1 +v 10.11 -1.37 -1 +v 9.65 -1.64 -1 +v 9.26 0.91 -1 +v 9.61 0.54 -1 +v 9.26 0.54 -1 +v -9.37 -0.58 -1 +v 9.61 1.62 -1 +v 10.1 0.91 -1 +v 9.61 0.91 -1 +v 10.58 0.91 -1 +v 10.58 0.54 -1 +v 10.1 0.54 -1 +v 10.1 1.92 -1 +v 9.82 -1.86 -1 +v 10.05 -1.96 -1 +v 12.64 -1.97 -1 +v 12.11 -2 -1 +v 12.26 -1.61 -1 +v 17.76 -1.52 -1 +v 17.95 -1.51 -1 +v 18.02 -1.93 -1 +v 16.98 0.54 -1 +v 16.63 0.91 -1 +v 16.98 0.91 -1 +v 17.46 0.54 -1 +v 17.46 0.91 -1 +v 12.51 -1.57 -1 +v 16.98 1.62 -1 +v 17.95 0.91 -1 +v 17.95 0.54 -1 +v 17.46 1.92 -1 +v 16.63 0.54 -1 +v 12.89 -1.28 -1 +v 13.41 -1.37 -1 +v 13.17 -1.69 -1 +v 13.02 -1.02 -1 +v 17.19 -1.86 -1 +v 15.13 -0.06 -1 +v 15.83 0.91 -1 +v 16.41 0.91 -1 +v 15.42 -0.42 -1 +v 12.72 -1.46 -1 +v 15.15 -0.81 -1 +v 14.39 -1.94 -1 +v 14.85 -0.45 -1 +v 13.52 -1.08 -1 +v 12.91 -1.87 -1 +v 11.46 -1.79 -1 +v 11.71 -1.41 -1 +v 11.7 -1.91 -1 +v 11.43 -0.23 -1 +v 11.4 -0.63 -1 +v 10.9 -0.75 -1 +v 11.99 -1.57 -1 +v 11.26 -1.62 -1 +v 11.56 -1.21 -1 +v 11.1 -1.4 -1 +v 11.44 -0.95 -1 +v 10.99 -1.15 -1 +v 14.49 0.91 -1 +v 13.89 0.91 -1 +v -14.41 0.91 -1 +v 11.48 0.04 -1 +v 10.92 -0.19 -1 +v 10.99 0.1 -1 +v 13.02 -0.23 -1 +v 11.11 0.36 -1 +v 11.58 0.24 -1 +v 11.27 0.58 -1 +v 11.74 0.42 -1 +v 11.47 0.75 -1 +v 12.01 0.55 -1 +v 11.7 0.88 -1 +v 11.95 0.95 -1 +v 12.36 0.58 -1 +v 12.36 0.98 -1 +v 12.66 0.47 -1 +v 12.76 0.88 -1 +v 12.98 0.76 -1 +v 12.88 0.25 -1 +v 13.17 0.59 -1 +v 13.33 0.37 -1 +v 12.99 0 -1 +v 13.48 0.02 -1 +v 13.54 -0.63 -1 +v -13.75 0.32 -1 +v -13.6 0.45 -1 +v -13.32 0.56 -1 +v -13.62 0.85 -1 +v -13.38 0.95 -1 +v -12.85 0.43 -1 +v -12.75 0.25 -1 +v -12.41 0.69 -1 +v -12.14 0.2 -1 +v -12.22 -0.1 -1 +v -12.71 0.02 -1 +v -10.83 0.81 -1 +v -11.23 0.48 -1 +v -11.13 0.95 -1 +v -11.67 0.95 -1 +v -12.71 -1.94 -1 +v -12.22 -1.94 -1 +v -11.02 0.06 -1 +v -11.08 0.33 -1 +v -10.54 0.21 -1 +v -11.41 0.98 -1 +v -11.41 0.55 -1 +v -10.53 -1.94 -1 +v -11.01 -1.94 -1 +v -10.63 0.56 -1 +v -14.41 -1.94 -1 +v -12.59 0.85 -1 +v -13.82 0.7 -1 +v -13.98 0.91 -1 +v 3.81 -0.23 -1 +v 4.33 -0.63 -1 +v -13.93 -1.94 -1 +v 3.51 -1.46 -1 +v 4.31 -1.08 -1 +v 3.43 -1.97 -1 +v 3.7 -1.87 -1 +v 3.3 -1.57 -1 +v 3.05 -1.61 -1 +v 1.89 -1.4 -1 +v 1.71 -0.19 -1 +v 2.26 0.04 -1 +v 3.96 0.59 -1 +v 3.77 0.76 -1 +v 2.74 0.95 -1 +v 2.8 0.55 -1 +v -4.97 -1.33 -1 +v 2.48 0.88 -1 +v 2.53 0.42 -1 +v 2.06 0.58 -1 +v 2.36 0.24 -1 +v 1.89 0.36 -1 +v 1.78 0.1 -1 +v -5.73 0.98 -1 +v -5.47 0.57 -1 +v -5.75 0.56 -1 +v -2.89 -1.58 -1 +v -2.57 -1.61 -1 +v -2.41 -2 -1 +v 0.62 0.31 -1 +v 0.84 0.77 -1 +v 0.99 0.62 -1 +v 1.78 -1.15 -1 +v 1.1 0.43 -1 +v -5.86 -1.98 -1 +v -6.12 -1.91 -1 +v 0.62 -0.88 -1 +v 0.4 -0.8 -1 +v 0.73 -0.38 -1 +v 2.9 -2 -1 +v 16.49 -1.94 -1 +v 15.89 -1.94 -1 +v 2.26 0.75 -1 +v 3.14 0.58 -1 +v 4.27 0.02 -1 +v -0.56 -1.87 -1 +v -0.37 -1.47 -1 +v -0.25 -1.98 -1 +v -4.24 -0.23 -1 +v -0.47 0.01 -1 +v -0.78 -0.38 -1 +v -0.91 -0.22 -1 +v -5.04 0.88 -1 +v -5.16 0.47 -1 +v -5.29 0.96 -1 +v 0.95 -1.75 -1 +v 0.72 -1.89 -1 +v 0.61 -1.47 -1 +v -0.5 -0.54 -1 +v -0.28 -0.09 -1 +v -5.97 0.48 -1 +v 1.13 -1.56 -1 +v 0.73 -1.32 -1 +v -6.67 0.44 -1 +v -6.18 0.31 -1 +v -6.32 0.09 -1 +v 0.77 -1.15 -1 +v 1.23 -1.33 -1 +v 0.5 0.46 -1 +v 0.26 0.57 -1 +v 0.62 0.88 -1 +v -0.55 0.18 -1 +v -1.01 0.03 -1 +v -4.45 0.38 -1 +v -4.33 0.13 -1 +v -4.9 0.21 -1 +v -4.76 -0.13 -1 +v -6.71 -1.4 -1 +v -6.55 -1.62 -1 +v -5.69 -1.6 -1 +v -5.58 -2 -1 +v -4.65 -1.67 -1 +v -5.19 -1.51 -1 +v -5.22 -1.96 -1 +v -5.4 -1.59 -1 +v -6.39 -0.88 -1 +v -6.83 -1.15 -1 +v -6.92 -0.72 -1 +v -6.42 -0.34 -1 +v -6.89 -0.14 -1 +v -6.81 0.18 -1 +v -6.48 0.66 -1 +v -6.2 0.85 -1 +v -4.81 0.76 -1 +v -4.61 0.59 -1 +v -4.89 -1.83 -1 +v 1.16 0.18 -1 +v 0.69 0.11 -1 +v -0.98 0.41 -1 +v -1.52 -1.33 -1 +v -1.63 -1.56 -1 +v -2.02 -1.32 -1 +v -1.51 -0.87 -1 +v -1.98 -1.15 -1 +v -2.02 -1 -1 +v -0.5 0.88 -1 +v -0.37 0.5 -1 +v -0.78 0.71 -1 +v -0.15 0.57 -1 +v -0.09 0.98 -1 +v 0.34 0.96 -1 +v -2.92 0.57 -1 +v -3.27 0.88 -1 +v -2.86 0.98 -1 +v -2.49 0.57 -1 +v -3.54 0.71 -1 +v -3.13 0.5 -1 +v -3.28 0.35 -1 +v -0.51 0.35 -1 +v -3.26 -0.54 -1 +v -3.05 -0.09 -1 +v -2.35 -0.8 -1 +v -1.8 -1.75 -1 +v -2.03 -1.89 -1 +v -2.14 -1.47 -1 +v -0.78 -1.73 -1 +v -0.54 -1.26 -1 +v -1 -1.42 -1 +v 0.18 -1.61 -1 +v 0.46 -1.56 -1 +v 0.34 -2 -1 +v 1.24 -0.87 -1 +v 1.27 -1.1 -1 +v 1.02 -0.54 -1 +v 0.73 -1 -1 +v 1.15 -0.68 -1 +v -3.13 -1.47 -1 +v -3.01 -1.98 -1 +v -3.33 -1.87 -1 +v -0.62 -1.01 -1 +v -2.29 -1.56 -1 +v -1.1 -1.08 -1 +v -2.02 -0.38 -1 +v -2.13 -0.88 -1 +v -0.13 -1.58 -1 +v -1.74 -0.54 -1 +v -2.13 0.31 -1 +v -1.65 0.43 -1 +v -2.06 0.11 -1 +v -3.23 0.01 -1 +v -3.54 -0.38 -1 +v -3.38 -1.01 -1 +v -3.3 -1.26 -1 +v -3.76 -1.42 -1 +v -3.67 -0.22 -1 +v -3.78 0.03 -1 +v -3.74 0.41 -1 +v -3.31 0.18 -1 +v -2.41 0.96 -1 +v -2.13 0.88 -1 +v -2.25 0.46 -1 +v -1.91 0.77 -1 +v -1.76 0.62 -1 +v -1.59 0.18 -1 +v -1.48 -1.1 -1 +v -1.6 -0.68 -1 +v -3.55 -1.73 -1 +v -3.86 -1.08 -1 +v 13.81 -1.94 -1 +v -18.02 2 1 +v -18.02 -1.94 1 +v -17.5 -0.13 1 +v 5.62 0.93 1 +v 5.63 0.47 1 +v 5.9 0.58 1 +v -17.5 0.33 1 +v -15.07 -1.94 1 +v -17.5 -1.47 1 +v -15.31 -0.13 1 +v -15.31 0.33 1 +v -17.5 1.54 1 +v -11.91 0.85 1 +v -12.12 0.7 1 +v -11.75 0.53 1 +v -15.16 1.54 1 +v -15.16 2 1 +v -15.07 -1.47 1 +v 6.78 -1.58 1 +v 6.39 -1.5 1 +v 6.62 -1.76 1 +v -9.33 0.59 1 +v -9.33 2 1 +v -9.81 2 1 +v 6.78 -1.94 1 +v 7.23 -1.94 1 +v 6.74 2 1 +v 6.74 0.59 1 +v 7.23 2 1 +v 5.95 0.98 1 +v 2.05 -1.62 1 +v 2.25 -1.79 1 +v 2.52 -1.43 1 +v -7.33 -0.33 1 +v -7.92 0.1 1 +v -7.83 -0.33 1 +v 5.4 0.23 1 +v 4.89 0.26 1 +v 5.27 -0.13 1 +v 4.74 -0.51 1 +v 5.24 -0.68 1 +v -6.35 -1.79 1 +v -5.97 -1.51 1 +v -6.18 -1.33 1 +v 4.78 -0.92 1 +v 5.34 -1.12 1 +v 6.2 0.95 1 +v 6.16 0.56 1 +v 6.53 0.81 1 +v 5.09 -1.59 1 +v 4.9 -1.29 1 +v 5.34 -1.82 1 +v 5.67 -1.51 1 +v 5.47 -1.33 1 +v 5.87 -1.59 1 +v 5.65 -1.96 1 +v 3.93 -1.71 1 +v 4.18 -1.4 1 +v 3.68 -1.28 1 +v 5.98 -2 1 +v 3.81 -1.02 1 +v 6.44 -1.9 1 +v 4.78 -0.1 1 +v 5.02 0.51 1 +v 5.27 0.77 1 +v 6.36 0.48 1 +v 6.61 0.24 1 +v 6.75 -0.11 1 +v 10.4 -1.52 1 +v 10.34 -1.98 1 +v 10.65 -1.93 1 +v 6.73 -1.02 1 +v 6.78 -0.68 1 +v 6.63 -1.26 1 +v 6.11 -1.6 1 +v 6.22 -1.98 1 +v 3.54 0.88 1 +v 3.2 0.98 1 +v 3.42 0.48 1 +v -7.99 -1.24 1 +v -7.53 -1.38 1 +v -7.85 -0.88 1 +v 2.49 -1.91 1 +v 2.81 -1.58 1 +v -9.02 -1.9 1 +v -9.02 -1.48 1 +v -9.2 -1.77 1 +v 2.35 -1.21 1 +v -7.83 0.74 1 +v -8.04 0.87 1 +v -8.24 0.49 1 +v -9.21 0.21 1 +v -8.96 0.47 1 +v -8.29 0.95 1 +v -8.65 0.98 1 +v -8.52 0.58 1 +v 2.24 -0.98 1 +v 2.19 -0.63 1 +v 1.71 -0.86 1 +v 2.21 -0.23 1 +v 1.69 -0.31 1 +v -4.33 -1.16 1 +v -4.76 -0.87 1 +v -4.45 -1.43 1 +v -8.05 0.31 1 +v 4.12 0.37 1 +v 3.66 0.26 1 +v 3.77 0.03 1 +v -12.82 0.95 1 +v -13.11 0.98 1 +v -13.04 0.54 1 +v -9.37 -0.35 1 +v -8.52 -1.6 1 +v -8.79 -1.58 1 +v -8.81 -1.98 1 +v -9.31 -0.02 1 +v -13.92 -0.18 1 +v -13.85 0.14 1 +v -13.98 0.51 1 +v -9.81 -1.94 1 +v -9.36 -1.94 1 +v -9.36 -1.58 1 +v -8.57 -2 1 +v -8.23 -1.5 1 +v -8.32 -1.98 1 +v -7.39 -1.02 1 +v -9.25 -1.19 1 +v -9.35 -0.87 1 +v -12.3 0.48 1 +v -11.99 0.39 1 +v -9.06 0.85 1 +v -8.74 0.56 1 +v -7.66 0.57 1 +v -7.45 0.23 1 +v -7.69 -1.6 1 +v -8.01 -1.87 1 +v 10.58 -1.51 1 +v 17.42 -1.96 1 +v 17.7 -1.98 1 +v 17.59 -1.5 1 +v 17.48 -1.38 1 +v 16.98 -1 1 +v 17.02 -1.62 1 +v 10.23 -1.5 1 +v 9.61 -1 1 +v 9.65 -1.62 1 +v 10.11 -1.38 1 +v 9.26 0.91 1 +v 9.26 0.54 1 +v 9.61 0.54 1 +v 9.61 1.62 1 +v 9.61 0.91 1 +v 10.1 0.91 1 +v 10.58 0.54 1 +v 10.58 0.91 1 +v 10.1 0.54 1 +v 10.1 1.92 1 +v 9.8 -1.84 1 +v 10.05 -1.96 1 +v 12.6 -1.98 1 +v 12.26 -1.61 1 +v 12.06 -2 1 +v 17.77 -1.52 1 +v 18.02 -1.93 1 +v 17.95 -1.51 1 +v 16.98 0.54 1 +v 16.98 0.91 1 +v 16.63 0.91 1 +v 17.46 0.54 1 +v 17.46 0.91 1 +v 12.51 -1.57 1 +v 16.98 1.62 1 +v 17.95 0.54 1 +v 17.95 0.91 1 +v 17.46 1.92 1 +v 16.63 0.54 1 +v 12.89 -1.28 1 +v 13.15 -1.71 1 +v 13.4 -1.4 1 +v 13.02 -1.02 1 +v 17.16 -1.84 1 +v 15.13 -0.06 1 +v 16.41 0.91 1 +v 15.83 0.91 1 +v 15.42 -0.42 1 +v 12.72 -1.46 1 +v 15.15 -0.81 1 +v 14.85 -0.45 1 +v 14.39 -1.94 1 +v 13.52 -1.08 1 +v 12.91 -1.87 1 +v 11.46 -1.79 1 +v 11.7 -1.91 1 +v 11.74 -1.43 1 +v 11.43 -0.23 1 +v 10.9 -0.31 1 +v 11.4 -0.63 1 +v 12.02 -1.58 1 +v 11.26 -1.62 1 +v 11.56 -1.21 1 +v 11.1 -1.4 1 +v 11.45 -0.98 1 +v 10.99 -1.15 1 +v 10.92 -0.86 1 +v 14.49 0.91 1 +v 13.89 0.91 1 +v -14.41 0.91 1 +v 11.47 0.01 1 +v 10.99 0.1 1 +v 13.02 -0.23 1 +v 11.11 0.36 1 +v 11.58 0.24 1 +v 11.27 0.58 1 +v 11.72 0.4 1 +v 11.47 0.75 1 +v 11.99 0.55 1 +v 11.7 0.88 1 +v 11.95 0.95 1 +v 12.32 0.58 1 +v 12.41 0.98 1 +v 12.64 0.48 1 +v 12.76 0.88 1 +v 12.98 0.76 1 +v 12.87 0.26 1 +v 13.17 0.59 1 +v 13.33 0.37 1 +v 12.98 0.03 1 +v 13.45 0.12 1 +v 13.51 -0.17 1 +v 13.54 -0.63 1 +v -13.75 0.32 1 +v -13.6 0.45 1 +v -13.35 0.55 1 +v -13.62 0.85 1 +v -13.38 0.95 1 +v -12.87 0.45 1 +v -12.41 0.69 1 +v -12.76 0.27 1 +v -12.15 0.17 1 +v -12.71 0.04 1 +v -12.22 -0.12 1 +v -10.8 0.79 1 +v -11.1 0.94 1 +v -11.23 0.48 1 +v -11.67 0.95 1 +v -12.71 -1.94 1 +v -12.22 -1.94 1 +v -11.02 0.08 1 +v -10.54 0.18 1 +v -11.1 0.35 1 +v -11.44 0.56 1 +v -11.41 0.98 1 +v -10.53 -1.94 1 +v -11.01 -1.94 1 +v -10.61 0.53 1 +v -14.41 -1.94 1 +v -12.59 0.85 1 +v -13.82 0.7 1 +v -13.98 0.91 1 +v 4.33 -0.63 1 +v 3.81 -0.23 1 +v -13.93 -1.94 1 +v 3.51 -1.46 1 +v 4.31 -1.08 1 +v 3.39 -1.98 1 +v 3.7 -1.87 1 +v 3.3 -1.57 1 +v 3.05 -1.61 1 +v 1.89 -1.4 1 +v 2.25 0.01 1 +v 3.96 0.59 1 +v 3.77 0.76 1 +v 2.74 0.95 1 +v 2.77 0.55 1 +v 2.48 0.88 1 +v 2.51 0.4 1 +v 2.06 0.58 1 +v 2.36 0.24 1 +v -4.61 0.59 1 +v -4.98 0.3 1 +v -4.84 0.09 1 +v 1.89 0.36 1 +v 1.78 0.1 1 +v -6.92 -0.66 1 +v -6.32 -1.12 1 +v -6.42 -0.68 1 +v -2.86 -1.59 1 +v -2.64 -2 1 +v -2.54 -1.6 1 +v 0.62 0.31 1 +v 0.99 0.62 1 +v 0.84 0.77 1 +v 1.78 -1.15 1 +v 1.1 0.43 1 +v -6.03 -1.95 1 +v 0.61 -0.88 1 +v 0.76 -0.39 1 +v 0.38 -0.79 1 +v 2.85 -2 1 +v 16.49 -1.94 1 +v 15.89 -1.94 1 +v 2.26 0.75 1 +v 3.11 0.58 1 +v 4.23 0.12 1 +v 4.3 -0.17 1 +v -4.9 -1.24 1 +v -0.56 -1.87 1 +v -0.29 -1.97 1 +v -0.36 -1.49 1 +v -4.24 -0.28 1 +v -4.74 -0.32 1 +v -4.26 -0.88 1 +v -0.52 0.07 1 +v -0.91 -0.22 1 +v -0.78 -0.38 1 +v -6.39 -0.14 1 +v 0.95 -1.75 1 +v 0.61 -1.47 1 +v 0.72 -1.89 1 +v -5.69 0.98 1 +v -6 0.47 1 +v -5.68 0.58 1 +v 1.13 -1.56 1 +v 0.73 -1.32 1 +v 1.23 -1.33 1 +v 0.77 -1.15 1 +v 0.5 0.46 1 +v 0.62 0.88 1 +v 0.31 0.55 1 +v -0.34 -0.07 1 +v -0.53 -0.53 1 +v -4.45 0.38 1 +v -4.33 0.13 1 +v -6.71 -1.4 1 +v -6.55 -1.62 1 +v -5.58 -2 1 +v -5.75 -1.59 1 +v -4.68 -1.69 1 +v -5.16 -1.49 1 +v -5.22 -1.96 1 +v -5.47 -1.6 1 +v -6.83 -1.15 1 +v -6.89 -0.14 1 +v -6.81 0.18 1 +v -6.25 0.22 1 +v -6.67 0.44 1 +v -6.48 0.66 1 +v -6.16 0.87 1 +v -5.4 0.56 1 +v -5.13 0.92 1 +v -5.19 0.48 1 +v -4.81 0.76 1 +v -4.89 -1.83 1 +v 0.21 -1.6 1 +v 0.12 -2 1 +v 0.43 -1.98 1 +v 1.16 0.18 1 +v 0.69 0.11 1 +v -0.99 -0.04 1 +v -1.02 0.15 1 +v -0.55 0.21 1 +v -0.97 0.44 1 +v -1.52 -1.33 1 +v -2.02 -1.32 1 +v -1.63 -1.56 1 +v -1.51 -0.87 1 +v -2.02 -1 1 +v -1.98 -1.15 1 +v -0.48 0.89 1 +v -0.76 0.73 1 +v -0.39 0.49 1 +v -0.18 0.56 1 +v -0.05 0.98 1 +v 0.06 0.58 1 +v 0.34 0.96 1 +v -2.94 0.56 1 +v -2.82 0.98 1 +v -3.24 0.89 1 +v -2.69 0.58 1 +v -3.52 0.73 1 +v -3.28 0.35 1 +v -3.15 0.49 1 +v -3.31 0.21 1 +v -3.78 0.15 1 +v -3.28 0.07 1 +v -0.51 0.35 1 +v -3.29 -0.53 1 +v -2.37 -0.79 1 +v -3.1 -0.07 1 +v -1.8 -1.75 1 +v -2.14 -1.47 1 +v -2.03 -1.89 1 +v -0.54 -1.26 1 +v -0.81 -1.71 1 +v -1.02 -1.39 1 +v 0.48 -1.55 1 +v 1.24 -0.87 1 +v 1.27 -1.1 1 +v 0.73 -1 1 +v 1.02 -0.54 1 +v 1.15 -0.68 1 +v -3.12 -1.49 1 +v -3.33 -1.87 1 +v -3.05 -1.97 1 +v -0.62 -1.01 1 +v -2.32 -1.98 1 +v -2.27 -1.55 1 +v -1.1 -1.08 1 +v -1.99 -0.39 1 +v -2.14 -0.88 1 +v -0.1 -1.59 1 +v -1.74 -0.54 1 +v -2.13 0.31 1 +v -2.06 0.11 1 +v -1.65 0.43 1 +v -3.54 -0.38 1 +v -3.38 -1.01 1 +v -3.78 -1.39 1 +v -3.3 -1.26 1 +v -3.67 -0.22 1 +v -3.75 -0.04 1 +v -3.73 0.44 1 +v -2.44 0.55 1 +v -2.41 0.96 1 +v -2.13 0.88 1 +v -2.25 0.46 1 +v -1.91 0.77 1 +v -1.76 0.62 1 +v -1.59 0.18 1 +v -1.48 -1.1 1 +v -1.6 -0.68 1 +v -3.57 -1.71 1 +v -3.86 -1.08 1 +v 13.81 -1.94 1 +f 1 2 3 +f 4 5 6 +f 7 2 1 +f 8 3 9 +f 10 7 11 +f 10 2 7 +f 12 7 1 +f 13 14 15 +f 16 12 17 +f 17 12 1 +f 2 9 3 +f 9 18 8 +f 19 20 21 +f 22 19 23 +f 24 25 26 +f 27 19 28 +f 29 30 31 +f 4 32 5 +f 33 34 35 +f 36 37 38 +f 39 40 41 +f 42 37 43 +f 44 45 46 +f 47 48 49 +f 50 51 52 +f 53 52 54 +f 55 56 54 +f 57 58 59 +f 55 60 56 +f 58 61 59 +f 62 21 20 +f 54 56 53 +f 53 50 52 +f 52 51 43 +f 63 43 51 +f 42 43 63 +f 37 42 38 +f 36 38 64 +f 36 64 65 +f 66 65 67 +f 36 65 66 +f 66 67 6 +f 4 6 67 +f 49 5 47 +f 5 32 47 +f 49 48 68 +f 31 68 48 +f 31 69 68 +f 31 70 69 +f 71 72 73 +f 22 74 28 +f 19 22 28 +f 21 23 19 +f 75 21 62 +f 76 75 62 +f 60 55 75 +f 60 75 76 +f 77 78 79 +f 80 81 82 +f 83 34 84 +f 85 86 87 +f 88 89 90 +f 33 91 34 +f 92 93 94 +f 24 95 96 +f 97 98 99 +f 100 101 102 +f 102 101 103 +f 94 93 97 +f 104 105 44 +f 92 106 93 +f 107 108 109 +f 110 111 112 +f 25 24 113 +f 114 115 87 +f 116 117 118 +f 119 120 121 +f 122 121 123 +f 124 115 114 +f 87 115 85 +f 125 124 114 +f 78 90 126 +f 120 87 86 +f 121 120 86 +f 122 119 121 +f 127 15 128 +f 24 96 113 +f 24 129 95 +f 130 129 99 +f 95 129 130 +f 98 130 99 +f 93 98 97 +f 92 131 106 +f 106 131 132 +f 88 106 132 +f 89 88 132 +f 133 90 89 +f 126 90 133 +f 134 78 126 +f 79 78 134 +f 135 77 79 +f 135 125 77 +f 135 124 125 +f 71 136 72 +f 137 138 139 +f 140 141 142 +f 71 73 143 +f 144 145 146 +f 147 148 149 +f 113 150 122 +f 151 152 153 +f 152 154 155 +f 153 152 156 +f 156 152 155 +f 152 151 157 +f 150 119 122 +f 156 144 148 +f 153 156 148 +f 153 148 147 +f 156 145 144 +f 25 113 122 +f 158 146 145 +f 143 158 145 +f 159 158 143 +f 143 73 159 +f 160 161 162 +f 163 164 165 +f 165 139 163 +f 166 167 168 +f 139 138 163 +f 166 169 142 +f 169 140 142 +f 170 169 168 +f 162 171 160 +f 172 170 168 +f 170 173 174 +f 172 175 170 +f 169 170 174 +f 176 167 166 +f 177 178 179 +f 178 177 180 +f 169 166 168 +f 140 181 141 +f 182 183 184 +f 140 138 181 +f 185 182 184 +f 138 137 181 +f 186 177 179 +f 187 188 189 +f 190 178 180 +f 191 160 186 +f 192 193 194 +f 191 186 179 +f 195 196 197 +f 171 186 160 +f 198 194 193 +f 199 200 193 +f 201 202 200 +f 203 197 202 +f 204 182 205 +f 206 117 116 +f 207 208 209 +f 207 195 208 +f 197 208 195 +f 197 196 202 +f 203 202 201 +f 199 201 200 +f 192 199 193 +f 194 198 161 +f 162 161 198 +f 210 196 195 +f 209 211 207 +f 212 211 213 +f 207 211 212 +f 214 213 215 +f 212 213 214 +f 216 214 217 +f 214 215 217 +f 216 217 218 +f 219 216 220 +f 216 218 220 +f 221 220 222 +f 219 220 221 +f 222 223 221 +f 224 223 225 +f 221 223 224 +f 225 226 224 +f 224 226 227 +f 227 226 228 +f 210 227 228 +f 229 210 228 +f 230 117 231 +f 232 231 233 +f 234 232 233 +f 111 110 235 +f 127 236 237 +f 238 239 240 +f 241 242 243 +f 244 14 13 +f 239 245 240 +f 196 210 229 +f 246 245 239 +f 247 248 249 +f 236 238 240 +f 127 238 236 +f 14 128 15 +f 244 250 251 +f 14 244 251 +f 242 251 243 +f 252 247 249 +f 247 252 253 +f 248 254 249 +f 206 116 255 +f 248 241 254 +f 242 241 248 +f 250 243 251 +f 256 237 235 +f 237 236 235 +f 235 110 256 +f 128 238 127 +f 232 112 111 +f 232 234 112 +f 233 231 257 +f 258 117 206 +f 231 117 257 +f 230 118 117 +f 102 259 260 +f 261 255 116 +f 57 262 58 +f 61 263 59 +f 264 262 265 +f 262 57 265 +f 264 266 262 +f 266 264 267 +f 100 91 268 +f 103 269 270 +f 103 101 269 +f 107 109 271 +f 271 109 272 +f 272 81 80 +f 273 82 274 +f 275 44 105 +f 276 274 277 +f 278 277 279 +f 280 270 281 +f 282 283 284 +f 281 270 269 +f 285 286 287 +f 288 289 290 +f 291 101 100 +f 292 288 290 +f 293 294 41 +f 291 100 268 +f 268 91 33 +f 294 39 41 +f 83 35 34 +f 295 296 297 +f 298 83 84 +f 267 298 84 +f 264 298 267 +f 299 300 187 +f 280 279 270 +f 102 103 259 +f 280 278 279 +f 278 301 277 +f 276 277 301 +f 274 276 273 +f 274 82 302 +f 81 302 82 +f 109 81 272 +f 107 303 108 +f 108 303 259 +f 259 303 260 +f 299 187 185 +f 304 305 306 +f 307 46 45 +f 308 309 310 +f 311 312 313 +f 314 315 316 +f 317 308 318 +f 282 284 319 +f 320 314 321 +f 322 323 324 +f 321 325 326 +f 327 328 329 +f 318 296 317 +f 308 317 309 +f 330 308 331 +f 332 333 334 +f 333 307 335 +f 336 40 337 +f 293 338 339 +f 293 41 338 +f 340 341 275 +f 342 343 341 +f 39 337 40 +f 344 40 345 +f 40 336 345 +f 346 344 345 +f 347 344 346 +f 346 348 347 +f 324 348 349 +f 347 348 324 +f 324 349 322 +f 322 350 323 +f 319 350 351 +f 323 350 319 +f 282 319 351 +f 283 282 313 +f 312 283 313 +f 312 311 352 +f 334 352 353 +f 312 352 334 +f 353 332 334 +f 334 333 335 +f 335 307 45 +f 46 104 44 +f 105 340 275 +f 341 340 354 +f 342 341 354 +f 339 343 342 +f 339 338 343 +f 355 356 292 +f 331 308 310 +f 289 327 329 +f 330 331 357 +f 358 359 360 +f 361 362 363 +f 364 365 366 +f 365 364 367 +f 367 364 368 +f 328 368 369 +f 288 292 356 +f 327 289 288 +f 328 367 368 +f 329 328 369 +f 370 371 372 +f 370 372 373 +f 374 375 376 +f 365 377 366 +f 378 379 380 +f 377 357 366 +f 357 377 330 +f 381 382 383 +f 305 384 385 +f 384 386 385 +f 387 388 389 +f 297 296 318 +f 390 391 325 +f 295 392 393 +f 297 392 295 +f 392 394 393 +f 325 393 390 +f 393 394 390 +f 325 391 326 +f 320 321 326 +f 314 316 321 +f 315 388 316 +f 389 388 315 +f 395 396 397 +f 398 385 386 +f 286 399 287 +f 386 400 398 +f 401 402 380 +f 384 305 304 +f 306 305 403 +f 389 306 403 +f 387 389 403 +f 363 402 404 +f 360 362 358 +f 405 406 407 +f 379 378 408 +f 408 378 409 +f 410 411 412 +f 413 408 409 +f 408 413 414 +f 415 376 416 +f 371 375 374 +f 371 370 375 +f 373 417 418 +f 373 372 417 +f 419 418 420 +f 420 421 405 +f 407 406 422 +f 421 406 405 +f 420 405 419 +f 373 418 419 +f 376 415 374 +f 414 415 416 +f 416 408 414 +f 401 380 379 +f 361 423 362 +f 402 401 404 +f 383 382 399 +f 363 404 424 +f 361 363 424 +f 362 423 358 +f 360 359 381 +f 381 383 360 +f 287 399 382 +f 395 397 425 +f 412 411 425 +f 412 426 410 +f 425 411 395 +f 395 285 396 +f 396 285 287 +f 182 185 189 +f 205 182 189 +f 189 188 427 +f 187 189 185 +f 28 74 70 +f 28 70 30 +f 70 31 30 +f 428 429 430 +f 431 432 433 +f 434 428 430 +f 435 436 429 +f 437 438 434 +f 437 434 430 +f 439 428 434 +f 440 441 442 +f 443 444 439 +f 444 428 439 +f 430 429 436 +f 436 435 445 +f 446 447 448 +f 449 450 451 +f 452 453 446 +f 454 455 456 +f 431 433 457 +f 458 459 460 +f 461 462 463 +f 464 465 466 +f 466 467 468 +f 469 470 471 +f 472 473 468 +f 474 475 476 +f 477 473 478 +f 479 480 481 +f 482 480 483 +f 484 485 486 +f 482 483 487 +f 486 485 488 +f 489 448 447 +f 480 479 483 +f 479 481 477 +f 481 473 477 +f 472 478 473 +f 467 472 468 +f 466 490 467 +f 466 465 490 +f 464 491 465 +f 464 492 491 +f 464 432 492 +f 431 492 432 +f 475 474 433 +f 433 474 457 +f 475 493 476 +f 455 476 493 +f 455 493 494 +f 455 494 495 +f 496 497 498 +f 499 453 500 +f 446 453 499 +f 446 499 501 +f 447 446 501 +f 502 489 447 +f 503 489 502 +f 487 502 482 +f 487 503 502 +f 504 505 506 +f 507 508 509 +f 510 511 460 +f 512 513 514 +f 458 460 515 +f 516 517 518 +f 449 519 520 +f 521 522 523 +f 524 525 526 +f 525 527 528 +f 517 521 518 +f 529 530 531 +f 516 518 532 +f 533 534 535 +f 536 537 538 +f 451 539 449 +f 540 541 542 +f 449 539 543 +f 544 545 546 +f 547 548 549 +f 550 540 542 +f 541 512 542 +f 541 513 512 +f 551 540 552 +f 463 509 553 +f 549 514 513 +f 549 513 554 +f 547 549 554 +f 547 554 555 +f 556 557 441 +f 449 543 519 +f 449 520 558 +f 559 522 558 +f 520 559 558 +f 523 522 559 +f 518 521 523 +f 516 532 560 +f 462 561 560 +f 532 462 560 +f 461 561 462 +f 553 461 463 +f 508 553 509 +f 562 508 507 +f 563 562 507 +f 563 507 551 +f 563 551 552 +f 552 540 550 +f 496 498 564 +f 565 566 567 +f 568 569 570 +f 496 571 497 +f 572 573 574 +f 575 576 577 +f 578 579 580 +f 580 581 582 +f 579 583 580 +f 583 581 580 +f 580 584 578 +f 539 547 555 +f 583 577 572 +f 579 577 583 +f 579 575 577 +f 583 572 574 +f 451 547 539 +f 585 574 573 +f 571 574 585 +f 586 571 585 +f 571 586 497 +f 587 588 589 +f 590 591 592 +f 591 590 566 +f 593 594 595 +f 566 590 567 +f 593 569 596 +f 596 569 568 +f 597 594 596 +f 588 587 598 +f 599 594 597 +f 597 600 601 +f 599 597 602 +f 596 600 597 +f 603 593 595 +f 604 605 606 +f 606 607 604 +f 596 594 593 +f 568 570 608 +f 609 610 611 +f 568 608 567 +f 612 610 609 +f 567 608 565 +f 613 605 604 +f 614 615 616 +f 617 607 606 +f 618 613 587 +f 619 620 621 +f 618 605 613 +f 622 623 624 +f 598 587 613 +f 625 621 620 +f 626 621 627 +f 628 627 629 +f 630 629 631 +f 623 631 624 +f 632 633 609 +f 634 544 546 +f 635 636 623 +f 635 623 622 +f 631 629 624 +f 630 628 629 +f 626 627 628 +f 619 621 626 +f 620 589 625 +f 588 625 589 +f 637 622 624 +f 636 635 638 +f 639 640 638 +f 635 639 638 +f 641 642 640 +f 639 641 640 +f 643 644 641 +f 641 644 642 +f 643 645 644 +f 646 647 643 +f 643 647 645 +f 648 649 647 +f 646 648 647 +f 649 648 650 +f 651 652 650 +f 648 651 650 +f 652 651 653 +f 651 654 653 +f 654 655 653 +f 654 656 655 +f 637 656 654 +f 657 656 637 +f 658 659 546 +f 660 661 659 +f 662 661 660 +f 538 663 536 +f 556 664 665 +f 666 667 668 +f 669 670 671 +f 672 440 442 +f 668 667 673 +f 624 657 637 +f 674 668 673 +f 675 676 677 +f 665 667 666 +f 556 665 666 +f 442 441 557 +f 672 678 679 +f 442 678 672 +f 671 670 678 +f 680 676 675 +f 675 681 680 +f 677 676 682 +f 634 683 544 +f 677 682 669 +f 671 677 669 +f 679 678 670 +f 684 663 664 +f 664 663 665 +f 663 684 536 +f 557 556 666 +f 660 538 537 +f 660 537 662 +f 661 685 659 +f 686 634 546 +f 659 685 546 +f 658 546 545 +f 525 687 688 +f 689 544 683 +f 484 486 690 +f 488 485 691 +f 692 693 690 +f 690 693 484 +f 692 690 694 +f 694 695 692 +f 524 696 515 +f 527 697 528 +f 533 698 534 +f 698 699 534 +f 699 504 506 +f 700 701 505 +f 702 703 701 +f 704 705 703 +f 706 707 708 +f 709 710 697 +f 711 712 713 +f 710 528 697 +f 714 715 716 +f 526 525 528 +f 717 718 719 +f 720 524 526 +f 721 718 717 +f 720 696 524 +f 696 458 515 +f 722 470 469 +f 510 460 459 +f 723 724 725 +f 726 511 510 +f 695 511 726 +f 692 695 726 +f 727 614 728 +f 709 697 705 +f 525 688 527 +f 709 705 704 +f 704 703 729 +f 702 729 703 +f 701 700 702 +f 701 730 505 +f 506 505 730 +f 534 699 506 +f 533 535 731 +f 731 535 732 +f 535 688 732 +f 688 687 732 +f 727 612 614 +f 531 530 733 +f 734 735 736 +f 737 738 739 +f 740 741 742 +f 713 743 711 +f 744 745 746 +f 747 748 749 +f 750 751 744 +f 751 752 753 +f 754 755 756 +f 757 758 725 +f 757 742 758 +f 759 708 760 +f 760 708 737 +f 761 762 712 +f 722 763 764 +f 722 764 470 +f 765 733 766 +f 767 766 768 +f 469 471 762 +f 712 762 471 +f 712 769 761 +f 711 769 712 +f 711 743 770 +f 743 771 770 +f 772 773 771 +f 743 772 771 +f 773 772 774 +f 748 775 774 +f 772 748 774 +f 747 775 748 +f 776 777 747 +f 749 776 747 +f 778 777 776 +f 778 779 777 +f 707 706 779 +f 778 707 779 +f 706 708 759 +f 708 738 737 +f 738 530 739 +f 739 530 529 +f 531 733 765 +f 766 780 765 +f 767 780 766 +f 763 767 768 +f 763 768 764 +f 781 782 783 +f 784 721 785 +f 786 741 740 +f 787 786 740 +f 719 755 754 +f 788 789 787 +f 790 791 792 +f 793 794 795 +f 796 797 798 +f 798 799 796 +f 799 800 796 +f 801 802 800 +f 756 802 801 +f 717 785 721 +f 754 717 719 +f 801 800 799 +f 755 802 756 +f 803 804 805 +f 803 806 804 +f 807 808 809 +f 810 811 812 +f 798 797 813 +f 814 815 816 +f 813 797 789 +f 789 788 813 +f 740 788 787 +f 817 818 819 +f 757 740 742 +f 736 820 821 +f 821 820 822 +f 781 783 823 +f 724 757 725 +f 824 753 825 +f 723 826 827 +f 724 723 827 +f 827 826 828 +f 753 824 826 +f 826 824 828 +f 753 752 825 +f 750 752 751 +f 744 751 745 +f 746 745 823 +f 783 746 823 +f 829 830 831 +f 832 822 820 +f 716 833 834 +f 822 832 835 +f 836 815 837 +f 821 734 736 +f 735 838 736 +f 782 838 735 +f 781 838 782 +f 794 839 837 +f 791 790 795 +f 840 841 842 +f 816 843 814 +f 844 845 846 +f 847 843 812 +f 812 848 847 +f 849 810 808 +f 805 807 809 +f 805 809 803 +f 806 850 851 +f 850 852 851 +f 806 851 804 +f 853 854 852 +f 854 840 855 +f 841 856 842 +f 855 840 842 +f 854 853 840 +f 850 853 852 +f 808 807 849 +f 811 810 849 +f 848 812 811 +f 843 816 812 +f 836 816 815 +f 793 795 857 +f 837 839 836 +f 818 834 819 +f 794 858 839 +f 793 858 794 +f 795 790 857 +f 791 817 792 +f 817 791 818 +f 833 819 834 +f 829 859 830 +f 845 859 846 +f 845 844 860 +f 859 829 846 +f 829 831 714 +f 831 715 714 +f 715 833 716 +f 609 615 612 +f 633 615 609 +f 615 861 616 +f 614 612 615 +f 453 495 500 +f 453 456 495 +f 495 456 455 +f 18 445 8 +f 435 8 445 +f 9 436 18 +f 445 18 436 +f 2 430 9 +f 436 9 430 +f 10 437 2 +f 430 2 437 +f 11 438 10 +f 437 10 438 +f 7 434 11 +f 438 11 434 +f 12 439 7 +f 434 7 439 +f 16 443 12 +f 439 12 443 +f 17 444 16 +f 443 16 444 +f 1 428 17 +f 444 17 428 +f 3 429 1 +f 428 1 429 +f 8 435 3 +f 429 3 435 +f 76 503 60 +f 487 60 503 +f 62 489 76 +f 503 76 489 +f 20 448 62 +f 489 62 448 +f 19 446 20 +f 448 20 446 +f 27 452 19 +f 446 19 452 +f 28 453 27 +f 452 27 453 +f 30 456 28 +f 453 28 456 +f 29 454 30 +f 456 30 454 +f 31 455 29 +f 454 29 455 +f 48 476 31 +f 455 31 476 +f 47 474 48 +f 476 48 474 +f 32 457 47 +f 474 47 457 +f 4 431 32 +f 457 32 431 +f 67 492 4 +f 431 4 492 +f 65 491 67 +f 492 67 491 +f 64 465 65 +f 491 65 465 +f 38 490 64 +f 465 64 490 +f 42 467 38 +f 490 38 467 +f 63 472 42 +f 467 42 472 +f 51 478 63 +f 472 63 478 +f 50 477 51 +f 478 51 477 +f 53 479 50 +f 477 50 479 +f 56 483 53 +f 479 53 483 +f 60 487 56 +f 483 56 487 +f 55 482 75 +f 502 75 482 +f 54 480 55 +f 482 55 480 +f 52 481 54 +f 480 54 481 +f 481 52 473 +f 43 473 52 +f 473 43 468 +f 37 468 43 +f 468 37 466 +f 36 466 37 +f 466 36 464 +f 66 464 36 +f 6 432 66 +f 464 66 432 +f 5 433 6 +f 432 6 433 +f 49 475 5 +f 433 5 475 +f 68 493 49 +f 475 49 493 +f 69 494 68 +f 493 68 494 +f 70 495 69 +f 494 69 495 +f 74 500 70 +f 495 70 500 +f 22 499 74 +f 500 74 499 +f 499 22 501 +f 23 501 22 +f 21 447 23 +f 501 23 447 +f 75 502 21 +f 447 21 502 +f 550 124 552 +f 135 563 124 +f 552 124 563 +f 79 562 135 +f 563 135 562 +f 134 508 79 +f 562 79 508 +f 126 553 134 +f 508 134 553 +f 133 553 126 +f 89 461 133 +f 553 133 461 +f 132 561 89 +f 461 89 561 +f 131 560 132 +f 561 132 560 +f 92 516 131 +f 560 131 516 +f 94 517 92 +f 516 92 517 +f 97 521 94 +f 517 94 521 +f 99 522 97 +f 521 97 522 +f 129 558 99 +f 522 99 558 +f 24 449 129 +f 558 129 449 +f 26 450 24 +f 449 24 450 +f 25 451 26 +f 450 26 451 +f 122 547 25 +f 451 25 547 +f 123 548 122 +f 547 122 548 +f 121 549 123 +f 548 123 549 +f 86 514 121 +f 549 121 514 +f 85 512 86 +f 514 86 512 +f 115 542 85 +f 512 85 542 +f 124 550 115 +f 542 115 550 +f 87 541 114 +f 540 114 541 +f 541 87 513 +f 120 513 87 +f 513 120 554 +f 119 554 120 +f 554 119 555 +f 150 555 119 +f 555 150 539 +f 113 539 150 +f 539 113 543 +f 96 519 113 +f 543 113 519 +f 95 520 96 +f 519 96 520 +f 130 559 95 +f 520 95 559 +f 98 523 130 +f 559 130 523 +f 93 518 98 +f 523 98 518 +f 106 532 93 +f 518 93 532 +f 532 106 462 +f 88 462 106 +f 462 88 463 +f 90 463 88 +f 463 90 509 +f 78 509 90 +f 509 78 507 +f 77 507 78 +f 125 551 77 +f 507 77 551 +f 114 540 125 +f 551 125 540 +f 72 498 73 +f 497 73 498 +f 136 564 72 +f 498 72 564 +f 71 496 136 +f 564 136 496 +f 143 571 71 +f 496 71 571 +f 145 574 143 +f 571 143 574 +f 156 583 145 +f 574 145 583 +f 155 581 156 +f 583 156 581 +f 154 582 155 +f 581 155 582 +f 152 580 154 +f 582 154 580 +f 157 584 152 +f 580 152 584 +f 151 578 157 +f 584 157 578 +f 153 579 151 +f 578 151 579 +f 147 575 153 +f 579 153 575 +f 149 576 147 +f 575 147 576 +f 148 577 149 +f 576 149 577 +f 144 572 148 +f 577 148 572 +f 146 573 144 +f 572 144 573 +f 158 585 146 +f 573 146 585 +f 159 586 158 +f 585 158 586 +f 73 497 159 +f 586 159 497 +f 165 591 139 +f 566 139 591 +f 164 592 165 +f 591 165 592 +f 163 590 164 +f 592 164 590 +f 138 567 163 +f 590 163 567 +f 140 568 138 +f 567 138 568 +f 169 596 140 +f 568 140 596 +f 174 600 169 +f 596 169 600 +f 173 601 174 +f 600 174 601 +f 170 597 173 +f 601 173 597 +f 175 602 170 +f 597 170 602 +f 172 599 175 +f 602 175 599 +f 168 594 172 +f 599 172 594 +f 167 595 168 +f 594 168 595 +f 176 603 167 +f 595 167 603 +f 166 593 176 +f 603 176 593 +f 142 569 166 +f 593 166 569 +f 141 570 142 +f 569 142 570 +f 181 608 141 +f 570 141 608 +f 137 565 181 +f 608 181 565 +f 139 566 137 +f 565 137 566 +f 160 587 161 +f 589 161 587 +f 191 618 160 +f 587 160 618 +f 179 605 191 +f 618 191 605 +f 178 606 179 +f 605 179 606 +f 190 617 178 +f 606 178 617 +f 180 607 190 +f 617 190 607 +f 177 604 180 +f 607 180 604 +f 186 613 177 +f 604 177 613 +f 171 598 186 +f 613 186 598 +f 162 588 171 +f 598 171 588 +f 198 625 162 +f 588 162 625 +f 193 621 198 +f 625 198 621 +f 200 627 193 +f 621 193 627 +f 202 629 200 +f 627 200 629 +f 196 624 202 +f 629 202 624 +f 229 657 196 +f 624 196 657 +f 228 656 229 +f 657 229 656 +f 656 228 655 +f 226 653 228 +f 655 228 653 +f 225 652 226 +f 653 226 652 +f 223 650 225 +f 652 225 650 +f 222 649 223 +f 650 223 649 +f 220 647 222 +f 649 222 647 +f 218 645 220 +f 647 220 645 +f 217 644 218 +f 645 218 644 +f 215 642 217 +f 644 217 642 +f 213 640 215 +f 642 215 640 +f 211 638 213 +f 640 213 638 +f 209 636 211 +f 638 211 636 +f 208 623 209 +f 636 209 623 +f 197 623 208 +f 623 197 631 +f 203 630 197 +f 631 197 630 +f 201 628 203 +f 630 203 628 +f 199 626 201 +f 628 201 626 +f 192 619 199 +f 626 199 619 +f 194 620 192 +f 619 192 620 +f 161 589 194 +f 620 194 589 +f 195 622 210 +f 637 210 622 +f 207 635 195 +f 622 195 635 +f 212 639 207 +f 635 207 639 +f 214 641 212 +f 639 212 641 +f 216 643 214 +f 641 214 643 +f 219 646 216 +f 643 216 646 +f 221 648 219 +f 646 219 648 +f 224 651 221 +f 648 221 651 +f 227 654 224 +f 651 224 654 +f 210 637 227 +f 654 227 637 +f 116 544 261 +f 689 261 544 +f 118 545 116 +f 544 116 545 +f 230 658 118 +f 545 118 658 +f 231 659 230 +f 658 230 659 +f 232 660 231 +f 659 231 660 +f 111 538 232 +f 660 232 538 +f 235 663 111 +f 538 111 663 +f 236 665 235 +f 663 235 665 +f 240 667 236 +f 665 236 667 +f 245 673 240 +f 667 240 673 +f 246 674 245 +f 673 245 674 +f 239 668 246 +f 674 246 668 +f 238 666 239 +f 668 239 666 +f 128 557 238 +f 666 238 557 +f 14 442 128 +f 557 128 442 +f 251 678 14 +f 442 14 678 +f 242 671 251 +f 678 251 671 +f 248 677 242 +f 671 242 677 +f 247 675 248 +f 677 248 675 +f 253 681 247 +f 675 247 681 +f 252 680 253 +f 681 253 680 +f 249 676 252 +f 680 252 676 +f 254 682 249 +f 676 249 682 +f 241 669 254 +f 682 254 669 +f 243 670 241 +f 669 241 670 +f 250 679 243 +f 670 243 679 +f 244 672 250 +f 679 250 672 +f 13 440 244 +f 672 244 440 +f 15 441 13 +f 440 13 441 +f 127 556 15 +f 441 15 556 +f 237 664 127 +f 556 127 664 +f 256 684 237 +f 664 237 684 +f 110 536 256 +f 684 256 536 +f 112 537 110 +f 536 110 537 +f 234 662 112 +f 537 112 662 +f 233 661 234 +f 662 234 661 +f 257 685 233 +f 661 233 685 +f 117 546 257 +f 685 257 546 +f 258 686 117 +f 546 117 686 +f 206 634 258 +f 686 258 634 +f 255 683 206 +f 634 206 683 +f 261 689 255 +f 683 255 689 +f 264 692 298 +f 726 298 692 +f 265 693 264 +f 692 264 693 +f 57 484 265 +f 693 265 484 +f 59 485 57 +f 484 57 485 +f 263 691 59 +f 485 59 691 +f 61 488 263 +f 691 263 488 +f 58 486 61 +f 488 61 486 +f 262 690 58 +f 486 58 690 +f 266 694 262 +f 690 262 694 +f 267 695 266 +f 694 266 695 +f 84 511 267 +f 695 267 511 +f 34 460 84 +f 511 84 460 +f 91 515 34 +f 460 34 515 +f 100 524 91 +f 515 91 524 +f 102 525 100 +f 524 100 525 +f 260 687 102 +f 525 102 687 +f 303 732 260 +f 687 260 732 +f 732 303 731 +f 107 533 303 +f 731 303 533 +f 271 698 107 +f 533 107 698 +f 272 699 271 +f 698 271 699 +f 80 504 272 +f 699 272 504 +f 82 505 80 +f 504 80 505 +f 273 700 82 +f 505 82 700 +f 276 702 273 +f 700 273 702 +f 301 729 276 +f 702 276 729 +f 278 704 301 +f 729 301 704 +f 280 709 278 +f 704 278 709 +f 281 710 280 +f 709 280 710 +f 269 528 281 +f 710 281 528 +f 101 528 269 +f 528 101 526 +f 291 720 101 +f 526 101 720 +f 268 696 291 +f 720 291 696 +f 33 458 268 +f 696 268 458 +f 35 459 33 +f 458 33 459 +f 83 510 35 +f 459 35 510 +f 298 726 83 +f 510 83 726 +f 103 527 259 +f 688 259 527 +f 270 697 103 +f 527 103 697 +f 279 705 270 +f 697 270 705 +f 277 703 279 +f 705 279 703 +f 274 701 277 +f 703 277 701 +f 302 730 274 +f 701 274 730 +f 81 506 302 +f 730 302 506 +f 109 534 81 +f 506 81 534 +f 108 535 109 +f 534 109 535 +f 259 688 108 +f 535 108 688 +f 342 767 339 +f 763 339 767 +f 354 780 342 +f 767 342 780 +f 340 765 354 +f 780 354 765 +f 105 531 340 +f 765 340 531 +f 104 529 105 +f 531 105 529 +f 46 739 104 +f 529 104 739 +f 307 737 46 +f 739 46 737 +f 333 760 307 +f 737 307 760 +f 332 759 333 +f 760 333 759 +f 353 706 332 +f 759 332 706 +f 352 779 353 +f 706 353 779 +f 311 777 352 +f 779 352 777 +f 313 777 311 +f 282 747 313 +f 777 313 747 +f 351 775 282 +f 747 282 775 +f 350 774 351 +f 775 351 774 +f 322 773 350 +f 774 350 773 +f 349 771 322 +f 773 322 771 +f 348 770 349 +f 771 349 770 +f 346 711 348 +f 770 348 711 +f 345 769 346 +f 711 346 769 +f 336 761 345 +f 769 345 761 +f 337 762 336 +f 761 336 762 +f 39 469 337 +f 762 337 469 +f 294 722 39 +f 469 39 722 +f 293 722 294 +f 339 763 293 +f 722 293 763 +f 768 338 764 +f 41 470 338 +f 764 338 470 +f 40 471 41 +f 470 41 471 +f 471 40 712 +f 344 712 40 +f 712 344 713 +f 347 713 344 +f 713 347 743 +f 324 743 347 +f 743 324 772 +f 323 772 324 +f 319 748 323 +f 772 323 748 +f 284 749 319 +f 748 319 749 +f 283 749 284 +f 749 283 776 +f 312 778 283 +f 776 283 778 +f 334 707 312 +f 778 312 707 +f 707 334 708 +f 335 708 334 +f 708 335 738 +f 45 738 335 +f 738 45 530 +f 44 530 45 +f 530 44 733 +f 275 733 44 +f 341 766 275 +f 733 275 766 +f 343 768 341 +f 766 341 768 +f 338 768 343 +f 782 389 783 +f 315 746 389 +f 783 389 746 +f 314 744 315 +f 746 315 744 +f 320 750 314 +f 744 314 750 +f 326 752 320 +f 750 320 752 +f 391 825 326 +f 752 326 825 +f 390 824 391 +f 825 391 824 +f 394 828 390 +f 824 390 828 +f 392 827 394 +f 828 394 827 +f 297 724 392 +f 827 392 724 +f 318 757 297 +f 724 297 757 +f 308 757 318 +f 757 308 740 +f 330 740 308 +f 740 330 788 +f 377 813 330 +f 788 330 813 +f 365 798 377 +f 813 377 798 +f 367 799 365 +f 798 365 799 +f 328 801 367 +f 799 367 801 +f 801 328 756 +f 327 754 328 +f 756 328 754 +f 288 717 327 +f 754 327 717 +f 356 785 288 +f 717 288 785 +f 355 784 356 +f 785 356 784 +f 292 721 355 +f 784 355 721 +f 290 718 292 +f 721 292 718 +f 289 719 290 +f 718 290 719 +f 329 755 289 +f 719 289 755 +f 369 802 329 +f 755 329 802 +f 368 800 369 +f 802 369 800 +f 364 796 368 +f 800 368 796 +f 366 797 364 +f 796 364 797 +f 357 789 366 +f 797 366 789 +f 331 787 357 +f 789 357 787 +f 787 331 786 +f 310 741 331 +f 786 331 741 +f 309 742 310 +f 741 310 742 +f 317 758 309 +f 742 309 758 +f 296 725 317 +f 758 317 725 +f 295 723 296 +f 725 296 723 +f 393 826 295 +f 723 295 826 +f 325 753 393 +f 826 393 753 +f 321 751 325 +f 753 325 751 +f 316 745 321 +f 751 321 745 +f 388 823 316 +f 745 316 823 +f 387 781 388 +f 823 388 781 +f 403 838 387 +f 781 387 838 +f 305 736 403 +f 838 403 736 +f 385 820 305 +f 736 305 820 +f 398 832 385 +f 820 385 832 +f 400 835 398 +f 832 398 835 +f 386 822 400 +f 835 400 822 +f 384 821 386 +f 822 386 821 +f 304 734 384 +f 821 384 734 +f 306 735 304 +f 734 304 735 +f 389 782 306 +f 735 306 782 +f 715 287 833 +f 382 819 287 +f 833 287 819 +f 381 817 382 +f 819 382 817 +f 359 792 381 +f 817 381 792 +f 358 790 359 +f 792 359 790 +f 423 857 358 +f 790 358 857 +f 361 793 423 +f 857 423 793 +f 424 858 361 +f 793 361 858 +f 404 839 424 +f 858 424 839 +f 401 836 404 +f 839 404 836 +f 379 816 401 +f 836 401 816 +f 408 816 379 +f 816 408 812 +f 416 812 408 +f 812 416 810 +f 376 808 416 +f 810 416 808 +f 375 809 376 +f 808 376 809 +f 370 803 375 +f 809 375 803 +f 373 806 370 +f 803 370 806 +f 806 373 850 +f 419 853 373 +f 850 373 853 +f 405 840 419 +f 853 419 840 +f 407 841 405 +f 840 405 841 +f 422 856 407 +f 841 407 856 +f 406 842 422 +f 856 422 842 +f 421 855 406 +f 842 406 855 +f 420 854 421 +f 855 421 854 +f 418 852 420 +f 854 420 852 +f 417 851 418 +f 852 418 851 +f 372 804 417 +f 851 417 804 +f 371 805 372 +f 804 372 805 +f 374 807 371 +f 805 371 807 +f 415 849 374 +f 807 374 849 +f 414 811 415 +f 849 415 811 +f 811 414 848 +f 413 847 414 +f 848 414 847 +f 409 843 413 +f 847 413 843 +f 378 814 409 +f 843 409 814 +f 380 815 378 +f 814 378 815 +f 402 837 380 +f 815 380 837 +f 363 794 402 +f 837 402 794 +f 362 795 363 +f 794 363 795 +f 360 791 362 +f 795 362 791 +f 383 818 360 +f 791 360 818 +f 399 834 383 +f 818 383 834 +f 286 716 399 +f 834 399 716 +f 285 714 286 +f 716 286 714 +f 395 829 285 +f 714 285 829 +f 411 846 395 +f 829 395 846 +f 410 844 411 +f 846 411 844 +f 426 860 410 +f 844 410 860 +f 412 845 426 +f 860 426 845 +f 425 859 412 +f 845 412 859 +f 397 830 425 +f 859 425 830 +f 396 831 397 +f 830 397 831 +f 287 715 396 +f 831 396 715 +f 187 614 188 +f 616 188 614 +f 300 728 187 +f 614 187 728 +f 299 727 300 +f 728 300 727 +f 185 612 299 +f 727 299 612 +f 184 610 185 +f 612 185 610 +f 183 611 184 +f 610 184 611 +f 182 609 183 +f 611 183 609 +f 204 632 182 +f 609 182 632 +f 205 633 204 +f 632 204 633 +f 189 615 205 +f 633 205 615 +f 427 861 189 +f 615 189 861 +f 188 616 427 +f 861 427 616 diff --git a/resources/data/hints.ini b/resources/data/hints.ini index 1b07d14..e3ab0bf 100644 --- a/resources/data/hints.ini +++ b/resources/data/hints.ini @@ -231,6 +231,11 @@ disabled_tags = SLA text = Fullscreen mode\nDid you know that you can switch QIDISlicer to fullscreen mode? Use the F11 hotkey. enabled_tags = Windows +[hint:Printables integration] +text = Printables.com integration\nDid you know that when you are browsing Printables.com, you can send 3D model files to PrusaSlicer with a single click? Learn more in the documentation. +documentation_link = https://help.qidi3d.com/article/prusaslicer-printables-com-integration_399198 +weight = 3 + [hint:Cut tool] text = Cut tool\nDid you know that you can cut a model at any angle and even create aligning pins with the updated Cut tool? Learn more in the documentation. documentation_link = https://help.qidi3d.com/article/cut-tool_1779 diff --git a/resources/icons/PrusaSlicer-gcodeviewer-mac_128px.png b/resources/icons/PrusaSlicer-gcodeviewer-mac_128px.png new file mode 100644 index 0000000..b62f3fd Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer-mac_128px.png differ diff --git a/resources/icons/PrusaSlicer-gcodeviewer.ico b/resources/icons/PrusaSlicer-gcodeviewer.ico new file mode 100644 index 0000000..1cd867e Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer.ico differ diff --git a/resources/icons/PrusaSlicer-gcodeviewer.svg b/resources/icons/PrusaSlicer-gcodeviewer.svg new file mode 100644 index 0000000..6312bee --- /dev/null +++ b/resources/icons/PrusaSlicer-gcodeviewer.svg @@ -0,0 +1,73 @@ + + + + + + + diff --git a/resources/icons/PrusaSlicer-gcodeviewer_128px.png b/resources/icons/PrusaSlicer-gcodeviewer_128px.png new file mode 100644 index 0000000..475ddb6 Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer_128px.png differ diff --git a/resources/icons/PrusaSlicer-gcodeviewer_192px.png b/resources/icons/PrusaSlicer-gcodeviewer_192px.png new file mode 100644 index 0000000..2f3b036 Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer_192px.png differ diff --git a/resources/icons/PrusaSlicer-gcodeviewer_32px.png b/resources/icons/PrusaSlicer-gcodeviewer_32px.png new file mode 100644 index 0000000..eaba5e2 Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer_32px.png differ diff --git a/resources/icons/PrusaSlicer-mac_128px.png b/resources/icons/PrusaSlicer-mac_128px.png new file mode 100644 index 0000000..ff0b093 Binary files /dev/null and b/resources/icons/PrusaSlicer-mac_128px.png differ diff --git a/resources/icons/PrusaSlicer.icns b/resources/icons/PrusaSlicer.icns new file mode 100644 index 0000000..5f6a7fe Binary files /dev/null and b/resources/icons/PrusaSlicer.icns differ diff --git a/resources/icons/PrusaSlicer.ico b/resources/icons/PrusaSlicer.ico new file mode 100644 index 0000000..4a335cb Binary files /dev/null and b/resources/icons/PrusaSlicer.ico differ diff --git a/resources/icons/PrusaSlicer.png b/resources/icons/PrusaSlicer.png new file mode 100644 index 0000000..dc51630 Binary files /dev/null and b/resources/icons/PrusaSlicer.png differ diff --git a/resources/icons/PrusaSlicer.svg b/resources/icons/PrusaSlicer.svg new file mode 100644 index 0000000..927c3e7 --- /dev/null +++ b/resources/icons/PrusaSlicer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/icons/PrusaSlicer_128px.png b/resources/icons/PrusaSlicer_128px.png new file mode 100644 index 0000000..ddf2223 Binary files /dev/null and b/resources/icons/PrusaSlicer_128px.png differ diff --git a/resources/icons/PrusaSlicer_192px.png b/resources/icons/PrusaSlicer_192px.png new file mode 100644 index 0000000..a667be3 Binary files /dev/null and b/resources/icons/PrusaSlicer_192px.png differ diff --git a/resources/icons/PrusaSlicer_192px_grayscale.png b/resources/icons/PrusaSlicer_192px_grayscale.png new file mode 100644 index 0000000..6a52fb0 Binary files /dev/null and b/resources/icons/PrusaSlicer_192px_grayscale.png differ diff --git a/resources/icons/PrusaSlicer_192px_transparent.png b/resources/icons/PrusaSlicer_192px_transparent.png new file mode 100644 index 0000000..18dd923 Binary files /dev/null and b/resources/icons/PrusaSlicer_192px_transparent.png differ diff --git a/resources/icons/PrusaSlicer_32px.png b/resources/icons/PrusaSlicer_32px.png new file mode 100644 index 0000000..bbbf840 Binary files /dev/null and b/resources/icons/PrusaSlicer_32px.png differ diff --git a/resources/icons/_thumbnail.png b/resources/icons/_thumbnail.png deleted file mode 100644 index 51fcf92..0000000 Binary files a/resources/icons/_thumbnail.png and /dev/null differ diff --git a/resources/icons/align_horizontal_center.svg b/resources/icons/align_horizontal_center.svg new file mode 100644 index 0000000..7234939 --- /dev/null +++ b/resources/icons/align_horizontal_center.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/align_horizontal_left.svg b/resources/icons/align_horizontal_left.svg new file mode 100644 index 0000000..1b88ee7 --- /dev/null +++ b/resources/icons/align_horizontal_left.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/icons/align_horizontal_right.svg b/resources/icons/align_horizontal_right.svg new file mode 100644 index 0000000..b4dffb0 --- /dev/null +++ b/resources/icons/align_horizontal_right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/align_vertical_bottom.svg b/resources/icons/align_vertical_bottom.svg new file mode 100644 index 0000000..5c0a94b --- /dev/null +++ b/resources/icons/align_vertical_bottom.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/icons/align_vertical_center.svg b/resources/icons/align_vertical_center.svg new file mode 100644 index 0000000..e3655be --- /dev/null +++ b/resources/icons/align_vertical_center.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/icons/align_vertical_top.svg b/resources/icons/align_vertical_top.svg new file mode 100644 index 0000000..a882176 --- /dev/null +++ b/resources/icons/align_vertical_top.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/icons/burn.svg b/resources/icons/burn.svg new file mode 100644 index 0000000..e7b58cd --- /dev/null +++ b/resources/icons/burn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/convert_file.svg b/resources/icons/convert_file.svg new file mode 100644 index 0000000..2de2b70 --- /dev/null +++ b/resources/icons/convert_file.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/custom-gcode_gcode.svg b/resources/icons/custom-gcode_gcode.svg new file mode 100644 index 0000000..38bcd21 --- /dev/null +++ b/resources/icons/custom-gcode_gcode.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/custom-gcode_measure.svg b/resources/icons/custom-gcode_measure.svg new file mode 100644 index 0000000..3c13dd7 --- /dev/null +++ b/resources/icons/custom-gcode_measure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/custom-gcode_object-info.svg b/resources/icons/custom-gcode_object-info.svg new file mode 100644 index 0000000..aa51310 --- /dev/null +++ b/resources/icons/custom-gcode_object-info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/custom-gcode_single.svg b/resources/icons/custom-gcode_single.svg new file mode 100644 index 0000000..d177860 --- /dev/null +++ b/resources/icons/custom-gcode_single.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/custom-gcode_slicing-state.svg b/resources/icons/custom-gcode_slicing-state.svg new file mode 100644 index 0000000..4b4bef6 --- /dev/null +++ b/resources/icons/custom-gcode_slicing-state.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/custom-gcode_slicing-state_global.svg b/resources/icons/custom-gcode_slicing-state_global.svg new file mode 100644 index 0000000..4f5131c --- /dev/null +++ b/resources/icons/custom-gcode_slicing-state_global.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/icons/custom-gcode_stats.svg b/resources/icons/custom-gcode_stats.svg new file mode 100644 index 0000000..fde8fec --- /dev/null +++ b/resources/icons/custom-gcode_stats.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/custom-gcode_vector-index.svg b/resources/icons/custom-gcode_vector-index.svg new file mode 100644 index 0000000..4307dd7 --- /dev/null +++ b/resources/icons/custom-gcode_vector-index.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/custom-gcode_vector.svg b/resources/icons/custom-gcode_vector.svg new file mode 100644 index 0000000..4ac1ab9 --- /dev/null +++ b/resources/icons/custom-gcode_vector.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/drop_down.svg b/resources/icons/drop_down.svg new file mode 100644 index 0000000..1276e34 --- /dev/null +++ b/resources/icons/drop_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/reflection_x.svg b/resources/icons/reflection_x.svg new file mode 100644 index 0000000..7b9e18c --- /dev/null +++ b/resources/icons/reflection_x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/reflection_y.svg b/resources/icons/reflection_y.svg new file mode 100644 index 0000000..97581af --- /dev/null +++ b/resources/icons/reflection_y.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/refresh.svg b/resources/icons/refresh.svg new file mode 100644 index 0000000..c9dedef --- /dev/null +++ b/resources/icons/refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/scalar_param.svg b/resources/icons/scalar_param.svg new file mode 100644 index 0000000..f9386ab --- /dev/null +++ b/resources/icons/scalar_param.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/spin_dec.svg b/resources/icons/spin_dec.svg new file mode 100644 index 0000000..889ad9c --- /dev/null +++ b/resources/icons/spin_dec.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/spin_dec_act.svg b/resources/icons/spin_dec_act.svg new file mode 100644 index 0000000..a9924d8 --- /dev/null +++ b/resources/icons/spin_dec_act.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/spin_inc.svg b/resources/icons/spin_inc.svg new file mode 100644 index 0000000..cf6b3a6 --- /dev/null +++ b/resources/icons/spin_inc.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/spin_inc_act.svg b/resources/icons/spin_inc_act.svg new file mode 100644 index 0000000..e052bb7 --- /dev/null +++ b/resources/icons/spin_inc_act.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/svg_modifier.svg b/resources/icons/svg_modifier.svg new file mode 100644 index 0000000..97cf900 --- /dev/null +++ b/resources/icons/svg_modifier.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/svg_negative.svg b/resources/icons/svg_negative.svg new file mode 100644 index 0000000..5335a78 --- /dev/null +++ b/resources/icons/svg_negative.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/icons/svg_part.svg b/resources/icons/svg_part.svg new file mode 100644 index 0000000..5686024 --- /dev/null +++ b/resources/icons/svg_part.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/icons/toggle_off.svg b/resources/icons/toggle_off.svg new file mode 100644 index 0000000..4a7925a --- /dev/null +++ b/resources/icons/toggle_off.svg @@ -0,0 +1,5 @@ + + + rect x="1" y="1.00293" width="22" height="12" rx="6" fill="#BBG"/--> + + diff --git a/resources/icons/toggle_on.svg b/resources/icons/toggle_on.svg new file mode 100644 index 0000000..caae947 --- /dev/null +++ b/resources/icons/toggle_on.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/vector_filament_param.svg b/resources/icons/vector_filament_param.svg new file mode 100644 index 0000000..a3404cf --- /dev/null +++ b/resources/icons/vector_filament_param.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/vector_param.svg b/resources/icons/vector_param.svg new file mode 100644 index 0000000..a5c8aff --- /dev/null +++ b/resources/icons/vector_param.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/localization/list.txt b/resources/localization/list.txt index de553f2..18ef74b 100644 --- a/resources/localization/list.txt +++ b/resources/localization/list.txt @@ -33,6 +33,7 @@ src/slic3r/GUI/DesktopIntegrationDialog.cpp src/slic3r/GUI/DoubleSlider.cpp src/slic3r/GUI/Downloader.cpp src/slic3r/GUI/DownloaderFileGet.cpp +src/slic3r/GUI/EditGCodeDialog.cpp src/slic3r/GUI/ExtraRenderers.cpp src/slic3r/GUI/ExtruderSequenceDialog.cpp src/slic3r/GUI/Field.cpp @@ -43,7 +44,6 @@ src/slic3r/GUI/GCodeViewer.cpp src/slic3r/GUI/Gizmos/GLGizmoCut.cpp src/slic3r/GUI/Gizmos/GLGizmoCut.hpp src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp -src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp @@ -61,6 +61,7 @@ src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp src/slic3r/GUI/Gizmos/GLGizmosManager.cpp src/slic3r/GUI/GLCanvas3D.cpp src/slic3r/GUI/GUI.cpp @@ -110,6 +111,7 @@ src/slic3r/GUI/Tab.cpp src/slic3r/GUI/Tab.hpp src/slic3r/GUI/UnsavedChangesDialog.cpp src/slic3r/GUI/UpdateDialogs.cpp +src/slic3r/GUI/WifiConfigDialog.cpp src/slic3r/GUI/WipeTowerDialog.cpp src/slic3r/GUI/wxExtensions.cpp src/slic3r/Utils/AppUpdater.cpp diff --git a/resources/shaders/110/mm_gouraud.vs b/resources/shaders/110/mm_gouraud.vs index 10c2523..86cd1c0 100644 --- a/resources/shaders/110/mm_gouraud.vs +++ b/resources/shaders/110/mm_gouraud.vs @@ -1,11 +1,9 @@ #version 110 -const vec3 ZERO = vec3(0.0, 0.0, 0.0); - uniform mat4 view_model_matrix; uniform mat4 projection_matrix; - uniform mat4 volume_world_matrix; + // Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane. uniform vec2 z_range; // Clipping plane - general orientation. Used by the SLA gizmo. diff --git a/resources/shaders/140/mm_gouraud.vs b/resources/shaders/140/mm_gouraud.vs index 30223f4..f8b5152 100644 --- a/resources/shaders/140/mm_gouraud.vs +++ b/resources/shaders/140/mm_gouraud.vs @@ -1,11 +1,9 @@ #version 140 -const vec3 ZERO = vec3(0.0, 0.0, 0.0); - uniform mat4 view_model_matrix; uniform mat4 projection_matrix; - uniform mat4 volume_world_matrix; + // Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane. uniform vec2 z_range; // Clipping plane - general orientation. Used by the SLA gizmo. diff --git a/resources/shaders/ES/mm_gouraud.vs b/resources/shaders/ES/mm_gouraud.vs index 8ad5fca..cd9f407 100644 --- a/resources/shaders/ES/mm_gouraud.vs +++ b/resources/shaders/ES/mm_gouraud.vs @@ -1,11 +1,9 @@ #version 100 -const vec3 ZERO = vec3(0.0, 0.0, 0.0); - uniform mat4 view_model_matrix; uniform mat4 projection_matrix; - uniform mat4 volume_world_matrix; + // Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane. uniform vec2 z_range; // Clipping plane - general orientation. Used by the SLA gizmo. diff --git a/resources/stl_thumb/STLThumbWinShellExtension.dll b/resources/stl_thumb/STLThumbWinShellExtension.dll new file mode 100644 index 0000000..2b19d1f Binary files /dev/null and b/resources/stl_thumb/STLThumbWinShellExtension.dll differ diff --git a/resources/stl_thumb/stl-thumb.exe b/resources/stl_thumb/stl-thumb.exe new file mode 100644 index 0000000..961fa38 Binary files /dev/null and b/resources/stl_thumb/stl-thumb.exe differ diff --git a/resources/web/guide/index.html b/resources/web/guide/index.html index 5b55a55..cd97a3a 100644 --- a/resources/web/guide/index.html +++ b/resources/web/guide/index.html @@ -398,6 +398,22 @@ 2399.00 ± 147.00 12.90 ± 0.90 + + ASA-Aero + ≤30% + / + ++ + + + ++ + 70℃ + 55℃ + 8.25 ± 0.15 + 593.99 ± 24.34 + 11.52 ± 0.58 + 14.31 ± 1.66 + 457.94 ± 20.84 + 2.29 ± 0.13 + PA12-CF ≤15% @@ -430,6 +446,22 @@ 5969.35 ± 145.28 6.17 ± 0.2 + + PC/ABS-FR + ≤15% + / + +++ + + + +++ + 88℃ + 83℃ + 52.51 ± 0.28 + 2588.73 ± 64.81 + 5.55 ± 0.99 + 85.95 ± 0.83 + 2504.55 ± 22.88 + 8.39 ± 0.46 + PET-CF ≤15% diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 874209f..cdc625d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -60,8 +60,8 @@ if (SLIC3R_GUI) message(STATUS "wx-config path: ${wxWidgets_CONFIG_EXECUTABLE}") endif() - find_package(JPEG QUIET) - find_package(TIFF QUIET) + find_package(JPEG MODULE QUIET) + find_package(TIFF MODULE QUIET) # Tiff exported config is broken for static build find_package(NanoSVG REQUIRED) string(REGEX MATCH "wxpng" WX_PNG_BUILTIN ${wxWidgets_LIBRARIES}) diff --git a/src/QIDISlicer.cpp b/src/QIDISlicer.cpp index 5e1bc08..1c6c231 100644 --- a/src/QIDISlicer.cpp +++ b/src/QIDISlicer.cpp @@ -2,8 +2,12 @@ // Why? #define _WIN32_WINNT 0x0502 // The standard Windows includes. + #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN + #endif // WIN32_LEAN_AND_MEAN + #ifndef NOMINMAX #define NOMINMAX + #endif // NOMINMAX #include #include #ifdef SLIC3R_GUI @@ -272,6 +276,20 @@ int CLI::run(int argc, char **argv) } } + if (!start_gui) { + const auto* post_process = m_print_config.opt("post_process"); + if (post_process != nullptr && !post_process->values.empty()) { + boost::nowide::cout << "\nA post-processing script has been detected in the config data:\n\n"; + for (const auto& s : post_process->values) { + boost::nowide::cout << "> " << s << "\n"; + } + boost::nowide::cout << "\nContinue(Y/N) ? "; + char in; + boost::nowide::cin >> in; + if (in != 'Y' && in != 'y') + return 0; + } + } // Apply command line options to a more specific DynamicPrintConfig which provides normalize() // (command line options override --load files) m_print_config.apply(m_extra_config, true); diff --git a/src/QIDISlicer_app_msvc.cpp b/src/QIDISlicer_app_msvc.cpp index ba6beef..a142ddf 100644 --- a/src/QIDISlicer_app_msvc.cpp +++ b/src/QIDISlicer_app_msvc.cpp @@ -1,8 +1,12 @@ // Why? #define _WIN32_WINNT 0x0502 // The standard Windows includes. +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif // WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX #define NOMINMAX +#endif // NOMINMAX #include #include #include diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index cd51ccc..0229f9a 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -84,15 +84,25 @@ inline IntPoint IntPoint2d(cInt x, cInt y) ); } -inline cInt Round(double val) +// Fast rounding upwards. +inline double FRound(double a) { - double v = val < 0 ? val - 0.5 : val + 0.5; + // Why does Java Math.round(0.49999999999999994) return 1? + // https://stackoverflow.com/questions/9902968/why-does-math-round0-49999999999999994-return-1 + return a == 0.49999999999999994 ? 0 : floor(a + 0.5); +} + +template +inline IType Round(double val) +{ + double v = FRound(val); #if defined(CLIPPERLIB_INT32) && ! defined(NDEBUG) - static constexpr const double hi = 65536 * 16383; + static_assert(sizeof(IType) == 4 || sizeof(IType) == 8, "IType must be int32 or int64"); + static constexpr const double hi = 65536. * 16383. * (sizeof(IType) == 4 ? 1 : 65536. * 65536.); if (v > hi || -v > hi) throw clipperException("Coordinate outside allowed range"); #endif - return static_cast(v); + return static_cast(v); } // Overriding the Eigen operators because we don't want to compare Z coordinate if IntPoint is 3 dimensional. @@ -340,7 +350,7 @@ inline cInt TopX(TEdge &edge, const cInt currentY) { return (currentY == edge.Top.y()) ? edge.Top.x() : - edge.Bot.x() + Round(edge.Dx *(currentY - edge.Bot.y())); + edge.Bot.x() + Round(edge.Dx *(currentY - edge.Bot.y())); } //------------------------------------------------------------------------------ @@ -350,65 +360,53 @@ void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) ip.z() = 0; #endif - double b1, b2; if (Edge1.Dx == Edge2.Dx) { ip.y() = Edge1.Curr.y(); ip.x() = TopX(Edge1, ip.y()); return; } - else if (Edge1.Delta.x() == 0) + + int64_t y; + if (Edge1.Delta.x() == 0) { ip.x() = Edge1.Bot.x(); - if (IsHorizontal(Edge2)) - ip.y() = Edge2.Bot.y(); - else - { - b2 = Edge2.Bot.y() - (Edge2.Bot.x() / Edge2.Dx); - ip.y() = Round(ip.x() / Edge2.Dx + b2); - } + y = IsHorizontal(Edge2) ? + Edge2.Bot.y() : + Round(ip.x() / Edge2.Dx + Edge2.Bot.y() - (Edge2.Bot.x() / Edge2.Dx)); + } else if (Edge2.Delta.x() == 0) { ip.x() = Edge2.Bot.x(); - if (IsHorizontal(Edge1)) - ip.y() = Edge1.Bot.y(); - else - { - b1 = Edge1.Bot.y() - (Edge1.Bot.x() / Edge1.Dx); - ip.y() = Round(ip.x() / Edge1.Dx + b1); - } - } - else + y = IsHorizontal(Edge1) ? + Edge1.Bot.y() : + Round(ip.x() / Edge1.Dx + Edge1.Bot.y() - (Edge1.Bot.x() / Edge1.Dx)); + } + else { - b1 = double(Edge1.Bot.x()) - double(Edge1.Bot.y()) * Edge1.Dx; - b2 = double(Edge2.Bot.x()) - double(Edge2.Bot.y()) * Edge2.Dx; + double b1 = double(Edge1.Bot.x()) - double(Edge1.Bot.y()) * Edge1.Dx; + double b2 = double(Edge2.Bot.x()) - double(Edge2.Bot.y()) * Edge2.Dx; double q = (b2-b1) / (Edge1.Dx - Edge2.Dx); - ip.y() = Round(q); - ip.x() = (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) ? - Round(Edge1.Dx * q + b1) : - Round(Edge2.Dx * q + b2); + y = Round(q); + ip.x() = (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) ? + Round(Edge1.Dx * q + b1) : + Round(Edge2.Dx * q + b2); } - if (ip.y() < Edge1.Top.y() || ip.y() < Edge2.Top.y()) + ip.y() = cInt(y); + if (y < Edge1.Top.y() || y < Edge2.Top.y()) { - if (Edge1.Top.y() > Edge2.Top.y()) - ip.y() = Edge1.Top.y(); - else - ip.y() = Edge2.Top.y(); - if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) - ip.x() = TopX(Edge1, ip.y()); - else - ip.x() = TopX(Edge2, ip.y()); - } + ip.y() = (Edge1.Top.y() > Edge2.Top.y() ? Edge1 : Edge2).Top.y(); + y = ip.y(); + ip.x() = TopX(std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx) ? Edge1 : Edge2, ip.y()); + } //finally, don't allow 'ip' to be BELOW curr.y() (ie bottom of scanbeam) ... - if (ip.y() > Edge1.Curr.y()) + if (y > Edge1.Curr.y()) { ip.y() = Edge1.Curr.y(); //use the more vertical edge to derive X ... - if (std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx)) - ip.x() = TopX(Edge2, ip.y()); else - ip.x() = TopX(Edge1, ip.y()); + ip.x() = TopX(std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx) ? Edge2 : Edge1, ip.y()); } } //------------------------------------------------------------------------------ @@ -3539,8 +3537,8 @@ void ClipperOffset::DoOffset(double delta) for (cInt j = 1; j <= steps; j++) { m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[0].x() + X * delta), - Round(m_srcPoly[0].y() + Y * delta))); + Round(m_srcPoly[0].x() + X * delta), + Round(m_srcPoly[0].y() + Y * delta))); double X2 = X; X = X * m_cos - m_sin * Y; Y = X2 * m_sin + Y * m_cos; @@ -3552,8 +3550,8 @@ void ClipperOffset::DoOffset(double delta) for (int j = 0; j < 4; ++j) { m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[0].x() + X * delta), - Round(m_srcPoly[0].y() + Y * delta))); + Round(m_srcPoly[0].x() + X * delta), + Round(m_srcPoly[0].y() + Y * delta))); if (X < 0) X = 1; else if (Y < 0) Y = 1; else X = -1; @@ -3606,9 +3604,9 @@ void ClipperOffset::DoOffset(double delta) if (node.m_endtype == etOpenButt) { int j = len - 1; - pt1 = IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * delta), Round(m_srcPoly[j].y() + m_normals[j].y() * delta)); + pt1 = IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * delta), Round(m_srcPoly[j].y() + m_normals[j].y() * delta)); m_destPoly.emplace_back(pt1); - pt1 = IntPoint2d(Round(m_srcPoly[j].x() - m_normals[j].x() * delta), Round(m_srcPoly[j].y() - m_normals[j].y() * delta)); + pt1 = IntPoint2d(Round(m_srcPoly[j].x() - m_normals[j].x() * delta), Round(m_srcPoly[j].y() - m_normals[j].y() * delta)); m_destPoly.emplace_back(pt1); } else @@ -3633,9 +3631,9 @@ void ClipperOffset::DoOffset(double delta) if (node.m_endtype == etOpenButt) { - pt1 = IntPoint2d(Round(m_srcPoly[0].x() - m_normals[0].x() * delta), Round(m_srcPoly[0].y() - m_normals[0].y() * delta)); + pt1 = IntPoint2d(Round(m_srcPoly[0].x() - m_normals[0].x() * delta), Round(m_srcPoly[0].y() - m_normals[0].y() * delta)); m_destPoly.emplace_back(pt1); - pt1 = IntPoint2d(Round(m_srcPoly[0].x() + m_normals[0].x() * delta), Round(m_srcPoly[0].y() + m_normals[0].y() * delta)); + pt1 = IntPoint2d(Round(m_srcPoly[0].x() + m_normals[0].x() * delta), Round(m_srcPoly[0].y() + m_normals[0].y() * delta)); m_destPoly.emplace_back(pt1); } else @@ -3663,8 +3661,8 @@ void ClipperOffset::OffsetPoint(int j, int& k, JoinType jointype) double cosA = (m_normals[k].x() * m_normals[j].x() + m_normals[j].y() * m_normals[k].y() ); if (cosA > 0) // angle => 0 degrees { - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), + Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); return; } //else angle => 180 degrees @@ -3674,11 +3672,11 @@ void ClipperOffset::OffsetPoint(int j, int& k, JoinType jointype) if (m_sinA * m_delta < 0) { - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), + Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); m_destPoly.emplace_back(m_srcPoly[j]); - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), + Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); } else switch (jointype) @@ -3702,19 +3700,19 @@ void ClipperOffset::DoSquare(int j, int k) double dx = std::tan(std::atan2(m_sinA, m_normals[k].x() * m_normals[j].x() + m_normals[k].y() * m_normals[j].y()) / 4); m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + m_delta * (m_normals[k].x() - m_normals[k].y() * dx)), - Round(m_srcPoly[j].y() + m_delta * (m_normals[k].y() + m_normals[k].x() * dx)))); + Round(m_srcPoly[j].x() + m_delta * (m_normals[k].x() - m_normals[k].y() * dx)), + Round(m_srcPoly[j].y() + m_delta * (m_normals[k].y() + m_normals[k].x() * dx)))); m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + m_delta * (m_normals[j].x() + m_normals[j].y() * dx)), - Round(m_srcPoly[j].y() + m_delta * (m_normals[j].y() - m_normals[j].x() * dx)))); + Round(m_srcPoly[j].x() + m_delta * (m_normals[j].x() + m_normals[j].y() * dx)), + Round(m_srcPoly[j].y() + m_delta * (m_normals[j].y() - m_normals[j].x() * dx)))); } //------------------------------------------------------------------------------ void ClipperOffset::DoMiter(int j, int k, double r) { double q = m_delta / r; - m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + (m_normals[k].x() + m_normals[j].x()) * q), - Round(m_srcPoly[j].y() + (m_normals[k].y() + m_normals[j].y()) * q))); + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + (m_normals[k].x() + m_normals[j].x()) * q), + Round(m_srcPoly[j].y() + (m_normals[k].y() + m_normals[j].y()) * q))); } //------------------------------------------------------------------------------ @@ -3722,21 +3720,21 @@ void ClipperOffset::DoRound(int j, int k) { double a = std::atan2(m_sinA, m_normals[k].x() * m_normals[j].x() + m_normals[k].y() * m_normals[j].y()); - auto steps = std::max(Round(m_StepsPerRad * std::fabs(a)), 1); + auto steps = std::max(Round(m_StepsPerRad * std::fabs(a)), 1); double X = m_normals[k].x(), Y = m_normals[k].y(), X2; for (int i = 0; i < steps; ++i) { m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + X * m_delta), - Round(m_srcPoly[j].y() + Y * m_delta))); + Round(m_srcPoly[j].x() + X * m_delta), + Round(m_srcPoly[j].y() + Y * m_delta))); X2 = X; X = X * m_cos - m_sin * Y; Y = X2 * m_sin + Y * m_cos; } m_destPoly.emplace_back(IntPoint2d( - Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), - Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); + Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), + Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); } //------------------------------------------------------------------------------ diff --git a/src/hints/HintsToPot.cpp b/src/hints/HintsToPot.cpp index 957c3ea..064e1bd 100644 --- a/src/hints/HintsToPot.cpp +++ b/src/hints/HintsToPot.cpp @@ -6,6 +6,7 @@ #include #include #include +#include bool write_to_pot(boost::filesystem::path path, const std::vector>& data) { @@ -60,7 +61,7 @@ int main(int argc, char* argv[]) } try { path_to_ini = boost::filesystem::canonical(boost::filesystem::path(argv[1])).parent_path() / "resources" / "data" / "hints.ini"; - path_to_pot = boost::filesystem::canonical(boost::filesystem::path(argv[2])).parent_path() / "localization" /"QIDISlicer.pot"; + path_to_pot = boost::filesystem::canonical(boost::filesystem::path(argv[2])).parent_path() / "localization" /"PrusaSlicer.pot"; } catch (std::exception&) { std::cout << "HINTS_TO_POT FAILED: BOOST CANNONICAL" << std::endl; return -1; diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 990b319..9331481 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -117,7 +117,8 @@ inline std::tuple coordinate_aligned_ray_hit_count(size_t } template -inline std::vector> get_intersections_with_line(size_t node_idx, +inline void insert_intersections_with_line(std::vector> &result, + size_t node_idx, const TreeType &tree, const std::vector &lines, const LineType &line, @@ -128,11 +129,10 @@ inline std::vector> get_intersections_with_line(si if (node.is_leaf()) { VectorType intersection_pt; if (line_alg::intersection(line, lines[node.idx], &intersection_pt)) { - return {std::pair(intersection_pt, node.idx)}; - } else { - return {}; + result.emplace_back(intersection_pt, node.idx); + } + return; } - } else { size_t left_node_idx = node_idx * 2 + 1; size_t right_node_idx = left_node_idx + 1; const auto &node_left = tree.node(left_node_idx); @@ -140,22 +140,15 @@ inline std::vector> get_intersections_with_line(si assert(node_left.is_valid()); assert(node_right.is_valid()); - std::vector> result; if (node_left.bbox.intersects(line_bb)) { - std::vector> intersections = - get_intersections_with_line(left_node_idx, tree, lines, line, line_bb); - result.insert(result.end(), intersections.begin(), intersections.end()); + insert_intersections_with_line(result, left_node_idx, tree, lines, line, line_bb); } if (node_right.bbox.intersects(line_bb)) { - std::vector> intersections = - get_intersections_with_line(right_node_idx, tree, lines, line, line_bb); - result.insert(result.end(), intersections.begin(), intersections.end()); + insert_intersections_with_line(result, right_node_idx, tree, lines, line, line_bb); } - return result; - } } } // namespace detail @@ -273,7 +266,8 @@ inline std::vector> get_intersections_with_line(co auto line_bb = typename TreeType::BoundingBox(line.a, line.a); line_bb.extend(line.b); - auto intersections = detail::get_intersections_with_line(0, tree, lines, line, line_bb); + std::vector> intersections; // result + detail::insert_intersections_with_line(intersections, 0, tree, lines, line, line_bb); if (sorted) { using Floating = typename std::conditional::value, typename LineType::Scalar, double>::type; @@ -345,7 +339,7 @@ public: return dist; } - std::vector all_lines_in_radius(const Vec<2, Scalar> &point, Floating radius) + std::vector all_lines_in_radius(const Vec<2, Scalar> &point, Floating radius) const { return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast(), radius * radius); } diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 57219e8..a9f8bcb 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -90,11 +90,11 @@ void AppConfig::set_defaults() set("associate_3mf", "0"); if (get("associate_stl").empty()) set("associate_stl", "0"); - if (get("associate_step").empty()) - set("associate_step", "0"); if (get("tabs_as_menu").empty()) set("tabs_as_menu", "0"); + if (get("suppress_round_corners").empty()) + set("suppress_round_corners", "1"); #endif // _WIN32 // remove old 'use_legacy_opengl' parameter from this config, if present @@ -151,7 +151,7 @@ void AppConfig::set_defaults() set("default_action_on_new_project", "none"); // , "keep(transfer)", "discard" or "save" if (get("color_mapinulation_panel").empty()) - set("color_mapinulation_panel", "1"); + set("color_mapinulation_panel", "0"); if (get("order_volumes").empty()) set("order_volumes", "1"); @@ -166,6 +166,8 @@ void AppConfig::set_defaults() #ifdef _WIN32 if (get("associate_gcode").empty()) set("associate_gcode", "0"); + if (get("associate_bgcode").empty()) + set("associate_bgcode", "0"); #endif // _WIN32 } @@ -196,6 +198,8 @@ void AppConfig::set_defaults() if (get("allow_ip_resolve").empty()) set("allow_ip_resolve", "1"); + if (get("wifi_config_dialog_declined").empty()) + set("wifi_config_dialog_declined", "0"); #ifdef _WIN32 if (get("use_legacy_3DConnexion").empty()) set("use_legacy_3DConnexion", "0"); @@ -208,7 +212,6 @@ void AppConfig::set_defaults() //B45 if (get("machine_list_minification").empty()) set("machine_list_minification", "1"); - #endif // _WIN32 // Remove legacy window positions/sizes @@ -360,21 +363,11 @@ std::string AppConfig::load(const std::string &path) const auto model_name = kvp.first.substr(MODEL_PREFIX.size()); std::vector variants; //B9 - std::vector emails; - std::vector skypes; if (! unescape_strings_cstyle(kvp.second.data(), variants)) { continue; } for (const auto &variant : variants) { vendor[model_name].insert(variant); } //B19 - if (! unescape_strings_cstyle(kvp.second.data(), emails)) { continue; } - for (const auto &email : emails) { - vendor[model_name].insert(email); - } - if (! unescape_strings_cstyle(kvp.second.data(), skypes)) { continue; } - for (const auto &skype : skypes) { - vendor[model_name].insert(skype); - } } } else { // This must be a section name. Read the entries of a section. @@ -390,11 +383,17 @@ std::string AppConfig::load(const std::string &path) //B7 // if (ini_ver) { // m_orig_version = *ini_ver; + if (ini_ver) { + m_orig_version = *ini_ver; // // Make 1.40.0 alphas compare well // ini_ver->set_metadata(boost::none); // ini_ver->set_prerelease(boost::none); // m_legacy_datadir = ini_ver < Semver(1, 40, 0); // } + ini_ver->set_metadata(boost::none); + ini_ver->set_prerelease(boost::none); + m_legacy_datadir = ini_ver < Semver(1, 40, 0); + } // Legacy conversion if (m_mode == EAppMode::Editor) { diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp index 37d3d4e..853facb 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp @@ -9,29 +9,31 @@ #include "RedistributeBeadingStrategy.hpp" #include "OuterWallInsetBeadingStrategy.hpp" -#include #include -namespace Slic3r::Arachne -{ +namespace Slic3r::Arachne { -BeadingStrategyPtr BeadingStrategyFactory::makeStrategy( - const coord_t preferred_bead_width_outer, - const coord_t preferred_bead_width_inner, - const coord_t preferred_transition_length, - const float transitioning_angle, - const bool print_thin_walls, - const coord_t min_bead_width, - const coord_t min_feature_size, - const double wall_split_middle_threshold, - const double wall_add_middle_threshold, - const coord_t max_bead_count, - const coord_t outer_wall_offset, - const int inward_distributed_center_wall_count, - const double minimum_variable_line_ratio -) +BeadingStrategyPtr BeadingStrategyFactory::makeStrategy(const coord_t preferred_bead_width_outer, + const coord_t preferred_bead_width_inner, + const coord_t preferred_transition_length, + const float transitioning_angle, + const bool print_thin_walls, + const coord_t min_bead_width, + const coord_t min_feature_size, + const double wall_split_middle_threshold, + const double wall_add_middle_threshold, + const coord_t max_bead_count, + const coord_t outer_wall_offset, + const int inward_distributed_center_wall_count, + const double minimum_variable_line_ratio) { - BeadingStrategyPtr ret = std::make_unique(preferred_bead_width_inner, preferred_transition_length, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold, inward_distributed_center_wall_count); + // Handle a special case when there is just one external perimeter. + // Because big differences in bead width for inner and other perimeters cause issues with current beading strategies. + const coord_t optimal_width = max_bead_count <= 2 ? preferred_bead_width_outer : preferred_bead_width_inner; + BeadingStrategyPtr ret = std::make_unique(optimal_width, preferred_transition_length, transitioning_angle, + wall_split_middle_threshold, wall_add_middle_threshold, + inward_distributed_center_wall_count); + BOOST_LOG_TRIVIAL(trace) << "Applying the Redistribute meta-strategy with outer-wall width = " << preferred_bead_width_outer << ", inner-wall width = " << preferred_bead_width_inner << "."; ret = std::make_unique(preferred_bead_width_outer, minimum_variable_line_ratio, std::move(ret)); @@ -39,12 +41,13 @@ BeadingStrategyPtr BeadingStrategyFactory::makeStrategy( BOOST_LOG_TRIVIAL(trace) << "Applying the Widening Beading meta-strategy with minimum input width " << min_feature_size << " and minimum output width " << min_bead_width << "."; ret = std::make_unique(std::move(ret), min_feature_size, min_bead_width); } + if (outer_wall_offset > 0) { BOOST_LOG_TRIVIAL(trace) << "Applying the OuterWallOffset meta-strategy with offset = " << outer_wall_offset << "."; ret = std::make_unique(outer_wall_offset, std::move(ret)); } - //Apply the LimitedBeadingStrategy last, since that adds a 0-width marker wall which other beading strategies shouldn't touch. + // Apply the LimitedBeadingStrategy last, since that adds a 0-width marker wall which other beading strategies shouldn't touch. BOOST_LOG_TRIVIAL(trace) << "Applying the Limited Beading meta-strategy with maximum bead count = " << max_bead_count << "."; ret = std::make_unique(max_bead_count, std::move(ret)); return ret; diff --git a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp index eefcab8..5345c49 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp @@ -24,17 +24,16 @@ WideningBeadingStrategy::Beading WideningBeadingStrategy::compute(coord_t thickn if (thickness < optimal_width) { Beading ret; ret.total_thickness = thickness; - if (thickness >= min_input_width) - { + if (thickness >= min_input_width) { ret.bead_widths.emplace_back(std::max(thickness, min_output_width)); ret.toolpath_locations.emplace_back(thickness / 2); - } else { + ret.left_over = 0; + } else ret.left_over = thickness; - } + return ret; - } else { + } else return parent->compute(thickness, bead_count); - } } coord_t WideningBeadingStrategy::getOptimalThickness(coord_t bead_count) const diff --git a/src/libslic3r/Arrange/ArrangeImpl.hpp b/src/libslic3r/Arrange/ArrangeImpl.hpp index b7e8af3..434bdd4 100644 --- a/src/libslic3r/Arrange/ArrangeImpl.hpp +++ b/src/libslic3r/Arrange/ArrangeImpl.hpp @@ -441,7 +441,7 @@ ArrItem AdvancedItemConverter::get_arritem(const Arrangeable &arrbl, auto simpl_tol = static_cast(this->simplification_tolerance()); - if (simpl_tol > 0) + if (simpl_tol > 0.) { outline = expolygons_simplify(outline, simpl_tol); if (!envelope.empty()) diff --git a/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.cpp b/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.cpp index 4e289a7..a0ecbf8 100644 --- a/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.cpp +++ b/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.cpp @@ -4,6 +4,11 @@ namespace Slic3r { ArrangeSettingsDb_AppCfg::ArrangeSettingsDb_AppCfg(AppConfig *appcfg) : m_appcfg{appcfg} +{ + sync(); +} + +void ArrangeSettingsDb_AppCfg::sync() { m_settings_fff.postfix = "_fff"; m_settings_fff_seq.postfix = "_fff_seq_print"; @@ -57,27 +62,41 @@ ArrangeSettingsDb_AppCfg::ArrangeSettingsDb_AppCfg(AppConfig *appcfg) : m_appcfg if (!dist_fff_str.empty()) m_settings_fff.vals.d_obj = string_to_float_decimal_point(dist_fff_str); + else + m_settings_fff.vals.d_obj = m_settings_fff.defaults.d_obj; if (!dist_bed_fff_str.empty()) m_settings_fff.vals.d_bed = string_to_float_decimal_point(dist_bed_fff_str); + else + m_settings_fff.vals.d_bed = m_settings_fff.defaults.d_bed; if (!dist_fff_seq_print_str.empty()) m_settings_fff_seq.vals.d_obj = string_to_float_decimal_point(dist_fff_seq_print_str); + else + m_settings_fff_seq.vals.d_obj = m_settings_fff_seq.defaults.d_obj; if (!dist_bed_fff_seq_print_str.empty()) m_settings_fff_seq.vals.d_bed = string_to_float_decimal_point(dist_bed_fff_seq_print_str); + else + m_settings_fff_seq.vals.d_bed = m_settings_fff_seq.defaults.d_bed; if (!dist_sla_str.empty()) m_settings_sla.vals.d_obj = string_to_float_decimal_point(dist_sla_str); + else + m_settings_sla.vals.d_obj = m_settings_sla.defaults.d_obj; if (!dist_bed_sla_str.empty()) m_settings_sla.vals.d_bed = string_to_float_decimal_point(dist_bed_sla_str); + else + m_settings_sla.vals.d_bed = m_settings_sla.defaults.d_bed; if (!en_rot_fff_str.empty()) m_settings_fff.vals.rotations = (en_rot_fff_str == "1" || en_rot_fff_str == "yes"); if (!en_rot_fff_seqp_str.empty()) m_settings_fff_seq.vals.rotations = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "yes"); + else + m_settings_fff_seq.vals.rotations = m_settings_fff_seq.defaults.rotations; if (!en_rot_sla_str.empty()) m_settings_sla.vals.rotations = (en_rot_sla_str == "1" || en_rot_sla_str == "yes"); @@ -90,38 +109,26 @@ ArrangeSettingsDb_AppCfg::ArrangeSettingsDb_AppCfg(AppConfig *appcfg) : m_appcfg // if (!alignment_fff_seqp_str.empty()) // m_arrange_settings_fff_seq_print.alignment = std::stoi(alignment_fff_seqp_str); + else + m_settings_sla.vals.rotations = m_settings_sla.defaults.rotations; // Override default alignment and save save/load it to a temporary slot "alignment_xl" - ArrangeSettingsView::XLPivots arr_alignment = ArrangeSettingsView::xlpFrontLeft; - if (!alignment_xl_str.empty()) { - int align_val = std::stoi(alignment_xl_str); - - if (align_val >= 0 && align_val < ArrangeSettingsView::xlpCount) - arr_alignment = - static_cast(align_val); - } + auto arr_alignment = ArrangeSettingsView::to_xl_pivots(alignment_xl_str) + .value_or(m_settings_fff.defaults.xl_align); m_settings_sla.vals.xl_align = arr_alignment ; m_settings_fff.vals.xl_align = arr_alignment ; m_settings_fff_seq.vals.xl_align = arr_alignment ; - ArrangeSettingsView::GeometryHandling geom_handl = arr2::ArrangeSettingsView::ghConvex; - if (!geom_handling_str.empty()) { - int gh = std::stoi(geom_handling_str); - if(gh >= 0 && gh < ArrangeSettingsView::GeometryHandling::ghCount) - geom_handl = static_cast(gh); - } + auto geom_handl = ArrangeSettingsView::to_geometry_handling(geom_handling_str) + .value_or(m_settings_fff.defaults.geom_handling); m_settings_sla.vals.geom_handling = geom_handl; m_settings_fff.vals.geom_handling = geom_handl; m_settings_fff_seq.vals.geom_handling = geom_handl; - ArrangeSettingsView::ArrangeStrategy arr_strategy = arr2::ArrangeSettingsView::asAuto; - if (!strategy_str.empty()) { - int strateg = std::stoi(strategy_str); - if(strateg >= 0 && strateg < ArrangeSettingsView::ArrangeStrategy::asCount) - arr_strategy = static_cast(strateg); - } + auto arr_strategy = ArrangeSettingsView::to_arrange_strategy(strategy_str) + .value_or(m_settings_fff.defaults.arr_strategy); m_settings_sla.vals.arr_strategy = arr_strategy; m_settings_fff.vals.arr_strategy = arr_strategy; @@ -174,7 +181,7 @@ arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_rotation_enabled(bool v) arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_xl_alignment(XLPivots v) { m_settings_fff.vals.xl_align = v; - m_appcfg->set("arrange", "alignment_xl", std::to_string(v)); + m_appcfg->set("arrange", "alignment_xl", std::string{get_label(v)}); return *this; } @@ -182,7 +189,7 @@ arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_xl_alignment(XLPivots v) arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_geometry_handling(GeometryHandling v) { m_settings_fff.vals.geom_handling = v; - m_appcfg->set("arrange", "geometry_handling", std::to_string(v)); + m_appcfg->set("arrange", "geometry_handling", std::string{get_label(v)}); return *this; } @@ -190,7 +197,7 @@ arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_geometry_handling(Geometr arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_arrange_strategy(ArrangeStrategy v) { m_settings_fff.vals.arr_strategy = v; - m_appcfg->set("arrange", "arrange_strategy", std::to_string(v)); + m_appcfg->set("arrange", "arrange_strategy", std::string{get_label(v)}); return *this; } diff --git a/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.hpp b/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.hpp index c3b6e07..570510c 100644 --- a/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.hpp +++ b/src/libslic3r/Arrange/ArrangeSettingsDb_AppCfg.hpp @@ -52,6 +52,7 @@ private: public: explicit ArrangeSettingsDb_AppCfg(AppConfig *appcfg); + void sync(); float get_distance_from_objects() const override { return get_ref(this).d_obj; } float get_distance_from_bed() const override { return get_ref(this).d_bed; } bool is_rotation_enabled() const override { return get_ref(this).rotations; } diff --git a/src/libslic3r/Arrange/ArrangeSettingsView.hpp b/src/libslic3r/Arrange/ArrangeSettingsView.hpp index 53791b1..b5a9d47 100644 --- a/src/libslic3r/Arrange/ArrangeSettingsView.hpp +++ b/src/libslic3r/Arrange/ArrangeSettingsView.hpp @@ -2,8 +2,13 @@ #ifndef ARRANGESETTINGSVIEW_HPP #define ARRANGESETTINGSVIEW_HPP +#include +#include + +#include "libslic3r/StaticMap.hpp" namespace Slic3r { namespace arr2 { +using namespace std::string_view_literals; class ArrangeSettingsView { public: @@ -28,6 +33,114 @@ public: virtual XLPivots get_xl_alignment() const = 0; virtual GeometryHandling get_geometry_handling() const = 0; virtual ArrangeStrategy get_arrange_strategy() const = 0; + static constexpr std::string_view get_label(GeometryHandling v) + { + constexpr auto STR = std::array{ + "0"sv, // convex + "1"sv, // balanced + "2"sv, // advanced + "-1"sv, // undefined + }; + + return STR[v]; + } + + static constexpr std::string_view get_label(ArrangeStrategy v) + { + constexpr auto STR = std::array{ + "0"sv, // auto + "1"sv, // pulltocenter + "-1"sv, // undefined + }; + + return STR[v]; + } + + static constexpr std::string_view get_label(XLPivots v) + { + constexpr auto STR = std::array{ + "0"sv, // center + "1"sv, // rearleft + "2"sv, // frontleft + "3"sv, // frontright + "4"sv, // rearright + "5"sv, // random + "-1"sv, // undefined + }; + + return STR[v]; + } + +private: + + template + using EnumMap = StaticMap; + + template + static constexpr std::optional get_enumval(std::string_view str, + const EnumMap &emap) + { + std::optional ret; + + if (auto v = query(emap, str); v.has_value()) { + ret = *v; + } + + return ret; + } + +public: + + static constexpr std::optional to_geometry_handling(std::string_view str) + { + return get_enumval(str, GeometryHandlingLabels); + } + + static constexpr std::optional to_arrange_strategy(std::string_view str) + { + return get_enumval(str, ArrangeStrategyLabels); + } + + static constexpr std::optional to_xl_pivots(std::string_view str) + { + return get_enumval(str, XLPivotsLabels); + } + +private: + + static constexpr const auto GeometryHandlingLabels = make_staticmap({ + {"convex"sv, ghConvex}, + {"balanced"sv, ghBalanced}, + {"advanced"sv, ghAdvanced}, + + {"0"sv, ghConvex}, + {"1"sv, ghBalanced}, + {"2"sv, ghAdvanced}, + }); + + static constexpr const auto ArrangeStrategyLabels = make_staticmap({ + {"auto"sv, asAuto}, + {"pulltocenter"sv, asPullToCenter}, + + {"0"sv, asAuto}, + {"1"sv, asPullToCenter} + }); + + static constexpr const auto XLPivotsLabels = make_staticmap({ + {"center"sv, xlpCenter }, + {"rearleft"sv, xlpRearLeft }, + {"frontleft"sv, xlpFrontLeft }, + {"frontright"sv, xlpFrontRight }, + {"rearright"sv, xlpRearRight }, + {"random"sv, xlpRandom }, + + {"0"sv, xlpCenter }, + {"1"sv, xlpRearLeft }, + {"2"sv, xlpFrontLeft }, + {"3"sv, xlpFrontRight }, + {"4"sv, xlpRearRight }, + {"5"sv, xlpRandom } + }); }; class ArrangeSettingsDb: public ArrangeSettingsView diff --git a/src/libslic3r/Arrange/Core/NFP/EdgeCache.cpp b/src/libslic3r/Arrange/Core/NFP/EdgeCache.cpp index 4ccd1e6..4a0a78a 100644 --- a/src/libslic3r/Arrange/Core/NFP/EdgeCache.cpp +++ b/src/libslic3r/Arrange/Core/NFP/EdgeCache.cpp @@ -91,7 +91,7 @@ void fill_distances(const Polygon &poly, std::vector &distances) double dist = 0.; auto lrange = line_range(poly); - for (const Line &l : lrange) { + for (const Line l : lrange) { dist += l.length(); distances.emplace_back(dist); } diff --git a/src/libslic3r/Arrange/Core/NFP/Kernels/GravityKernel.hpp b/src/libslic3r/Arrange/Core/NFP/Kernels/GravityKernel.hpp index 6507d07..875b228 100644 --- a/src/libslic3r/Arrange/Core/NFP/Kernels/GravityKernel.hpp +++ b/src/libslic3r/Arrange/Core/NFP/Kernels/GravityKernel.hpp @@ -14,7 +14,9 @@ struct GravityKernel { std::optional item_sink; Vec2d active_sink; - GravityKernel(Vec2crd gravity_center) : sink{gravity_center} {} + GravityKernel(Vec2crd gravity_center) : + sink{gravity_center}, active_sink{unscaled(gravity_center)} {} + GravityKernel() = default; template diff --git a/src/libslic3r/Arrange/Core/NFP/Kernels/KernelUtils.hpp b/src/libslic3r/Arrange/Core/NFP/Kernels/KernelUtils.hpp index 4bc0a71..a06bc75 100644 --- a/src/libslic3r/Arrange/Core/NFP/Kernels/KernelUtils.hpp +++ b/src/libslic3r/Arrange/Core/NFP/Kernels/KernelUtils.hpp @@ -24,6 +24,7 @@ bool find_initial_position(Itm &itm, { if (all_items_range(packing_context).empty()) { auto rotations = allowed_rotations(itm); + set_rotation(itm, 0.); auto chull = envelope_convex_hull(itm); for (double rot : rotations) { diff --git a/src/libslic3r/Arrange/Core/NFP/NFP.cpp b/src/libslic3r/Arrange/Core/NFP/NFP.cpp index 7b0df60..e3f4205 100644 --- a/src/libslic3r/Arrange/Core/NFP/NFP.cpp +++ b/src/libslic3r/Arrange/Core/NFP/NFP.cpp @@ -129,7 +129,7 @@ Polygon ifp_convex_convex(const Polygon &fixed, const Polygon &movable) // the zero area polygon formed by the edge. The union of all these sub-nfps // will contain a hole that is the actual ifp. auto lrange = line_range(fixed); - for (const Line &l : lrange) { // Older mac compilers generate warnging if line_range is called in-place + for (const Line l : lrange) { // Older mac compilers generate warnging if line_range is called in-place Polygon fixed = {l.a, l.b}; subnfps.emplace_back(nfp_convex_convex_legacy(fixed, movable)); } diff --git a/src/libslic3r/Arrange/Tasks/FillBedTask.hpp b/src/libslic3r/Arrange/Tasks/FillBedTask.hpp index a6c2cf1..64e3437 100644 --- a/src/libslic3r/Arrange/Tasks/FillBedTask.hpp +++ b/src/libslic3r/Arrange/Tasks/FillBedTask.hpp @@ -17,6 +17,7 @@ struct FillBedTask: public ArrangeTaskBase std::vector selected, unselected; + std::vector selected_fillers; ArrangeSettings settings; ExtendedBed bed; size_t selected_existing_count = 0; diff --git a/src/libslic3r/Arrange/Tasks/FillBedTaskImpl.hpp b/src/libslic3r/Arrange/Tasks/FillBedTaskImpl.hpp index 96d26d9..2aeceb8 100644 --- a/src/libslic3r/Arrange/Tasks/FillBedTaskImpl.hpp +++ b/src/libslic3r/Arrange/Tasks/FillBedTaskImpl.hpp @@ -102,8 +102,6 @@ void extract(FillBedTask &task, }; // Set the lowest priority to the shrinked prototype (hole filler) item - set_priority(prototype_item_shrinked, - lowest_priority(range(task.selected)) - 1); scene.model().for_each_arrangeable(collect_task_items); @@ -119,7 +117,7 @@ void extract(FillBedTask &task, // Add as many filler items as there are needed items. Most of them will // be discarded anyways. - std::fill_n(std::back_inserter(task.selected), needed_items, + std::fill_n(std::back_inserter(task.selected_fillers), needed_items, prototype_item_shrinked); } @@ -172,7 +170,7 @@ std::unique_ptr FillBedTask::process_native( void on_packed(ArrItem &itm) override { // Stop at the first filler that is not on the physical bed - do_stop = get_bed_index(itm) > PhysicalBedId && get_priority(itm) < 0; + do_stop = get_bed_index(itm) > PhysicalBedId && get_priority(itm) == 0; } } subctl(ctl, *this); @@ -181,6 +179,12 @@ std::unique_ptr FillBedTask::process_native( arranger->arrange(selected, unselected, bed, subctl); + auto unsel_cpy = unselected; + for (const auto &itm : selected) { + unsel_cpy.emplace_back(itm); + } + + arranger->arrange(selected_fillers, unsel_cpy, bed, FillBedCtl{ctl, *this}); auto arranged_range = Range{selected.begin(), selected.begin() + selected_existing_count}; @@ -193,6 +197,9 @@ std::unique_ptr FillBedTask::process_native( if (get_bed_index(itm) == PhysicalBedId) result->add_new_item(itm); + for (auto &itm : selected_fillers) + if (get_bed_index(itm) == PhysicalBedId) + result->add_new_item(itm); return result; } diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index 9a4c68a..f8da75a 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -68,7 +68,9 @@ private: template> static void construct(BoundingBoxType &out, It from, It to) { - if (from != to) { + if (from == to) { + out.defined = false; + } else { auto it = from; out.min = it->template cast(); out.max = out.min; diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index bdc1a19..672bb80 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -490,8 +490,10 @@ static void make_inner_brim(const Print &print, loops = union_pt_chained_outside_in(loops); std::reverse(loops.begin(), loops.end()); - extrusion_entities_append_loops(brim.entities, std::move(loops), ExtrusionRole::Skirt, float(flow.mm3_per_mm()), - float(flow.width()), float(print.skirt_first_layer_height())); + extrusion_entities_append_loops(brim.entities, std::move(loops), + ExtrusionAttributes{ + ExtrusionRole::Skirt, + ExtrusionFlow{ float(flow.mm3_per_mm()), float(flow.width()), float(print.skirt_first_layer_height()) } }); } // Produce brim lines around those objects, that have the brim enabled. @@ -672,7 +674,9 @@ ExtrusionEntityCollection make_brim(const Print &print, PrintTryCancel try_cance if (i + 1 == j && first_path.size() > 3 && first_path.front().x() == first_path.back().x() && first_path.front().y() == first_path.back().y()) { auto *loop = new ExtrusionLoop(); brim.entities.emplace_back(loop); - loop->paths.emplace_back(ExtrusionRole::Skirt, float(flow.mm3_per_mm()), float(flow.width()), float(print.skirt_first_layer_height())); + loop->paths.emplace_back(ExtrusionAttributes{ + ExtrusionRole::Skirt, + ExtrusionFlow{ float(flow.mm3_per_mm()), float(flow.width()), float(print.skirt_first_layer_height()) } }); Points &points = loop->paths.front().polyline.points; points.reserve(first_path.size()); for (const ClipperLib_Z::IntPoint &pt : first_path) @@ -683,7 +687,9 @@ ExtrusionEntityCollection make_brim(const Print &print, PrintTryCancel try_cance ExtrusionEntityCollection this_loop_trimmed; this_loop_trimmed.entities.reserve(j - i); for (; i < j; ++ i) { - this_loop_trimmed.entities.emplace_back(new ExtrusionPath(ExtrusionRole::Skirt, float(flow.mm3_per_mm()), float(flow.width()), float(print.skirt_first_layer_height()))); + this_loop_trimmed.entities.emplace_back(new ExtrusionPath({ + ExtrusionRole::Skirt, + ExtrusionFlow{ float(flow.mm3_per_mm()), float(flow.width()), float(print.skirt_first_layer_height()) } })); const ClipperLib_Z::Path &path = *loops_trimmed_order[i].first; Points &points = dynamic_cast(this_loop_trimmed.entities.back())->polyline.points; points.reserve(path.size()); @@ -699,7 +705,9 @@ ExtrusionEntityCollection make_brim(const Print &print, PrintTryCancel try_cance } } } else { - extrusion_entities_append_loops_and_paths(brim.entities, std::move(all_loops), ExtrusionRole::Skirt, float(flow.mm3_per_mm()), float(flow.width()), float(print.skirt_first_layer_height())); + extrusion_entities_append_loops_and_paths(brim.entities, std::move(all_loops), + ExtrusionAttributes{ ExtrusionRole::Skirt, + ExtrusionFlow{ float(flow.mm3_per_mm()), float(flow.width()), float(print.skirt_first_layer_height()) } }); } make_inner_brim(print, top_level_objects_with_brim, bottom_layers_expolygons, brim); diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 6f8a759..6cc97cc 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -14,6 +14,10 @@ if (TARGET OpenVDB::openvdb) set(OpenVDBUtils_SOURCES OpenVDBUtils.cpp OpenVDBUtils.hpp OpenVDBUtilsLegacy.hpp) endif() +find_package(LibBGCode REQUIRED COMPONENTS Convert) +slic3r_remap_configs(LibBGCode::bgcode_core RelWithDebInfo Release) +slic3r_remap_configs(LibBGCode::bgcode_binarize RelWithDebInfo Release) +slic3r_remap_configs(LibBGCode::bgcode_convert RelWithDebInfo Release) set(SLIC3R_SOURCES pchheader.cpp pchheader.hpp @@ -57,9 +61,11 @@ set(SLIC3R_SOURCES ElephantFootCompensation.hpp Emboss.cpp Emboss.hpp + EmbossShape.hpp enum_bitmask.hpp ExPolygon.cpp ExPolygon.hpp + ExPolygonSerialize.hpp ExPolygonsIndex.cpp ExPolygonsIndex.hpp Extruder.cpp @@ -132,6 +138,8 @@ set(SLIC3R_SOURCES Format/AnycubicSLA.cpp Format/STEP.hpp Format/STEP.cpp + Format/SVG.hpp + Format/SVG.cpp Format/SLAArchiveFormatRegistry.hpp Format/SLAArchiveFormatRegistry.cpp GCode/ThumbnailData.cpp @@ -142,8 +150,14 @@ set(SLIC3R_SOURCES GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp GCode/CoolingBuffer.hpp + GCode/ExtrusionProcessor.cpp + GCode/ExtrusionProcessor.hpp GCode/FindReplace.cpp GCode/FindReplace.hpp + GCode/LabelObjects.cpp + GCode/LabelObjects.hpp + GCode/GCodeWriter.cpp + GCode/GCodeWriter.hpp GCode/PostProcessor.cpp GCode/PostProcessor.hpp GCode/PressureEqualizer.cpp @@ -156,10 +170,16 @@ set(SLIC3R_SOURCES GCode/SpiralVase.hpp GCode/SeamPlacer.cpp GCode/SeamPlacer.hpp + GCode/SmoothPath.cpp + GCode/SmoothPath.hpp GCode/ToolOrdering.cpp GCode/ToolOrdering.hpp + GCode/Wipe.cpp + GCode/Wipe.hpp GCode/WipeTower.cpp GCode/WipeTower.hpp + GCode/WipeTowerIntegration.cpp + GCode/WipeTowerIntegration.hpp GCode/GCodeProcessor.cpp GCode/GCodeProcessor.hpp GCode/AvoidCrossingPerimeters.cpp @@ -170,10 +190,10 @@ set(SLIC3R_SOURCES GCodeReader.hpp # GCodeSender.cpp # GCodeSender.hpp - GCodeWriter.cpp - GCodeWriter.hpp Geometry.cpp Geometry.hpp + Geometry/ArcWelder.cpp + Geometry/ArcWelder.hpp Geometry/Bicubic.hpp Geometry/Circle.cpp Geometry/Circle.hpp @@ -476,6 +496,7 @@ set(SLIC3R_SOURCES Arachne/SkeletalTrapezoidationJoint.hpp Arachne/WallToolPaths.hpp Arachne/WallToolPaths.cpp + StaticMap.hpp ) add_library(libslic3r STATIC ${SLIC3R_SOURCES}) @@ -561,6 +582,7 @@ target_link_libraries(libslic3r ZLIB::ZLIB JPEG::JPEG qoi + LibBGCode::bgcode_convert ) if (APPLE) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index e942423..dfbb757 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -271,8 +271,8 @@ bool has_duplicate_points(const ClipperLib::PolyTree &polytree) // Offset CCW contours outside, CW contours (holes) inside. // Don't calculate union of the output paths. -template -static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) +template +static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType endType = ClipperLib::etClosedPolygon) { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); @@ -364,11 +364,11 @@ inline ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &inpu return PolyTreeToExPolygons(clipper_union(input, do_union ? ClipperLib::pftNonZero : ClipperLib::pftEvenOdd)); } -template -static ClipperLib::Paths raw_offset_polyline(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) +template +static ClipperLib::Paths raw_offset_polyline(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type = ClipperLib::etOpenButt) { assert(offset > 0); - return raw_offset(std::forward(paths), offset, joinType, miterLimit); + return raw_offset(std::forward(paths), offset, joinType, miterLimit, end_type); } template @@ -421,10 +421,17 @@ Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, Cli Slic3r::ExPolygons offset_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) { return PolyTreeToExPolygons(offset_paths(ClipperUtils::PolygonsProvider(polygons), delta, joinType, miterLimit)); } -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit))); } -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit))); } +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) + { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit, end_type))); } +Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) + { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit, end_type))); } + +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type, double miter_limit){ + assert(line_width > 1.f); return to_polygons(clipper_union( + raw_offset(ClipperUtils::SinglePathProvider(polygon.points), line_width/2, join_type, miter_limit, ClipperLib::etClosedLine)));} +Polygons contour_to_polygons(const Polygons &polygons, const float line_width, ClipperLib::JoinType join_type, double miter_limit){ + assert(line_width > 1.f); return to_polygons(clipper_union( + raw_offset(ClipperUtils::PolygonsProvider(polygons), line_width/2, join_type, miter_limit, ClipperLib::etClosedLine)));} // returns number of expolygons collected (0 or 1). static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::Paths &out) diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 7935034..7d036eb 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -30,6 +30,7 @@ static constexpr const float ClipperSafetyOffset = 10 static constexpr const Slic3r::ClipperLib::JoinType DefaultJoinType = Slic3r::ClipperLib::jtMiter; //FIXME evaluate the default miter limit. 3 seems to be extreme, Cura uses 1.2. +static constexpr const Slic3r::ClipperLib::EndType DefaultEndType = Slic3r::ClipperLib::etOpenButt; // Mitter Limit 3 is useful for perimeter generator, where sharp corners are extruded without needing a gap fill. // However such a high limit causes issues with large positive or negative offsets, where a sharp corner // is extended excessively. @@ -336,8 +337,8 @@ Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, Clipp // offset Polylines // Wherever applicable, please use the expand() / shrink() variants instead, they convey their purpose better. // Input polygons for negative offset shall be "normalized": There must be no overlap / intersections between the input polygons. -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit); -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit); +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); +Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons offset(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons offset(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); @@ -349,6 +350,8 @@ Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygons &expolygons, const float d Slic3r::ExPolygons offset_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::ExPolygons offset_ex(const Slic3r::SurfacesPtr &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); +Polygons contour_to_polygons(const Polygons &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); inline Slic3r::Polygons union_safety_offset (const Slic3r::Polygons &polygons) { return offset (polygons, ClipperSafetyOffset); } inline Slic3r::Polygons union_safety_offset (const Slic3r::ExPolygons &expolygons) { return offset (expolygons, ClipperSafetyOffset); } inline Slic3r::ExPolygons union_safety_offset_ex(const Slic3r::Polygons &polygons) { return offset_ex(polygons, ClipperSafetyOffset); } diff --git a/src/libslic3r/Color.cpp b/src/libslic3r/Color.cpp index 6d8daa0..954bef4 100644 --- a/src/libslic3r/Color.cpp +++ b/src/libslic3r/Color.cpp @@ -25,7 +25,7 @@ static void RGBtoHSV(float r, float g, float b, float& h, float& s, float& v) h = 60.0f * (std::fmod(((g - b) / delta), 6.0f)); else if (max_comp == g) h = 60.0f * (((b - r) / delta) + 2.0f); - else if (max_comp == b) + else // max_comp == b h = 60.0f * (((r - g) / delta) + 4.0f); s = (max_comp > 0.0f) ? delta / max_comp : 0.0f; diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index fa00924..99a1c9d 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -17,12 +17,14 @@ #include #include #include +#include #include #include #include #include #include +#include //FIXME for GCodeFlavor and gcfMarlin (for forward-compatibility conversion) // This is not nice, likely it would be better to pass the ConfigSubstitutionContext to handle_legacy(). #include "PrintConfig.hpp" @@ -311,12 +313,9 @@ void ConfigDef::finalize() if (def.type == coEnum) { assert(def.enum_def); assert(def.enum_def->is_valid_closed_enum()); - assert(def.gui_type != ConfigOptionDef::GUIType::i_enum_open && - def.gui_type != ConfigOptionDef::GUIType::f_enum_open && - def.gui_type != ConfigOptionDef::GUIType::select_open); + assert(! def.is_gui_type_enum_open()); def.enum_def->finalize_closed_enum(); - } else if (def.gui_type == ConfigOptionDef::GUIType::i_enum_open || def.gui_type == ConfigOptionDef::GUIType::f_enum_open || - def.gui_type == ConfigOptionDef::GUIType::select_open) { + } else if (def.is_gui_type_enum_open()) { assert(def.enum_def); assert(def.enum_def->is_valid_open_enum()); assert(def.gui_type != ConfigOptionDef::GUIType::i_enum_open || def.type == coInt || def.type == coInts); @@ -720,11 +719,37 @@ void ConfigBase::setenv_() const } } -ConfigSubstitutions ConfigBase::load(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) +ConfigSubstitutions ConfigBase::load(const std::string& filename, ForwardCompatibilitySubstitutionRule compatibility_rule) { - return is_gcode_file(file) ? - this->load_from_gcode_file(file, compatibility_rule) : - this->load_from_ini(file, compatibility_rule); + enum class EFileType + { + Ini, + AsciiGCode, + BinaryGCode + }; + + EFileType file_type; + + if (is_gcode_file(filename)) { + FILE* file = boost::nowide::fopen(filename.c_str(), "rb"); + if (file == nullptr) + throw Slic3r::RuntimeError(format("Error opening file %1%", filename)); + + std::vector cs_buffer(65536); + using namespace bgcode::core; + file_type = (is_valid_binary_gcode(*file, true, cs_buffer.data(), cs_buffer.size()) == EResult::Success) ? EFileType::BinaryGCode : EFileType::AsciiGCode; + fclose(file); + } + else + file_type = EFileType::Ini; + + switch (file_type) +{ + case EFileType::Ini: { return this->load_from_ini(filename, compatibility_rule); } + case EFileType::AsciiGCode: { return this->load_from_gcode_file(filename, compatibility_rule);} + case EFileType::BinaryGCode: { return this->load_from_binary_gcode_file(filename, compatibility_rule);} + default: { throw Slic3r::RuntimeError(format("Invalid file %1%", filename)); } + } } ConfigSubstitutions ConfigBase::load_from_ini(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) @@ -858,6 +883,7 @@ size_t ConfigBase::load_from_gcode_string_legacy(ConfigBase& config, const char* end = start; } + config.handle_legacy_composite(); return num_key_value_pairs; } @@ -928,10 +954,10 @@ private: }; // Load the config keys from the tail of a G-code file. -ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule) +ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &filename, ForwardCompatibilitySubstitutionRule compatibility_rule) { // Read a 64k block from the end of the G-code. - boost::nowide::ifstream ifs(file, std::ifstream::binary); + boost::nowide::ifstream ifs(filename, std::ifstream::binary); // Look for Slic3r or QIDISlicer header. // Look for the header across the whole file as the G-code may have been extended at the start by a post-processing script or the user. bool has_delimiters = false; @@ -986,7 +1012,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo break; } if (! end_found) - throw Slic3r::RuntimeError(format("Configuration block closing tag \"; qidislicer_config = end\" not found when reading %1%", file)); + throw Slic3r::RuntimeError(format("Configuration block closing tag \"; qidislicer_config = end\" not found when reading %1%", filename)); std::string key, value; while (reader.getline(line)) { if (line == "; qidislicer_config = begin") { @@ -1009,7 +1035,8 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo } } if (! begin_found) - throw Slic3r::RuntimeError(format("Configuration block opening tag \"; qidislicer_config = begin\" not found when reading %1%", file)); + throw Slic3r::RuntimeError( + format("Configuration block opening tag \"; qidislicer_config = begin\" not found when reading %1%", filename)); } else { @@ -1026,7 +1053,55 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo } if (key_value_pairs < 80) - throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs)); + throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", filename, key_value_pairs)); + + // Do legacy conversion on a completely loaded dictionary. + // Perform composite conversions, for example merging multiple keys into one key. + this->handle_legacy_composite(); + return std::move(substitutions_ctxt.substitutions); +} + +ConfigSubstitutions ConfigBase::load_from_binary_gcode_file(const std::string& filename, ForwardCompatibilitySubstitutionRule compatibility_rule) +{ + ConfigSubstitutionContext substitutions_ctxt(compatibility_rule); + + FilePtr file{ boost::nowide::fopen(filename.c_str(), "rb") }; + if (file.f == nullptr) + throw Slic3r::RuntimeError(format("Error opening file %1%", filename)); + + using namespace bgcode::core; + using namespace bgcode::binarize; + std::vector cs_buffer(65536); + EResult res = is_valid_binary_gcode(*file.f, true, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("File %1% does not contain a valid binary gcode\nError: %2%", filename, + std::string(translate_result(res)))); + + FileHeader file_header; + res = read_header(*file.f, file_header, nullptr); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error while reading file %1%: %2%", filename, std::string(translate_result(res)))); + + // searches for config block + BlockHeader block_header; + res = read_next_block_header(*file.f, file_header, block_header, EBlockType::SlicerMetadata, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error while reading file %1%: %2%", filename, std::string(translate_result(res)))); + if ((EBlockType)block_header.type != EBlockType::SlicerMetadata) + throw Slic3r::RuntimeError(format("Unable to find slicer metadata block in file %1%", filename)); + SlicerMetadataBlock slicer_metadata_block; + res = slicer_metadata_block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error while reading file %1%: %2%", filename, std::string(translate_result(res)))); + + // extracts data from block + for (const auto& [key, value] : slicer_metadata_block.raw_data) { + this->set_deserialize(key, value, substitutions_ctxt); + } + + // Do legacy conversion on a completely loaded dictionary. + // Perform composite conversions, for example merging multiple keys into one key. + this->handle_legacy_composite(); return std::move(substitutions_ctxt.substitutions); } diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index ca0ef17..52b5438 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -1815,6 +1815,8 @@ public: // Close parameter, string value could be one of the list values. select_close, }; + static bool is_gui_type_enum_open(const GUIType gui_type) + { return gui_type == ConfigOptionDef::GUIType::i_enum_open || gui_type == ConfigOptionDef::GUIType::f_enum_open || gui_type == ConfigOptionDef::GUIType::select_open; } // Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map. t_config_option_key opt_key; @@ -1832,6 +1834,7 @@ public: // Create a default option to be inserted into a DynamicConfig. ConfigOption* create_default_option() const; + bool is_scalar() const { return (int(this->type) & int(coVectorType)) == 0; } template ConfigOption* load_option_from_archive(Archive &archive) const { if (this->nullable) { switch (this->type) { @@ -1902,6 +1905,7 @@ public: // Special values - "i_enum_open", "f_enum_open" to provide combo box for int or float selection, // "select_open" - to open a selection dialog (currently only a serial port selection). GUIType gui_type { GUIType::undefined }; + bool is_gui_type_enum_open() const { return is_gui_type_enum_open(this->gui_type); } // Usually empty. Otherwise "serialized" or "show_value" // The flags may be combined. // "serialized" - vector valued option is entered in a single edit field. Values are separated by a semicolon. @@ -1965,7 +1969,7 @@ public: void set_enum_values(GUIType gui_type, const std::initializer_list il) { this->enum_def_new(); - assert(gui_type == GUIType::i_enum_open || gui_type == GUIType::f_enum_open || gui_type == GUIType::select_open); + assert(is_gui_type_enum_open(gui_type)); this->gui_type = gui_type; enum_def->set_values(il); } @@ -2077,6 +2081,7 @@ public: out.push_back(kvp.first); return out; } + bool empty() const { return options.empty(); } // Iterate through all of the CLI options and write them to a stream. std::ostream& print_cli_help( @@ -2307,7 +2312,8 @@ public: // Loading a "will be one day a legacy format" of configuration stored into 3MF or AMF. // Accepts the same data as load_from_ini_string(), only with each configuration line possibly prefixed with a semicolon (G-code comment). ConfigSubstitutions load_from_ini_string_commented(std::string &&data, ForwardCompatibilitySubstitutionRule compatibility_rule); - ConfigSubstitutions load_from_gcode_file(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule); + ConfigSubstitutions load_from_gcode_file(const std::string &filename, ForwardCompatibilitySubstitutionRule compatibility_rule); + ConfigSubstitutions load_from_binary_gcode_file(const std::string& filename, ForwardCompatibilitySubstitutionRule compatibility_rule); ConfigSubstitutions load(const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule); void save(const std::string &file) const; diff --git a/src/libslic3r/CustomGCode.cpp b/src/libslic3r/CustomGCode.cpp index 2134377..5aebf8e 100644 --- a/src/libslic3r/CustomGCode.cpp +++ b/src/libslic3r/CustomGCode.cpp @@ -1,7 +1,7 @@ #include "CustomGCode.hpp" #include "Config.hpp" #include "GCode.hpp" -#include "GCodeWriter.hpp" +#include "GCode/GCodeWriter.hpp" namespace Slic3r { diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp index a8db9e4..0eddbcd 100644 --- a/src/libslic3r/CutSurface.cpp +++ b/src/libslic3r/CutSurface.cpp @@ -455,6 +455,7 @@ std::vector select_patches(const ProjectionDistances &best_distances, const SurfacePatches &patches, const ExPolygons &shapes, + const BoundingBox &shapes_bb, const ExPolygonsIndices &s2i, const VCutAOIs &cutAOIs, const CutMeshes &meshes, @@ -601,8 +602,7 @@ SurfaceCut Slic3r::cut_surface(const ExPolygons &shapes, // Use only outline points // for each point select best projection priv::ProjectionDistances best_projection = priv::choose_best_distance(distances, shapes, start, s2i, patches); - std::vector use_patch = priv::select_patches(best_projection, patches, - shapes, s2i,model_cuts, cgal_models, projection); + std::vector use_patch = priv::select_patches(best_projection, patches, shapes, shapes_bb, s2i, model_cuts, cgal_models, projection); SurfaceCut result = merge_patches(patches, use_patch); //*/ @@ -1913,6 +1913,26 @@ uint32_t priv::find_closest_point_index(const Point &p, const std::vector &mask) { SearchData sd = create_search_data(shapes, mask); + if (sd.tree.nodes().size() == 0){ + // no lines in expolygon, check whether exist point to start + double closest_square_distance = INFINITY; + uint32_t closest_id = -1; + for (uint32_t i = 0; i < mask.size(); i++) + if (mask[i]){ + ExPolygonsIndex ei = s2i.cvt(i); + const Point& s_p = ei.is_contour()? + shapes[ei.expolygons_index].contour[ei.point_index]: + shapes[ei.expolygons_index].holes[ei.hole_index()][ei.point_index]; + double square_distance = (p - s_p).cast().squaredNorm(); + if (closest_id >= mask.size() || + closest_square_distance > square_distance) { + closest_id = i; + closest_square_distance = square_distance; + } + } + assert(closest_id < mask.size()); + return closest_id; + } size_t line_idx = std::numeric_limits::max(); Vec2d hit_point; Vec2d p_d = p.cast(); @@ -2218,6 +2238,10 @@ priv::ProjectionDistances priv::choose_best_distance( // Select point from shapes(text contour) which is closest to center (all in 2d) uint32_t unfinished_index = find_closest_point_index(start, shapes, s2i, mask_distances); + assert(unfinished_index < s2i.get_count()); + if (unfinished_index >= s2i.get_count()) + // no point to select + return result; #ifdef DEBUG_OUTPUT_DIR Connections connections; @@ -3196,13 +3220,15 @@ std::vector priv::select_patches(const ProjectionDistances &best_distances const SurfacePatches &patches, const ExPolygons &shapes, + const BoundingBox &shapes_bb, const ExPolygonsIndices &s2i, const VCutAOIs &cutAOIs, const CutMeshes &meshes, const Project &projection) { // extension to cover numerical mistake made by back projection patch from 3d to 2d - const float extend_delta = 5.f / Emboss::SHAPE_SCALE; // [Font points scaled by Emboss::SHAPE_SCALE] + Point s = shapes_bb.size(); + const float extend_delta = (s.x() + s.y())/ float(2 * 100); // vector of patches for shape std::vector> used_shapes_patches(shapes.size()); diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 0144f8a..c4721e5 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -391,6 +391,8 @@ static void distribute_modifiers_from_object(ModelObject* from_obj, const int in for (ModelVolume* vol : from_obj->volumes) if (!vol->is_model_part()) { + if (vol->cut_info.is_connector && !vol->cut_info.is_processed) + continue; auto bb = vol->mesh().transformed_bounding_box(inst_matrix * vol->get_matrix()); // Don't add modifiers which are not intersecting with solid parts if (obj1_bb.intersects(bb)) @@ -421,6 +423,7 @@ static void merge_solid_parts_inside_object(ModelObjectPtrs& objects) if (mv->is_model_part() && !mv->is_cut_connector()) mo->delete_volume(i); } + mo->sort_volumes(true); } } } @@ -460,6 +463,10 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowe // Just add Upper and Lower objects to cut_object_ptrs post_process(upper, lower, cut_object_ptrs); + merge_solid_parts_inside_object(cut_object_ptrs); + + // replace initial objects in model with cut object + finalize(cut_object_ptrs); } else if (volumes.size() > cut_parts_cnt) { // Means that object is cut with connectors @@ -487,16 +494,16 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowe post_process(upper, lower, cut_object_ptrs); // Add Dowel-connectors as separate objects to cut_object_ptrs - if (cut_connectors_obj.size() >= 3) - for (size_t id = 2; id < cut_connectors_obj.size(); id++) - cut_object_ptrs.push_back(cut_connectors_obj[id]); - } // Now merge all model parts together: merge_solid_parts_inside_object(cut_object_ptrs); finalize(cut_object_ptrs); + if (cut_connectors_obj.size() >= 3) + for (size_t id = 2; id < cut_connectors_obj.size(); id++) + m_model.add_object(*cut_connectors_obj[id]); + } return m_model.objects; } diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index 0e2e677..42de50e 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -1,5 +1,7 @@ +#include #include "Emboss.hpp" #include +#include #include #include #include @@ -23,18 +25,56 @@ // to get approx center of normal text line const double ASCENT_CENTER = 1/3.; // 0.5 is above small letter +// every glyph's shape point is divided by SHAPE_SCALE - increase precission of fixed point value +// stored in fonts (to be able represents curve by sequence of lines) +static constexpr double SHAPE_SCALE = 0.001; // SCALING_FACTOR promile is fine enough +static unsigned MAX_HEAL_ITERATION_OF_TEXT = 10; + using namespace Slic3r; using namespace Emboss; using fontinfo_opt = std::optional; -// for try approach to heal shape by Clipper::Closing -//#define HEAL_WITH_CLOSING +// NOTE: approach to heal shape by Clipper::Closing is not working // functionality to remove all spikes from shape +// Potentionaly useable for eliminate spike in layer //#define REMOVE_SPIKES +// function to remove useless islands and holes +// #define REMOVE_SMALL_ISLANDS +#ifdef REMOVE_SMALL_ISLANDS +namespace { void remove_small_islands(ExPolygons &shape, double minimal_area);} +#endif //REMOVE_SMALL_ISLANDS + +//#define VISUALIZE_HEAL +#ifdef VISUALIZE_HEAL +namespace { +// for debug purpose only +// NOTE: check scale when store svg !! +#include "libslic3r/SVG.hpp" // for visualize_heal +static std::string visualize_heal_svg_filepath = "C:/data/temp/heal.svg"; +void visualize_heal(const std::string &svg_filepath, const ExPolygons &expolygons) +{ + Points pts = to_points(expolygons); + BoundingBox bb(pts); + // double svg_scale = SHAPE_SCALE / unscale(1.); + // bb.scale(svg_scale); + SVG svg(svg_filepath, bb); + svg.draw(expolygons); + + Points duplicits = collect_duplicates(pts); + int black_size = std::max(bb.size().x(), bb.size().y()) / 20; + svg.draw(duplicits, "black", black_size); + + Slic3r::IntersectionsLines intersections_f = get_intersections(expolygons); + Points intersections = get_unique_intersections(intersections_f); + svg.draw(intersections, "red", black_size * 1.2); +} +} // namespace +#endif // VISUALIZE_HEAL + // do not expose out of this file stbtt_ data types -namespace priv{ +namespace{ using Polygon = Slic3r::Polygon; bool is_valid(const FontFile &font, unsigned int index); fontinfo_opt load_font_info(const unsigned char *data, unsigned int index = 0); @@ -44,8 +84,6 @@ std::optional get_glyph(const stbtt_fontinfo &font_info, int unicode_lett const Glyph* get_glyph(int unicode, const FontFile &font, const FontProp &font_prop, Glyphs &cache, fontinfo_opt &font_info_opt); -EmbossStyle create_style(std::wstring name, std::wstring path); - // scale and convert float to int coordinate Point to_point(const stbtt__point &point); @@ -53,27 +91,12 @@ Point to_point(const stbtt__point &point); void remove_bad(Polygons &polygons); void remove_bad(ExPolygons &expolygons); -// helpr for heal shape -// Return true when erase otherwise false -bool remove_same_neighbor(Polygon &points); -bool remove_same_neighbor(Polygons &polygons); -bool remove_same_neighbor(ExPolygons &expolygons); - // Try to remove self intersection by subtracting rect 2x2 px -bool remove_self_intersections(ExPolygons &shape, unsigned max_iteration = 10); ExPolygon create_bounding_rect(const ExPolygons &shape); -void remove_small_islands(ExPolygons &shape, double minimal_area); - -// NOTE: expolygons can't contain same_neighbor -Points collect_close_points(const ExPolygons &expolygons, double distance = .6); - // Heal duplicates points and self intersections bool heal_dupl_inter(ExPolygons &shape, unsigned max_iteration); -// for debug purpose -void visualize_heal(const std::string& svg_filepath, const ExPolygons &expolygons); - const Points pts_2x2({Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)}); const Points pts_3x3({Point(-1, -1), Point(1, -1), Point(1, 1), Point(-1, 1)}); @@ -90,17 +113,17 @@ struct SpikeDesc /// /// Size of spike width after cut of the tip, has to be grater than 2.5 /// When spike has same or more pixels with width less than 1 pixel - SpikeDesc(double bevel_size, double pixel_spike_length = 6) - { + SpikeDesc(double bevel_size, double pixel_spike_length = 6): // create min angle given by spike_length // Use it as minimal height of 1 pixel base spike - double angle = 2. * atan2(pixel_spike_length, .5); // [rad] - cos_angle = std::fabs(cos(angle)); + cos_angle(std::fabs(std::cos( + /*angle*/ 2. * std::atan2(pixel_spike_length, .5) + ))), // When remove spike this angle is set. // Value must be grater than min_angle - half_bevel = bevel_size / 2; - } + half_bevel(bevel_size / 2) + {} }; // return TRUE when remove point. It could create polygon with 2 points. @@ -116,9 +139,9 @@ void remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc); void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc); #endif -}; - -bool priv::remove_when_spike(Polygon &polygon, size_t index, const SpikeDesc &spike_desc) { +// spike ... very sharp corner - when not removed cause iteration of heal process +// index ... index of duplicit point in polygon +bool remove_when_spike(Slic3r::Polygon &polygon, size_t index, const SpikeDesc &spike_desc) { std::optional add; bool do_erase = false; @@ -202,9 +225,10 @@ bool priv::remove_when_spike(Polygon &polygon, size_t index, const SpikeDesc &sp return false; } -void priv::remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &duplicates) { - - auto check = [](Polygon &polygon, const Point &d) -> bool { +void remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &duplicates) { + if (duplicates.empty()) + return; + auto check = [](Slic3r::Polygon &polygon, const Point &d) -> bool { double spike_bevel = 1 / SHAPE_SCALE; double spike_length = 5.; const static SpikeDesc sd(spike_bevel, spike_length); @@ -234,14 +258,14 @@ void priv::remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &dup remove_bad(expolygons); } -bool priv::is_valid(const FontFile &font, unsigned int index) { +bool is_valid(const FontFile &font, unsigned int index) { if (font.data == nullptr) return false; if (font.data->empty()) return false; if (index >= font.infos.size()) return false; return true; } -fontinfo_opt priv::load_font_info( +fontinfo_opt load_font_info( const unsigned char *data, unsigned int index) { int font_offset = stbtt_GetFontOffsetForIndex(data, index); @@ -259,14 +283,14 @@ fontinfo_opt priv::load_font_info( return font_info; } -void priv::remove_bad(Polygons &polygons) { +void remove_bad(Polygons &polygons) { polygons.erase( std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.size() < 3; }), polygons.end()); } -void priv::remove_bad(ExPolygons &expolygons) { +void remove_bad(ExPolygons &expolygons) { expolygons.erase( std::remove_if(expolygons.begin(), expolygons.end(), [](const ExPolygon &p) { return p.contour.size() < 3; }), @@ -275,104 +299,7 @@ void priv::remove_bad(ExPolygons &expolygons) { for (ExPolygon &expolygon : expolygons) remove_bad(expolygon.holes); } - -bool priv::remove_same_neighbor(Slic3r::Polygon &polygon) -{ - Points &points = polygon.points; - if (points.empty()) return false; - auto last = std::unique(points.begin(), points.end()); - - // remove first and last neighbor duplication - if (const Point& last_point = *(last - 1); - last_point == points.front()) { - --last; - } - - // no duplicits - if (last == points.end()) return false; - - points.erase(last, points.end()); - return true; -} - -bool priv::remove_same_neighbor(Polygons &polygons) { - if (polygons.empty()) return false; - bool exist = false; - for (Polygon& polygon : polygons) - exist |= remove_same_neighbor(polygon); - // remove empty polygons - polygons.erase( - std::remove_if(polygons.begin(), polygons.end(), - [](const Polygon &p) { return p.points.size() <= 2; }), - polygons.end()); - return exist; -} - -bool priv::remove_same_neighbor(ExPolygons &expolygons) { - if(expolygons.empty()) return false; - bool remove_from_holes = false; - bool remove_from_contour = false; - for (ExPolygon &expoly : expolygons) { - remove_from_contour |= remove_same_neighbor(expoly.contour); - remove_from_holes |= remove_same_neighbor(expoly.holes); - } - // Removing of expolygons without contour - if (remove_from_contour) - expolygons.erase( - std::remove_if(expolygons.begin(), expolygons.end(), - [](const ExPolygon &p) { return p.contour.points.size() <=2; }), - expolygons.end()); - return remove_from_holes || remove_from_contour; -} - -Points priv::collect_close_points(const ExPolygons &expolygons, double distance) { - if (expolygons.empty()) return {}; - if (distance < 0.) return {}; - - // IMPROVE: use int(insted of double) lines and tree - const ExPolygonsIndices ids(expolygons); - const std::vector lines = Slic3r::to_linesf(expolygons, ids.get_count()); - AABBTreeIndirect::Tree<2, double> tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); - // Result close points - Points res; - size_t point_index = 0; - auto collect_close = [&res, &point_index, &lines, &tree, &distance, &ids, &expolygons](const Points &pts) { - for (const Point &p : pts) { - Vec2d p_d = p.cast(); - std::vector close_lines = AABBTreeLines::all_lines_in_radius(lines, tree, p_d, distance); - for (size_t index : close_lines) { - // skip point neighbour lines indices - if (index == point_index) continue; - if (&p != &pts.front()) { - if (index == point_index - 1) continue; - } else if (index == (pts.size()-1)) continue; - - // do not doubled side point of segment - const ExPolygonsIndex id = ids.cvt(index); - const ExPolygon &expoly = expolygons[id.expolygons_index]; - const Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()]; - const Points &poly_pts = poly.points; - const Point &line_a = poly_pts[id.point_index]; - const Point &line_b = (!ids.is_last_point(id)) ? poly_pts[id.point_index + 1] : poly_pts.front(); - assert(line_a == lines[index].a.cast()); - assert(line_b == lines[index].b.cast()); - if (p == line_a || p == line_b) continue; - res.push_back(p); - } - ++point_index; - } - }; - for (const ExPolygon &expoly : expolygons) { - collect_close(expoly.contour.points); - for (const Polygon &hole : expoly.holes) - collect_close(hole.points); - } - if (res.empty()) return {}; - std::sort(res.begin(), res.end()); - // only unique points - res.erase(std::unique(res.begin(), res.end()), res.end()); - return res; -} +} // end namespace bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double distance) { @@ -380,7 +307,7 @@ bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double dist if (distance < 0.) return false; // ExPolygons can't contain same neigbours - priv::remove_same_neighbor(expolygons); + remove_same_neighbor(expolygons); // IMPROVE: use int(insted of double) lines and tree const ExPolygonsIndices ids(expolygons); @@ -477,213 +404,241 @@ bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double dist return true; } -bool priv::remove_self_intersections(ExPolygons &shape, unsigned max_iteration) { - if (shape.empty()) - return true; - - Pointfs intersections_f = intersection_points(shape); - if (intersections_f.empty()) - return true; - - // create loop permanent memory - Polygons holes; - Points intersections; - - while (--max_iteration) { - // convert intersections into Points - assert(intersections.empty()); - intersections.reserve(intersections_f.size()); - std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), - [](const Vec2d &p) { return Point(std::floor(p.x()), std::floor(p.y())); }); - - // intersections should be unique poits - std::sort(intersections.begin(), intersections.end()); - auto it = std::unique(intersections.begin(), intersections.end()); - intersections.erase(it, intersections.end()); - - assert(holes.empty()); - holes.reserve(intersections.size()); - - // Fix self intersection in result by subtracting hole 2x2 - for (const Point &p : intersections) { - Polygon hole(priv::pts_2x2); - hole.translate(p); - holes.push_back(hole); - } - // Union of overlapped holes is not neccessary - // Clipper calculate winding number separately for each input parameter - // if (holes.size() > 1) holes = Slic3r::union_(holes); - shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::Yes); - - // TODO: find where diff ex could create same neighbor - priv::remove_same_neighbor(shape); - - // find new intersections made by diff_ex - intersections_f = intersection_points(shape); - if (intersections_f.empty()) - return true; - else { - // clear permanent vectors - holes.clear(); - intersections.clear(); - } - } - assert(max_iteration == 0); - assert(!intersections_f.empty()); - return false; -} - -ExPolygons Emboss::heal_shape(const Polygons &shape) +HealedExPolygons Emboss::heal_polygons(const Polygons &shape, bool is_non_zero, unsigned int max_iteration) { + const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::PolyFillType fill_type = is_non_zero ? + ClipperLib::pftNonZero : ClipperLib::pftEvenOdd; + // When edit this code check that font 'ALIENATE.TTF' and glyph 'i' still work // fix of self intersections // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm - ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), ClipperLib::pftNonZero); - const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), fill_type); ClipperLib::CleanPolygons(paths, clean_distance); Polygons polygons = to_polygons(paths); - polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.size() < 3; }), polygons.end()); - + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), + [](const Polygon &p) { return p.size() < 3; }), polygons.end()); + + if (polygons.empty()) + return {{}, false}; + // Do not remove all duplicates but do it better way // Overlap all duplicit points by rectangle 3x3 Points duplicits = collect_duplicates(to_points(polygons)); if (!duplicits.empty()) { polygons.reserve(polygons.size() + duplicits.size()); for (const Point &p : duplicits) { - Polygon rect_3x3(priv::pts_3x3); + Polygon rect_3x3(pts_3x3); rect_3x3.translate(p); polygons.push_back(rect_3x3); } } - - // TrueTypeFonts use non zero winding number - // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html - ExPolygons res = Slic3r::union_ex(polygons, ClipperLib::pftNonZero); - heal_shape(res); - return res; + ExPolygons res = Slic3r::union_ex(polygons, fill_type); + bool is_healed = heal_expolygons(res, max_iteration); + return {res, is_healed}; } -#include "libslic3r/SVG.hpp" -void priv::visualize_heal(const std::string &svg_filepath, const ExPolygons &expolygons) { - Points pts = to_points(expolygons); - BoundingBox bb(pts); - //double svg_scale = SHAPE_SCALE / unscale(1.); - // bb.scale(svg_scale); - SVG svg(svg_filepath, bb); - svg.draw(expolygons); - - Points duplicits = collect_duplicates(pts); - svg.draw(duplicits, "black", 7 / SHAPE_SCALE); - Pointfs intersections_f = intersection_points(expolygons); - Points intersections; - intersections.reserve(intersections_f.size()); - std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), - [](const Vec2d &p) { return p.cast(); }); - svg.draw(intersections, "red", 8 / SHAPE_SCALE); -} - -bool Emboss::heal_shape(ExPolygons &shape, unsigned max_iteration) +bool Emboss::heal_expolygons(ExPolygons &shape, unsigned max_iteration) { - return priv::heal_dupl_inter(shape, max_iteration); + return ::heal_dupl_inter(shape, max_iteration); } -#ifndef HEAL_WITH_CLOSING -bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) +namespace { + +Points get_unique_intersections(const Slic3r::IntersectionsLines &intersections) +{ + Points result; + if (intersections.empty()) + return result; + + // convert intersections into Points + result.reserve(intersections.size()); + std::transform(intersections.begin(), intersections.end(), std::back_inserter(result), + [](const Slic3r::IntersectionLines &i) { return Point( + std::floor(i.intersection.x()), + std::floor(i.intersection.y())); + }); + // intersections should be unique poits + std::sort(result.begin(), result.end()); + auto it = std::unique(result.begin(), result.end()); + result.erase(it, result.end()); + return result; +} + +Polygons get_holes_with_points(const Polygons &holes, const Points &points) +{ + Polygons result; + for (const Slic3r::Polygon &hole : holes) + for (const Point &p : points) + for (const Point &h : hole) + if (p == h) { + result.push_back(hole); + break; + } + return result; +} + +/// +/// Fill holes which create duplicits or intersections +/// When healing hole creates trouble in shape again try to heal by an union instead of diff_ex +/// +/// Holes which was substracted from shape previous +/// Current duplicates in shape +/// Current intersections in shape +/// Partialy healed shape[could be modified] +/// True when modify shape otherwise False +bool fill_trouble_holes(const Polygons &holes, const Points &duplicates, const Points &intersections, ExPolygons &shape) +{ + if (holes.empty()) + return false; + if (duplicates.empty() && intersections.empty()) + return false; + + Polygons fill = get_holes_with_points(holes, duplicates); + append(fill, get_holes_with_points(holes, intersections)); + if (fill.empty()) + return false; + + shape = union_ex(shape, fill); + return true; +} + +// extend functionality from Points.cpp --> collect_duplicates +// with address of duplicated points +struct Duplicate { + Point point; + std::vector indices; +}; +using Duplicates = std::vector; +Duplicates collect_duplicit_indices(const ExPolygons &expoly) +{ + Points pts = to_points(expoly); + + // initialize original index locations + std::vector idx(pts.size()); + std::iota(idx.begin(), idx.end(), 0); + std::sort(idx.begin(), idx.end(), + [&pts](uint32_t i1, uint32_t i2) { return pts[i1] < pts[i2]; }); + + Duplicates result; + const Point *prev = &pts[idx.front()]; + for (size_t i = 1; i < idx.size(); ++i) { + uint32_t index = idx[i]; + const Point *act = &pts[index]; + if (*prev == *act) { + // duplicit point + if (!result.empty() && result.back().point == *act) { + // more than 2 points with same coordinate + result.back().indices.push_back(index); + } else { + uint32_t prev_index = idx[i-1]; + result.push_back({*act, {prev_index, index}}); + } + continue; + } + prev = act; + } + return result; +} + +Points get_points(const Duplicates& duplicate_indices) +{ + Points result; + if (duplicate_indices.empty()) + return result; + + // convert intersections into Points + result.reserve(duplicate_indices.size()); + std::transform(duplicate_indices.begin(), duplicate_indices.end(), std::back_inserter(result), + [](const Duplicate &d) { return d.point; }); + return result; +} + +bool heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) { if (shape.empty()) return true; + remove_same_neighbor(shape); // create loop permanent memory Polygons holes; - Points intersections; - while (--max_iteration) { - priv::remove_same_neighbor(shape); - Pointfs intersections_f = intersection_points(shape); - - // convert intersections into Points - assert(intersections.empty()); - intersections.reserve(intersections_f.size()); - std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), - [](const Vec2d &p) { return Point(std::floor(p.x()), std::floor(p.y())); }); - - // intersections should be unique poits - std::sort(intersections.begin(), intersections.end()); - auto it = std::unique(intersections.begin(), intersections.end()); - intersections.erase(it, intersections.end()); - - Points duplicates = collect_duplicates(to_points(shape)); - // duplicates are already uniqua and sorted - + while (--max_iteration) { + Duplicates duplicate_indices = collect_duplicit_indices(shape); + //Points duplicates = collect_duplicates(to_points(shape)); + IntersectionsLines intersections = get_intersections(shape); + // Check whether shape is already healed - if (intersections.empty() && duplicates.empty()) + if (intersections.empty() && duplicate_indices.empty()) return true; - assert(holes.empty()); - holes.reserve(intersections.size() + duplicates.size()); + Points duplicate_points = get_points(duplicate_indices); + Points intersection_points = get_unique_intersections(intersections); - remove_spikes_in_duplicates(shape, duplicates); + if (fill_trouble_holes(holes, duplicate_points, intersection_points, shape)) { + holes.clear(); + continue; + } + + holes.clear(); + holes.reserve(intersections.size() + duplicate_points.size()); + + remove_spikes_in_duplicates(shape, duplicate_points); // Fix self intersection in result by subtracting hole 2x2 - for (const Point &p : intersections) { - Polygon hole(priv::pts_2x2); + for (const Point &p : intersection_points) { + Polygon hole(pts_2x2); hole.translate(p); holes.push_back(hole); } // Fix duplicit points by hole 3x3 around duplicit point - for (const Point &p : duplicates) { - Polygon hole(priv::pts_3x3); + for (const Point &p : duplicate_points) { + Polygon hole(pts_3x3); hole.translate(p); holes.push_back(hole); } - shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::Yes); - - // prepare for next loop - holes.clear(); - intersections.clear(); + shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::No); + // ApplySafetyOffset::Yes is incompatible with function fill_trouble_holes } - //priv::visualize_heal("C:/data/temp/heal.svg", shape); - assert(false); - shape = {priv::create_bounding_rect(shape)}; - return false; -} -#else -bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) -{ - priv::remove_same_neighbor(shape); + // Create partialy healed output + Duplicates duplicates = collect_duplicit_indices(shape); + IntersectionsLines intersections = get_intersections(shape); + if (duplicates.empty() && intersections.empty()){ + // healed in the last loop + return true; + } + + #ifdef VISUALIZE_HEAL + visualize_heal(visualize_heal_svg_filepath, shape); + #endif // VISUALIZE_HEAL - const float delta = 2.f; - const ClipperLib::JoinType joinType = ClipperLib::JoinType::jtRound; + assert(false); // Can not heal this shape + // investigate how to heal better way - // remove double points - while (max_iteration) { - --max_iteration; - - // if(!priv::remove_self_intersections(shape, max_iteration)) break; - shape = Slic3r::union_ex(shape); - shape = Slic3r::closing_ex(shape, delta, joinType); - - // double minimal_area = 1000; - // priv::remove_small_islands(shape, minimal_area); - - // check that duplicates and intersections do NOT exists - Points duplicits = collect_duplicates(to_points(shape)); - Pointfs intersections_f = intersection_points(shape); - if (duplicits.empty() && intersections_f.empty()) - return true; + ExPolygonsIndices ei(shape); + std::vector is_healed(shape.size(), {true}); + for (const Duplicate &duplicate : duplicates){ + for (uint32_t i : duplicate.indices) + is_healed[ei.cvt(i).expolygons_index] = false; + } + for (const IntersectionLines &intersection : intersections) { + is_healed[ei.cvt(intersection.line_index1).expolygons_index] = false; + is_healed[ei.cvt(intersection.line_index2).expolygons_index] = false; } - // priv::visualize_heal("C:/data/temp/heal.svg", shape); - assert(false); - shape = {priv::create_bounding_rect(shape)}; + for (size_t shape_index = 0; shape_index < shape.size(); shape_index++) { + if (!is_healed[shape_index]) { + // exchange non healed expoly with bb rect + ExPolygon &expoly = shape[shape_index]; + expoly = create_bounding_rect({expoly}); + } + } return false; } -#endif // !HEAL_WITH_CLOSING -ExPolygon priv::create_bounding_rect(const ExPolygons &shape) { +ExPolygon create_bounding_rect(const ExPolygons &shape) { BoundingBox bb = get_extents(shape); Point size = bb.size(); if (size.x() < 10) @@ -707,7 +662,8 @@ ExPolygon priv::create_bounding_rect(const ExPolygons &shape) { return ExPolygon(rect, hole); } -void priv::remove_small_islands(ExPolygons &expolygons, double minimal_area) { +#ifdef REMOVE_SMALL_ISLANDS +void remove_small_islands(ExPolygons &expolygons, double minimal_area) { if (expolygons.empty()) return; @@ -724,8 +680,9 @@ void priv::remove_small_islands(ExPolygons &expolygons, double minimal_area) { holes.erase(it, holes.end()); } } +#endif // REMOVE_SMALL_ISLANDS -std::optional priv::get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness) +std::optional get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness) { int glyph_index = stbtt_FindGlyphIndex(&font_info, unicode_letter); if (glyph_index == 0) { @@ -783,12 +740,18 @@ std::optional priv::get_glyph(const stbtt_fontinfo &font_info, int unicod std::reverse(pts.begin(), pts.end()); glyph_polygons.emplace_back(pts); } - if (!glyph_polygons.empty()) - glyph.shape = Emboss::heal_shape(glyph_polygons); + if (!glyph_polygons.empty()) { + unsigned max_iteration = 10; + // TrueTypeFonts use non zero winding number + // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html + bool is_non_zero = true; + glyph.shape = Emboss::heal_polygons(glyph_polygons, is_non_zero, max_iteration); + } return glyph; } -const Glyph* priv::get_glyph( +const Glyph* get_glyph( int unicode, const FontFile & font, const FontProp & font_prop, @@ -805,7 +768,7 @@ const Glyph* priv::get_glyph( if (!font_info_opt.has_value()) { - font_info_opt = priv::load_font_info(font.data->data(), font_index); + font_info_opt = load_font_info(font.data->data(), font_index); // can load font info? if (!font_info_opt.has_value()) return nullptr; } @@ -815,27 +778,24 @@ const Glyph* priv::get_glyph( // Fix for very small flatness because it create huge amount of points from curve if (flatness < RESOLUTION) flatness = RESOLUTION; - std::optional glyph_opt = - priv::get_glyph(*font_info_opt, unicode, flatness); + std::optional glyph_opt = get_glyph(*font_info_opt, unicode, flatness); // IMPROVE: multiple loadig glyph without data // has definition inside of font? if (!glyph_opt.has_value()) return nullptr; + Glyph &glyph = *glyph_opt; if (font_prop.char_gap.has_value()) - glyph_opt->advance_width += *font_prop.char_gap; + glyph.advance_width += *font_prop.char_gap; // scale glyph size - glyph_opt->advance_width = - static_cast(glyph_opt->advance_width / SHAPE_SCALE); - glyph_opt->left_side_bearing = - static_cast(glyph_opt->left_side_bearing / SHAPE_SCALE); + glyph.advance_width = static_cast(glyph.advance_width / SHAPE_SCALE); + glyph.left_side_bearing = static_cast(glyph.left_side_bearing / SHAPE_SCALE); - if (!glyph_opt->shape.empty()) { + if (!glyph.shape.empty()) { if (font_prop.boldness.has_value()) { - float delta = *font_prop.boldness / SHAPE_SCALE / - font_prop.size_in_mm; - glyph_opt->shape = Slic3r::union_ex(offset_ex(glyph_opt->shape, delta)); + float delta = static_cast(*font_prop.boldness / SHAPE_SCALE / font_prop.size_in_mm); + glyph.shape = Slic3r::union_ex(offset_ex(glyph.shape, delta)); } if (font_prop.skew.has_value()) { double ratio = *font_prop.skew; @@ -843,34 +803,38 @@ const Glyph* priv::get_glyph( for (Slic3r::Point &p : polygon.points) p.x() += static_cast(std::round(p.y() * ratio)); }; - for (ExPolygon &expolygon : glyph_opt->shape) { + for (ExPolygon &expolygon : glyph.shape) { skew(expolygon.contour); for (Polygon &hole : expolygon.holes) skew(hole); } } } - auto it = cache.insert({unicode, std::move(*glyph_opt)}); - assert(it.second); - return &it.first->second; + auto [it, success] = cache.try_emplace(unicode, std::move(glyph)); + assert(success); + return &it->second; } -EmbossStyle priv::create_style(std::wstring name, std::wstring path) { - return { boost::nowide::narrow(name.c_str()), - boost::nowide::narrow(path.c_str()), - EmbossStyle::Type::file_path, FontProp() }; -} - -Point priv::to_point(const stbtt__point &point) { +Point to_point(const stbtt__point &point) { return Point(static_cast(std::round(point.x / SHAPE_SCALE)), static_cast(std::round(point.y / SHAPE_SCALE))); } +} // namespace + #ifdef _WIN32 #include #include #include #include +namespace { +EmbossStyle create_style(const std::wstring& name, const std::wstring& path) { + return { boost::nowide::narrow(name.c_str()), + boost::nowide::narrow(path.c_str()), + EmbossStyle::Type::file_path, FontProp() }; +} +} // namespace + // Get system font file path std::optional Emboss::get_font_path(const std::wstring &font_face_name) { @@ -994,7 +958,7 @@ EmbossStyles Emboss::get_font_list_by_register() { if (pos >= font_name_w.size()) continue; // remove TrueType text from name font_name_w = std::wstring(font_name_w, 0, pos); - font_list.emplace_back(priv::create_style(font_name_w, path_w)); + font_list.emplace_back(create_style(font_name_w, path_w)); } while (result != ERROR_NO_MORE_ITEMS); delete[] font_name; delete[] fileTTF_name; @@ -1029,7 +993,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() { EmbossStyles font_list; for (const std::wstring &font_name : font_names) { - font_list.emplace_back(priv::create_style(font_name, L"")); + font_list.emplace_back(create_style(font_name, L"")); } return font_list; } @@ -1051,7 +1015,7 @@ EmbossStyles Emboss::get_font_list_by_folder() { if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; std::wstring file_name(fd.cFileName); // TODO: find font name instead of filename - result.emplace_back(priv::create_style(file_name, search_dir + file_name)); + result.emplace_back(create_style(file_name, search_dir + file_name)); } while (::FindNextFile(hFind, &fd)); ::FindClose(hFind); } @@ -1085,7 +1049,7 @@ std::unique_ptr Emboss::create_font_file( std::vector infos; infos.reserve(c_size); for (unsigned int i = 0; i < c_size; ++i) { - auto font_info = priv::load_font_info(data->data(), i); + auto font_info = load_font_info(data->data(), i); if (!font_info.has_value()) return nullptr; const stbtt_fontinfo *info = &(*font_info); @@ -1205,16 +1169,16 @@ std::optional Emboss::letter2glyph(const FontFile &font, int letter, float flatness) { - if (!priv::is_valid(font, font_index)) return {}; - auto font_info_opt = priv::load_font_info(font.data->data(), font_index); + if (!is_valid(font, font_index)) return {}; + auto font_info_opt = load_font_info(font.data->data(), font_index); if (!font_info_opt.has_value()) return {}; - return priv::get_glyph(*font_info_opt, letter, flatness); + return get_glyph(*font_info_opt, letter, flatness); } const FontFile::Info &Emboss::get_font_info(const FontFile &font, const FontProp &prop) { unsigned int font_index = prop.collection_number.value_or(0); - assert(priv::is_valid(font, font_index)); + assert(is_valid(font, font_index)); return font.infos[font_index]; } @@ -1245,7 +1209,7 @@ ExPolygons letter2shapes( if (letter == '\t') { // '\t' = 4*space => same as imgui const int count_spaces = 4; - const Glyph *space = priv::get_glyph(int(' '), font, font_prop, cache, font_info_cache); + const Glyph *space = get_glyph(int(' '), font, font_prop, cache, font_info_cache); if (space == nullptr) return {}; cursor.x() += count_spaces * space->advance_width; @@ -1258,7 +1222,7 @@ ExPolygons letter2shapes( auto it = cache.find(unicode); // Create glyph from font file and cache it - const Glyph *glyph_ptr = (it != cache.end()) ? &it->second : priv::get_glyph(unicode, font, font_prop, cache, font_info_cache); + const Glyph *glyph_ptr = (it != cache.end()) ? &it->second : get_glyph(unicode, font, font_prop, cache, font_info_cache); if (glyph_ptr == nullptr) return {}; @@ -1277,20 +1241,62 @@ ExPolygons letter2shapes( const int CANCEL_CHECK = 10; } // namespace -ExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, const char *text, const FontProp &font_prop, const std::function& was_canceled) +namespace { +HealedExPolygons union_with_delta(const ExPolygonsWithIds &shapes, float delta, unsigned max_heal_iteration) +{ + // unify to one expolygons + ExPolygons expolygons; + for (const ExPolygonsWithId &shape : shapes) { + if (shape.expoly.empty()) + continue; + expolygons_append(expolygons, offset_ex(shape.expoly, delta)); + } + ExPolygons result = union_ex(expolygons); + result = offset_ex(result, -delta); + bool is_healed = heal_expolygons(result, max_heal_iteration); + return {result, is_healed}; +} +} // namespace + +ExPolygons Slic3r::union_with_delta(EmbossShape &shape, float delta, unsigned max_heal_iteration) +{ + if (!shape.final_shape.expolygons.empty()) + return shape.final_shape; + + shape.final_shape = ::union_with_delta(shape.shapes_with_ids, delta, max_heal_iteration); + for (const ExPolygonsWithId &e : shape.shapes_with_ids) + if (!e.is_healed) + shape.final_shape.is_healed = false; + return shape.final_shape.expolygons; +} + +void Slic3r::translate(ExPolygonsWithIds &expolygons_with_ids, const Point &p) +{ + for (ExPolygonsWithId &expolygons_with_id : expolygons_with_ids) + translate(expolygons_with_id.expoly, p); +} + +BoundingBox Slic3r::get_extents(const ExPolygonsWithIds &expolygons_with_ids) +{ + BoundingBox bb; + for (const ExPolygonsWithId &expolygons_with_id : expolygons_with_ids) + bb.merge(get_extents(expolygons_with_id.expoly)); + return bb; +} + +void Slic3r::center(ExPolygonsWithIds &e) +{ + BoundingBox bb = get_extents(e); + translate(e, -bb.center()); +} + +HealedExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, const char *text, const FontProp &font_prop, const std::function& was_canceled) { std::wstring text_w = boost::nowide::widen(text); - std::vector vshapes = text2vshapes(font_with_cache, text_w, font_prop, was_canceled); - // unify to one expolygon - ExPolygons result; - for (ExPolygons &shapes : vshapes) { - if (shapes.empty()) - continue; - expolygons_append(result, std::move(shapes)); - } - result = Slic3r::union_ex(result); - heal_shape(result); - return result; + ExPolygonsWithIds vshapes = text2vshapes(font_with_cache, text_w, font_prop, was_canceled); + + float delta = static_cast(1. / SHAPE_SCALE); + return ::union_with_delta(vshapes, delta, MAX_HEAL_ITERATION_OF_TEXT); } namespace { @@ -1302,21 +1308,21 @@ namespace { /// To detect end of lines - to be able horizontal center the line /// Containe Horizontal and vertical alignment /// Needed for scale and font size -void align_shape(std::vector &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font); +void align_shape(ExPolygonsWithIds &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font); } -std::vector Emboss::text2vshapes(FontFileWithCache &font_with_cache, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled){ +ExPolygonsWithIds Emboss::text2vshapes(FontFileWithCache &font_with_cache, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled){ assert(font_with_cache.has_value()); const FontFile &font = *font_with_cache.font_file; unsigned int font_index = font_prop.collection_number.value_or(0); - if (!priv::is_valid(font, font_index)) + if (!is_valid(font, font_index)) return {}; unsigned counter = 0; Point cursor(0, 0); fontinfo_opt font_info_cache; - std::vector result; + ExPolygonsWithIds result; result.reserve(text.size()); for (wchar_t letter : text) { if (++counter == CANCEL_CHECK) { @@ -1324,7 +1330,8 @@ std::vector Emboss::text2vshapes(FontFileWithCache &font_with_cache, if (was_canceled()) return {}; } - result.emplace_back(letter2shapes(letter, cursor, font_with_cache, font_prop, font_info_cache)); + unsigned id = static_cast(letter); + result.push_back({id, letter2shapes(letter, cursor, font_with_cache, font_prop, font_info_cache)}); } align_shape(result, text, font_prop, font); @@ -1365,8 +1372,14 @@ unsigned Emboss::get_count_lines(const std::string &text) return get_count_lines(ws); } -void Emboss::apply_transformation(const FontProp &font_prop, Transform3d &transformation){ - apply_transformation(font_prop.angle, font_prop.distance, transformation); +unsigned Emboss::get_count_lines(const ExPolygonsWithIds &shapes) { + if (shapes.empty()) + return 0; // no glyphs + unsigned result = 1; // one line is minimum + for (const ExPolygonsWithId &shape_id : shapes) + if (shape_id.id == ENTER_UNICODE) + ++result; + return result; } void Emboss::apply_transformation(const std::optional& angle, const std::optional& distance, Transform3d &transformation) { @@ -1383,7 +1396,7 @@ void Emboss::apply_transformation(const std::optional& angle, const std:: bool Emboss::is_italic(const FontFile &font, unsigned int font_index) { if (font_index >= font.infos.size()) return false; - fontinfo_opt font_info_opt = priv::load_font_info(font.data->data(), font_index); + fontinfo_opt font_info_opt = load_font_info(font.data->data(), font_index); if (!font_info_opt.has_value()) return false; stbtt_fontinfo *info = &(*font_info_opt); @@ -1423,14 +1436,14 @@ std::string Emboss::create_range_text(const std::string &text, unsigned int font_index, bool *exist_unknown) { - if (!priv::is_valid(font, font_index)) return {}; + if (!is_valid(font, font_index)) return {}; std::wstring ws = boost::nowide::widen(text); // need remove symbols not contained in font std::sort(ws.begin(), ws.end()); - auto font_info_opt = priv::load_font_info(font.data->data(), 0); + auto font_info_opt = load_font_info(font.data->data(), 0); if (!font_info_opt.has_value()) return {}; const stbtt_fontinfo *font_info = &(*font_info_opt); @@ -1459,7 +1472,7 @@ std::string Emboss::create_range_text(const std::string &text, return boost::nowide::narrow(ws); } -double Emboss::get_shape_scale(const FontProp &fp, const FontFile &ff) +double Emboss::get_text_shape_scale(const FontProp &fp, const FontFile &ff) { const FontFile::Info &info = get_font_info(ff, fp); double scale = fp.size_in_mm / (double) info.unit_per_em; @@ -1467,7 +1480,7 @@ double Emboss::get_shape_scale(const FontProp &fp, const FontFile &ff) return scale * SHAPE_SCALE; } -namespace priv { +namespace { void add_quad(uint32_t i1, uint32_t i2, @@ -1608,7 +1621,7 @@ indexed_triangle_set polygons2model_duplicit( } return result; } -} // namespace priv +} // namespace indexed_triangle_set Emboss::polygons2model(const ExPolygons &shape2d, const IProjection &projection) @@ -1616,16 +1629,13 @@ indexed_triangle_set Emboss::polygons2model(const ExPolygons &shape2d, Points points = to_points(shape2d); Points duplicits = collect_duplicates(points); return (duplicits.empty()) ? - priv::polygons2model_unique(shape2d, projection, points) : - priv::polygons2model_duplicit(shape2d, projection, points, duplicits); + polygons2model_unique(shape2d, projection, points) : + polygons2model_duplicit(shape2d, projection, points, duplicits); } std::pair Emboss::ProjectZ::create_front_back(const Point &p) const { - Vec3d front( - p.x() * SHAPE_SCALE, - p.y() * SHAPE_SCALE, - 0.); + Vec3d front(p.x(), p.y(), 0.); return std::make_pair(front, project(front)); } @@ -1637,8 +1647,7 @@ Vec3d Emboss::ProjectZ::project(const Vec3d &point) const } std::optional Emboss::ProjectZ::unproject(const Vec3d &p, double *depth) const { - if (depth != nullptr) *depth /= SHAPE_SCALE; - return Vec2d(p.x() / SHAPE_SCALE, p.y() / SHAPE_SCALE); + return Vec2d(p.x(), p.y()); } @@ -1668,23 +1677,21 @@ std::optional Emboss::calc_up(const Transform3d &tr, double up_limit) Vec3d normal = tr_linear.col(2); // scaled matrix has base with different size normal.normalize(); - Vec3d suggested = suggest_up(normal); + Vec3d suggested = suggest_up(normal, up_limit); assert(is_approx(suggested.squaredNorm(), 1.)); Vec3d up = tr_linear.col(1); // tr * UnitY() - up.normalize(); - - double dot = suggested.dot(up); - if (dot >= 1. || dot <= -1.) - return {}; // zero angle - + up.normalize(); Matrix3d m; m.row(0) = up; m.row(1) = suggested; m.row(2) = normal; double det = m.determinant(); - - return -atan2(det, dot); + double dot = suggested.dot(up); + double res = -atan2(det, dot); + if (is_approx(res, 0.)) + return {}; + return res; } Transform3d Emboss::create_transformation_onto_surface(const Vec3d &position, @@ -1965,7 +1972,7 @@ int32_t get_align_x_offset(FontProp::HorizontalAlign align, const BoundingBox &s return 0; } -void align_shape(std::vector &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font) +void align_shape(ExPolygonsWithIds &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font) { // Shapes have to match letters in text assert(shapes.size() == text.length()); @@ -1974,22 +1981,22 @@ void align_shape(std::vector &shapes, const std::wstring &text, cons int y_offset = get_align_y_offset(prop.align.second, count_lines, font, prop); // Speed up for left aligned text - //if (prop.align.first == FontProp::HorizontalAlign::left){ - // // already horizontaly aligned - // for (ExPolygons shape : shapes) - // for (ExPolygon &s : shape) - // s.translate(Point(0, y_offset)); - // return; - //} + if (prop.align.first == FontProp::HorizontalAlign::left){ + // already horizontaly aligned + for (ExPolygonsWithId& shape : shapes) + for (ExPolygon &s : shape.expoly) + s.translate(Point(0, y_offset)); + return; + } BoundingBox shape_bb; - for (const ExPolygons& shape: shapes) - shape_bb.merge(get_extents(shape)); + for (const ExPolygonsWithId& shape: shapes) + shape_bb.merge(get_extents(shape.expoly)); auto get_line_bb = [&](size_t j) { BoundingBox line_bb; for (; j < text.length() && text[j] != '\n'; ++j) - line_bb.merge(get_extents(shapes[j])); + line_bb.merge(get_extents(shapes[j].expoly)); return line_bb; }; @@ -2003,7 +2010,7 @@ void align_shape(std::vector &shapes, const std::wstring &text, cons offset.x() = get_align_x_offset(prop.align.first, shape_bb, get_line_bb(i + 1)); continue; } - ExPolygons &shape = shapes[i]; + ExPolygons &shape = shapes[i].expoly; for (ExPolygon &s : shape) s.translate(offset); } @@ -2012,13 +2019,13 @@ void align_shape(std::vector &shapes, const std::wstring &text, cons double Emboss::get_align_y_offset_in_mm(FontProp::VerticalAlign align, unsigned count_lines, const FontFile &ff, const FontProp &fp){ float offset_in_font_point = get_align_y_offset(align, count_lines, ff, fp); - double scale = get_shape_scale(fp, ff); + double scale = get_text_shape_scale(fp, ff); return scale * offset_in_font_point; } #ifdef REMOVE_SPIKES #include -void priv::remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) +void remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) { enum class Type { add, // Move with point B on A-side and add new point on C-side @@ -2155,14 +2162,14 @@ void priv::remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) } } -void priv::remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc) +void remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc) { for (Polygon &polygon : polygons) remove_spikes(polygon, spike_desc); remove_bad(polygons); } -void priv::remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc) +void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc) { for (ExPolygon &expolygon : expolygons) { remove_spikes(expolygon.contour, spike_desc); @@ -2171,4 +2178,4 @@ void priv::remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc) remove_bad(expolygons); } -#endif // REMOVE_SPIKES \ No newline at end of file +#endif // REMOVE_SPIKES diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp index b6cdcab..9214860 100644 --- a/src/libslic3r/Emboss.hpp +++ b/src/libslic3r/Emboss.hpp @@ -8,6 +8,7 @@ #include // indexed_triangle_set #include "Polygon.hpp" #include "ExPolygon.hpp" +#include "EmbossShape.hpp" // ExPolygonsWithIds #include "BoundingBox.hpp" #include "TextConfiguration.hpp" @@ -21,7 +22,8 @@ namespace Emboss { // every glyph's shape point is divided by SHAPE_SCALE - increase precission of fixed point value // stored in fonts (to be able represents curve by sequence of lines) - static constexpr double SHAPE_SCALE = 0.001; // SCALING_FACTOR promile is fine enough + static const float UNION_DELTA = 50.0f; // [approx in nano meters depends on volume scale] + static const unsigned UNION_MAX_ITERATIN = 10; // [count] /// /// Collect fonts registred inside OS @@ -152,12 +154,14 @@ namespace Emboss /// User defined property of the font /// Way to interupt processing /// Inner polygon cw(outer ccw) - ExPolygons text2shapes (FontFileWithCache &font, const char *text, const FontProp &font_prop, const std::function &was_canceled = []() {return false;}); - std::vector text2vshapes(FontFileWithCache &font, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled = []() {return false;}); + HealedExPolygons text2shapes (FontFileWithCache &font, const char *text, const FontProp &font_prop, const std::function &was_canceled = []() {return false;}); + ExPolygonsWithIds text2vshapes(FontFileWithCache &font, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled = []() {return false;}); + const unsigned ENTER_UNICODE = static_cast('\n'); /// Sum of character '\n' unsigned get_count_lines(const std::wstring &ws); unsigned get_count_lines(const std::string &text); + unsigned get_count_lines(const ExPolygonsWithIds &shape); /// /// Fix duplicit points and self intersections in polygons. @@ -165,7 +169,7 @@ namespace Emboss /// /// Define wanted precision of shape after heal /// Healed shapes - ExPolygons heal_shape(const Polygons &shape); + HealedExPolygons heal_polygons(const Polygons &shape, bool is_non_zero = true, unsigned max_iteration = 10); /// /// NOTE: call Slic3r::union_ex before this call @@ -179,7 +183,7 @@ namespace Emboss /// Heal could create another issue, /// After healing it is checked again until shape is good or maximal count of iteration /// True when shapes is good otherwise False - bool heal_shape(ExPolygons &shape, unsigned max_iteration = 10); + bool heal_expolygons(ExPolygons &shape, unsigned max_iteration = 10); /// /// Divide line segments in place near to point @@ -198,7 +202,6 @@ namespace Emboss /// Z-move as surface distance(FontProp::distance) /// Z-rotation as angle to Y axis(FontProp::angle) /// In / Out transformation to modify by property - void apply_transformation(const FontProp &font_prop, Transform3d &transformation); void apply_transformation(const std::optional &angle, const std::optional &distance, Transform3d &transformation); /// @@ -226,7 +229,7 @@ namespace Emboss /// Property of font /// Font data /// Conversion to mm - double get_shape_scale(const FontProp &fp, const FontFile &ff); + double get_text_shape_scale(const FontProp &fp, const FontFile &ff); /// /// getter of font info by collection defined in prop @@ -334,7 +337,7 @@ namespace Emboss class ProjectZ : public IProjection { public: - ProjectZ(double depth) : m_depth(depth) {} + explicit ProjectZ(double depth) : m_depth(depth) {} // Inherited via IProject std::pair create_front_back(const Point &p) const override; Vec3d project(const Vec3d &point) const override; @@ -458,5 +461,11 @@ namespace Emboss std::vector calculate_angles(int32_t distance, const PolygonPoints& polygon_points, const Polygon &polygon); } // namespace Emboss +void translate(ExPolygonsWithIds &e, const Point &p); +BoundingBox get_extents(const ExPolygonsWithIds &e); +void center(ExPolygonsWithIds &e); +// delta .. safe offset before union (use as boolean close) +// NOTE: remove unprintable spaces between neighbor curves (made by linearization of curve) +ExPolygons union_with_delta(EmbossShape &shape, float delta, unsigned max_heal_iteration); } // namespace Slic3r #endif // slic3r_Emboss_hpp_ diff --git a/src/libslic3r/EmbossShape.hpp b/src/libslic3r/EmbossShape.hpp new file mode 100644 index 0000000..9094d2a --- /dev/null +++ b/src/libslic3r/EmbossShape.hpp @@ -0,0 +1,143 @@ +#ifndef slic3r_EmbossShape_hpp_ +#define slic3r_EmbossShape_hpp_ + +#include +#include +#include // unique_ptr +#include +#include +#include +#include +#include +#include "Point.hpp" // Transform3d +#include "ExPolygon.hpp" +#include "ExPolygonSerialize.hpp" +#include "nanosvg/nanosvg.h" // NSVGimage + +namespace Slic3r { + +struct EmbossProjection{ + // Emboss depth, Size in local Z direction + double depth = 1.; // [in loacal mm] + // NOTE: User should see and modify mainly world size not local + + // Flag that result volume use surface cutted from source objects + bool use_surface = false; + + bool operator==(const EmbossProjection &other) const { + return depth == other.depth && use_surface == other.use_surface; + } + + // undo / redo stack recovery + template void serialize(Archive &ar) { ar(depth, use_surface); } +}; + +// Extend expolygons with information whether it was successfull healed +struct HealedExPolygons{ + ExPolygons expolygons; + bool is_healed; + operator ExPolygons&() { return expolygons; } +}; + +// Help structure to identify expolygons grups +// e.g. emboss -> per glyph -> identify character +struct ExPolygonsWithId +{ + // Identificator for shape + // In text it separate letters and the name is unicode value of letter + // Is svg it is id of path + unsigned id; + + // shape defined by integer point contain only lines + // Curves are converted to sequence of lines + ExPolygons expoly; + + // flag whether expolygons are fully healed(without duplication) + bool is_healed = true; +}; +using ExPolygonsWithIds = std::vector; + +/// +/// Contain plane shape information to be able emboss it and edit it +/// +struct EmbossShape +{ + // shapes to to emboss separately over surface + ExPolygonsWithIds shapes_with_ids; + + // Only cache for final shape + // It is calculated from ExPolygonsWithIds + // Flag is_healed --> whether union of shapes is healed + // Healed mean without selfintersection and point duplication + HealedExPolygons final_shape; + + // scale of shape, multiplier to get 3d point in mm from integer shape + double scale = SCALING_FACTOR; + + // Define how to emboss shape + EmbossProjection projection; + + // !!! Volume stored in .3mf has transformed vertices. + // (baked transformation into vertices position) + // Only place for fill this is when load from .3mf + // This is correction for volume transformation + // Stored_Transform3d * fix_3mf_tr = Transform3d_before_store_to_3mf + std::optional fix_3mf_tr; + + struct SvgFile { + // File(.svg) path on local computer + // When empty can't reload from disk + std::string path; + + // File path into .3mf(.zip) + // When empty svg is not stored into .3mf file yet. + // and will create dialog to delete private data on save. + std::string path_in_3mf; + + // Loaded svg file data. + // !!! It is not serialized on undo/redo stack + std::shared_ptr image = nullptr; + + // Loaded string data from file + std::shared_ptr file_data = nullptr; + + template void save(Archive &ar) const { + // Note: image is only cache it is not neccessary to store + + // Store file data as plain string + assert(file_data != nullptr); + ar(path, path_in_3mf, (file_data != nullptr) ? *file_data : std::string("")); + } + template void load(Archive &ar) { + // for restore shared pointer on file data + std::string file_data_str; + ar(path, path_in_3mf, file_data_str); + if (!file_data_str.empty()) + file_data = std::make_unique(file_data_str); + } + }; + // When embossing shape is made by svg file this is source data + std::optional svg_file; + + // undo / redo stack recovery + template void save(Archive &ar) const + { + // final_shape is not neccessary to store - it is only cache + ar(shapes_with_ids, final_shape, scale, projection, svg_file); + cereal::save(ar, fix_3mf_tr); + } + template void load(Archive &ar) + { + ar(shapes_with_ids, final_shape, scale, projection, svg_file); + cereal::load(ar, fix_3mf_tr); + } +}; +} // namespace Slic3r + +// Serialization through the Cereal library +namespace cereal { +template void serialize(Archive &ar, Slic3r::ExPolygonsWithId &o) { ar(o.id, o.expoly, o.is_healed); } +template void serialize(Archive &ar, Slic3r::HealedExPolygons &o) { ar(o.expolygons, o.is_healed); } +}; // namespace cereal + +#endif // slic3r_EmbossShape_hpp_ diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index 19489bd..e4c17dd 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -457,6 +457,23 @@ bool has_duplicate_points(const ExPolygons &expolys) #endif } +bool remove_same_neighbor(ExPolygons &expolygons) +{ + if (expolygons.empty()) + return false; + bool remove_from_holes = false; + bool remove_from_contour = false; + for (ExPolygon &expoly : expolygons) { + remove_from_contour |= remove_same_neighbor(expoly.contour); + remove_from_holes |= remove_same_neighbor(expoly.holes); + } + // Removing of expolygons without contour + if (remove_from_contour) + expolygons.erase(std::remove_if(expolygons.begin(), expolygons.end(), + [](const ExPolygon &p) { return p.contour.points.size() <= 2; }), + expolygons.end()); + return remove_from_holes || remove_from_contour; +} bool remove_sticks(ExPolygon &poly) { return remove_sticks(poly.contour) || remove_sticks(poly.holes); diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 83b2648..04e6bc1 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -372,6 +372,10 @@ inline Points to_points(const ExPolygon &expoly) return out; } +inline void translate(ExPolygons &expolys, const Point &p) { + for (ExPolygon &expoly : expolys) + expoly.translate(p); +} inline void polygons_append(Polygons &dst, const ExPolygon &src) { dst.reserve(dst.size() + src.holes.size() + 1); @@ -461,6 +465,7 @@ std::vector get_extents_vector(const ExPolygons &polygons); bool has_duplicate_points(const ExPolygon &expoly); bool has_duplicate_points(const ExPolygons &expolys); +bool remove_same_neighbor(ExPolygons &expolys); bool remove_sticks(ExPolygon &poly); void keep_largest_contour_only(ExPolygons &polygons); diff --git a/src/libslic3r/ExPolygonSerialize.hpp b/src/libslic3r/ExPolygonSerialize.hpp new file mode 100644 index 0000000..712d470 --- /dev/null +++ b/src/libslic3r/ExPolygonSerialize.hpp @@ -0,0 +1,28 @@ +#ifndef slic3r_ExPolygonSerialize_hpp_ +#define slic3r_ExPolygonSerialize_hpp_ + +#include "ExPolygon.hpp" +#include "Point.hpp" // Cereal serialization of Point +#include +#include + +/// +/// External Cereal serialization of ExPolygons +/// + +// Serialization through the Cereal library +#include +namespace cereal { + +template +void serialize(Archive &archive, Slic3r::Polygon &polygon) { + archive(polygon.points); +} + +template +void serialize(Archive &archive, Slic3r::ExPolygon &expoly) { + archive(expoly.contour, expoly.holes); +} + +} // namespace Slic3r +#endif // slic3r_ExPolygonSerialize_hpp_ diff --git a/src/libslic3r/Extruder.cpp b/src/libslic3r/Extruder.cpp index b1a089d..460be0e 100644 --- a/src/libslic3r/Extruder.cpp +++ b/src/libslic3r/Extruder.cpp @@ -1,5 +1,5 @@ #include "Extruder.hpp" -#include "GCodeWriter.hpp" +#include "GCode/GCodeWriter.hpp" #include "PrintConfig.hpp" namespace Slic3r { @@ -16,6 +16,7 @@ Extruder::Extruder(unsigned int id, GCodeConfig *config) : std::pair Extruder::extrude(double dE) { + assert(! std::isnan(dE)); // in case of relative E distances we always reset to 0 before any output if (m_config->use_relative_e_distances) m_E = 0.; @@ -37,7 +38,8 @@ std::pair Extruder::extrude(double dE) value supplied will overwrite the previous one if any. */ std::pair Extruder::retract(double retract_length, double restart_extra) { - assert(restart_extra >= 0); + assert(! std::isnan(retract_length)); + assert(! std::isnan(restart_extra) && restart_extra >= 0); // in case of relative E distances we always reset to 0 before any output if (m_config->use_relative_e_distances) m_E = 0.; @@ -130,10 +132,6 @@ double Extruder::retract_length() const return m_config->retract_length.get_at(m_id); } -double Extruder::retract_lift() const -{ - return m_config->retract_lift.get_at(m_id); -} int Extruder::retract_speed() const { diff --git a/src/libslic3r/ExtrusionEntity.cpp b/src/libslic3r/ExtrusionEntity.cpp index 8a1a88b..5e69e12 100644 --- a/src/libslic3r/ExtrusionEntity.cpp +++ b/src/libslic3r/ExtrusionEntity.cpp @@ -2,6 +2,7 @@ #include "ExtrusionEntityCollection.hpp" #include "ExPolygon.hpp" #include "ClipperUtils.hpp" +#include "Exception.hpp" #include "Extruder.hpp" #include "Flow.hpp" #include @@ -38,12 +39,12 @@ double ExtrusionPath::length() const void ExtrusionPath::_inflate_collection(const Polylines &polylines, ExtrusionEntityCollection* collection) const { for (const Polyline &polyline : polylines) - collection->entities.emplace_back(new ExtrusionPath(polyline, *this)); + collection->entities.emplace_back(new ExtrusionPath(polyline, this->attributes())); } void ExtrusionPath::polygons_covered_by_width(Polygons &out, const float scaled_epsilon) const { - polygons_append(out, offset(this->polyline, float(scale_(this->width/2)) + scaled_epsilon)); + polygons_append(out, offset(this->polyline, float(scale_(m_attributes.width/2)) + scaled_epsilon)); } void ExtrusionPath::polygons_covered_by_spacing(Polygons &out, const float scaled_epsilon) const @@ -51,8 +52,8 @@ void ExtrusionPath::polygons_covered_by_spacing(Polygons &out, const float scale // Instantiating the Flow class to get the line spacing. // Don't know the nozzle diameter, setting to zero. It shall not matter it shall be optimized out by the compiler. bool bridge = this->role().is_bridge(); - assert(! bridge || this->width == this->height); - auto flow = bridge ? Flow::bridging_flow(this->width, 0.f) : Flow(this->width, this->height, 0.f); + assert(! bridge || m_attributes.width == m_attributes.height); + auto flow = bridge ? Flow::bridging_flow(m_attributes.width, 0.f) : Flow(m_attributes.width, m_attributes.height, 0.f); polygons_append(out, offset(this->polyline, 0.5f * float(flow.scaled_spacing()) + scaled_epsilon)); } @@ -87,7 +88,7 @@ double ExtrusionMultiPath::min_mm3_per_mm() const { double min_mm3_per_mm = std::numeric_limits::max(); for (const ExtrusionPath &path : this->paths) - min_mm3_per_mm = std::min(min_mm3_per_mm, path.mm3_per_mm); + min_mm3_per_mm = std::min(min_mm3_per_mm, path.min_mm3_per_mm()); return min_mm3_per_mm; } @@ -112,21 +113,34 @@ Polyline ExtrusionMultiPath::as_polyline() const return out; } -bool ExtrusionLoop::make_clockwise() +double ExtrusionLoop::area() const { - bool was_ccw = this->polygon().is_counter_clockwise(); - if (was_ccw) this->reverse(); - return was_ccw; -} - -bool ExtrusionLoop::make_counter_clockwise() -{ - bool was_cw = this->polygon().is_clockwise(); - if (was_cw) this->reverse(); - return was_cw; + double a = 0; + for (const ExtrusionPath &path : this->paths) { + assert(path.size() >= 2); + if (path.size() >= 2) { + // Assumming that the last point of one path segment is repeated at the start of the following path segment. + auto it = path.polyline.points.begin(); + Point prev = *it ++; + for (; it != path.polyline.points.end(); ++ it) { + a += cross2(prev.cast(), it->cast()); + prev = *it; + } + } + } + return a * 0.5; } void ExtrusionLoop::reverse() +{ +#if 0 + this->reverse_loop(); +#else + throw Slic3r::LogicError("ExtrusionLoop::reverse() must NOT be called"); +#endif +} + +void ExtrusionLoop::reverse_loop() { for (ExtrusionPath &path : this->paths) path.reverse(); @@ -248,8 +262,8 @@ void ExtrusionLoop::split_at(const Point &point, bool prefer_non_overhang, const // now split path_idx in two parts const ExtrusionPath &path = this->paths[path_idx]; - ExtrusionPath p1(path.role(), path.mm3_per_mm, path.width, path.height); - ExtrusionPath p2(path.role(), path.mm3_per_mm, path.width, path.height); + ExtrusionPath p1(path.attributes()); + ExtrusionPath p2(path.attributes()); path.polyline.split_at(p, &p1.polyline, &p2.polyline); if (this->paths.size() == 1) { @@ -316,7 +330,7 @@ double ExtrusionLoop::min_mm3_per_mm() const { double min_mm3_per_mm = std::numeric_limits::max(); for (const ExtrusionPath &path : this->paths) - min_mm3_per_mm = std::min(min_mm3_per_mm, path.mm3_per_mm); + min_mm3_per_mm = std::min(min_mm3_per_mm, path.min_mm3_per_mm()); return min_mm3_per_mm; } diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp index 277ac78..085cd81 100644 --- a/src/libslic3r/ExtrusionEntity.hpp +++ b/src/libslic3r/ExtrusionEntity.hpp @@ -3,10 +3,12 @@ #include "libslic3r.h" #include "ExtrusionRole.hpp" +#include "Flow.hpp" #include "Polygon.hpp" #include "Polyline.hpp" #include +#include #include #include @@ -55,28 +57,89 @@ public: virtual double total_volume() const = 0; }; -typedef std::vector ExtrusionEntitiesPtr; +using ExtrusionEntitiesPtr = std::vector; +class ExtrusionEntityReference final +{ +public: + ExtrusionEntityReference() = delete; + ExtrusionEntityReference(const ExtrusionEntity &extrusion_entity, bool flipped) : + m_extrusion_entity(&extrusion_entity), m_flipped(flipped) {} + ExtrusionEntityReference operator=(const ExtrusionEntityReference &rhs) + { m_extrusion_entity = rhs.m_extrusion_entity; m_flipped = rhs.m_flipped; return *this; } + + const ExtrusionEntity& extrusion_entity() const { return *m_extrusion_entity; } + template + const Type* cast() const { return dynamic_cast(m_extrusion_entity); } + bool flipped() const { return m_flipped; } + +private: + const ExtrusionEntity *m_extrusion_entity; + bool m_flipped; +}; + +using ExtrusionEntityReferences = std::vector; + +struct ExtrusionFlow +{ + ExtrusionFlow() = default; + ExtrusionFlow(double mm3_per_mm, float width, float height) : + mm3_per_mm{ mm3_per_mm }, width{ width }, height{ height } {} + ExtrusionFlow(const Flow &flow) : + mm3_per_mm(flow.mm3_per_mm()), width(flow.width()), height(flow.height()) {} + + // Volumetric velocity. mm^3 of plastic per mm of linear head motion. Used by the G-code generator. + double mm3_per_mm{ -1. }; + // Width of the extrusion, used for visualization purposes. + float width{ -1.f }; + // Height of the extrusion, used for visualization purposes. + float height{ -1.f }; +}; + +inline bool operator==(const ExtrusionFlow &lhs, const ExtrusionFlow &rhs) +{ + return lhs.mm3_per_mm == rhs.mm3_per_mm && lhs.width == rhs.width && lhs.height == rhs.height; +} + +struct OverhangAttributes { + float start_distance_from_prev_layer; + float end_distance_from_prev_layer; + float proximity_to_curled_lines; //value between 0 and 1 +}; + +struct ExtrusionAttributes : ExtrusionFlow +{ + ExtrusionAttributes() = default; + ExtrusionAttributes(ExtrusionRole role) : role{ role } {} + ExtrusionAttributes(ExtrusionRole role, const Flow &flow) : role{ role }, ExtrusionFlow{ flow } {} + ExtrusionAttributes(ExtrusionRole role, const ExtrusionFlow &flow) : role{ role }, ExtrusionFlow{ flow } {} + + // What is the role / purpose of this extrusion? + ExtrusionRole role{ ExtrusionRole::None }; + // Volumetric velocity. mm^3 of plastic per mm of linear head motion. Used by the G-code generator. + std::optional overhang_attributes; +}; + // Width of the extrusion, used for visualization purposes. +inline bool operator==(const ExtrusionAttributes &lhs, const ExtrusionAttributes &rhs) +{ + return static_cast(lhs) == static_cast(rhs) && + lhs.role == rhs.role; +} + // Height of the extrusion, used for visualization purposes. class ExtrusionPath : public ExtrusionEntity { public: Polyline polyline; - // Volumetric velocity. mm^3 of plastic per mm of linear head motion. Used by the G-code generator. - double mm3_per_mm; - // Width of the extrusion, used for visualization purposes. - float width; - // Height of the extrusion, used for visualization purposes. - float height; - ExtrusionPath(ExtrusionRole role) : mm3_per_mm(-1), width(-1), height(-1), m_role(role) {} - ExtrusionPath(ExtrusionRole role, double mm3_per_mm, float width, float height) : mm3_per_mm(mm3_per_mm), width(width), height(height), m_role(role) {} - ExtrusionPath(const ExtrusionPath& rhs) : polyline(rhs.polyline), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), m_role(rhs.m_role) {} - ExtrusionPath(ExtrusionPath&& rhs) : polyline(std::move(rhs.polyline)), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), m_role(rhs.m_role) {} - ExtrusionPath(const Polyline &polyline, const ExtrusionPath &rhs) : polyline(polyline), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), m_role(rhs.m_role) {} - ExtrusionPath(Polyline &&polyline, const ExtrusionPath &rhs) : polyline(std::move(polyline)), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), m_role(rhs.m_role) {} + ExtrusionPath(ExtrusionRole role) : m_attributes{ role } {} + ExtrusionPath(const ExtrusionAttributes &attributes) : m_attributes(attributes) {} + ExtrusionPath(const ExtrusionPath &rhs) : polyline(rhs.polyline), m_attributes(rhs.m_attributes) {} + ExtrusionPath(ExtrusionPath &&rhs) : polyline(std::move(rhs.polyline)), m_attributes(rhs.m_attributes) {} + ExtrusionPath(const Polyline &polyline, const ExtrusionAttributes &attribs) : polyline(polyline), m_attributes(attribs) {} + ExtrusionPath(Polyline &&polyline, const ExtrusionAttributes &attribs) : polyline(std::move(polyline)), m_attributes(attribs) {} - ExtrusionPath& operator=(const ExtrusionPath& rhs) { m_role = rhs.m_role; this->mm3_per_mm = rhs.mm3_per_mm; this->width = rhs.width; this->height = rhs.height; this->polyline = rhs.polyline; return *this; } - ExtrusionPath& operator=(ExtrusionPath&& rhs) { m_role = rhs.m_role; this->mm3_per_mm = rhs.mm3_per_mm; this->width = rhs.width; this->height = rhs.height; this->polyline = std::move(rhs.polyline); return *this; } + ExtrusionPath& operator=(const ExtrusionPath &rhs) { this->polyline = rhs.polyline; m_attributes = rhs.m_attributes; return *this; } + ExtrusionPath& operator=(ExtrusionPath &&rhs) { this->polyline = std::move(rhs.polyline); m_attributes = rhs.m_attributes; return *this; } ExtrusionEntity* clone() const override { return new ExtrusionPath(*this); } // Create a new object, initialize it with this object using the move semantics. @@ -97,7 +160,14 @@ public: void clip_end(double distance); void simplify(double tolerance); double length() const override; - ExtrusionRole role() const override { return m_role; } + const ExtrusionAttributes& attributes() const { return m_attributes; } + ExtrusionRole role() const override { return m_attributes.role; } + float width() const { return m_attributes.width; } + float height() const { return m_attributes.height; } + double mm3_per_mm() const { return m_attributes.mm3_per_mm; } + // Minimum volumetric velocity of this extrusion entity. Used by the constant nozzle pressure algorithm. + double min_mm3_per_mm() const override { return m_attributes.mm3_per_mm; } + std::optional& overhang_attributes_mutable() { return m_attributes.overhang_attributes; } // Produce a list of 2D polygons covered by the extruded paths, offsetted by the extrusion width. // Increase the offset by scaled_epsilon to achieve an overlap, so a union will produce no gaps. void polygons_covered_by_width(Polygons &out, const float scaled_epsilon) const override; @@ -110,22 +180,23 @@ public: Polygons polygons_covered_by_spacing(const float scaled_epsilon = 0.f) const { Polygons out; this->polygons_covered_by_spacing(out, scaled_epsilon); return out; } // Minimum volumetric velocity of this extrusion entity. Used by the constant nozzle pressure algorithm. - double min_mm3_per_mm() const override { return this->mm3_per_mm; } Polyline as_polyline() const override { return this->polyline; } void collect_polylines(Polylines &dst) const override { if (! this->polyline.empty()) dst.emplace_back(this->polyline); } void collect_points(Points &dst) const override { append(dst, this->polyline.points); } - double total_volume() const override { return mm3_per_mm * unscale(length()); } + double total_volume() const override { return m_attributes.mm3_per_mm * unscale(length()); } private: void _inflate_collection(const Polylines &polylines, ExtrusionEntityCollection* collection) const; - ExtrusionRole m_role; + ExtrusionAttributes m_attributes; }; class ExtrusionPathOriented : public ExtrusionPath { public: - ExtrusionPathOriented(ExtrusionRole role, double mm3_per_mm, float width, float height) : ExtrusionPath(role, mm3_per_mm, width, height) {} + ExtrusionPathOriented(const ExtrusionAttributes &attribs) : ExtrusionPath(attribs) {} + ExtrusionPathOriented(const Polyline &polyline, const ExtrusionAttributes &attribs) : ExtrusionPath(polyline, attribs) {} + ExtrusionPathOriented(Polyline &&polyline, const ExtrusionAttributes &attribs) : ExtrusionPath(std::move(polyline), attribs) {} ExtrusionEntity* clone() const override { return new ExtrusionPathOriented(*this); } // Create a new object, initialize it with this object using the move semantics. ExtrusionEntity* clone_move() override { return new ExtrusionPathOriented(std::move(*this)); } @@ -192,7 +263,8 @@ class ExtrusionLoop : public ExtrusionEntity public: ExtrusionPaths paths; - ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {} + ExtrusionLoop() = default; + ExtrusionLoop(ExtrusionLoopRole role) : m_loop_role(role) {} ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {} ExtrusionLoop(ExtrusionPaths &&paths, ExtrusionLoopRole role = elrDefault) : paths(std::move(paths)), m_loop_role(role) {} ExtrusionLoop(const ExtrusionPath &path, ExtrusionLoopRole role = elrDefault) : m_loop_role(role) @@ -204,9 +276,11 @@ public: ExtrusionEntity* clone() const override{ return new ExtrusionLoop (*this); } // Create a new object, initialize it with this object using the move semantics. ExtrusionEntity* clone_move() override { return new ExtrusionLoop(std::move(*this)); } - bool make_clockwise(); - bool make_counter_clockwise(); + double area() const; + bool is_counter_clockwise() const { return this->area() > 0; } + bool is_clockwise() const { return this->area() < 0; } void reverse() override; + void reverse_loop(); const Point& first_point() const override { return this->paths.front().polyline.points.front(); } const Point& last_point() const override { assert(this->first_point() == this->paths.back().polyline.points.back()); return this->first_point(); } const Point& middle_point() const override { auto& path = this->paths[this->paths.size() / 2]; return path.polyline.points[path.polyline.size() / 2]; } @@ -259,59 +333,51 @@ public: #endif /* NDEBUG */ private: - ExtrusionLoopRole m_loop_role; + ExtrusionLoopRole m_loop_role{ elrDefault }; }; -inline void extrusion_paths_append(ExtrusionPaths &dst, Polylines &polylines, ExtrusionRole role, double mm3_per_mm, float width, float height) +inline void extrusion_paths_append(ExtrusionPaths &dst, Polylines &polylines, const ExtrusionAttributes &attributes) { dst.reserve(dst.size() + polylines.size()); for (Polyline &polyline : polylines) - if (polyline.is_valid()) { - dst.push_back(ExtrusionPath(role, mm3_per_mm, width, height)); - dst.back().polyline = polyline; - } + if (polyline.is_valid()) + dst.emplace_back(polyline, attributes); } -inline void extrusion_paths_append(ExtrusionPaths &dst, Polylines &&polylines, ExtrusionRole role, double mm3_per_mm, float width, float height) +inline void extrusion_paths_append(ExtrusionPaths &dst, Polylines &&polylines, const ExtrusionAttributes &attributes) { dst.reserve(dst.size() + polylines.size()); for (Polyline &polyline : polylines) - if (polyline.is_valid()) { - dst.push_back(ExtrusionPath(role, mm3_per_mm, width, height)); - dst.back().polyline = std::move(polyline); - } + if (polyline.is_valid()) + dst.emplace_back(std::move(polyline), attributes); polylines.clear(); } -inline void extrusion_entities_append_paths(ExtrusionEntitiesPtr &dst, const Polylines &polylines, ExtrusionRole role, double mm3_per_mm, float width, float height, bool can_reverse = true) +inline void extrusion_entities_append_paths(ExtrusionEntitiesPtr &dst, const Polylines &polylines, const ExtrusionAttributes &attributes, bool can_reverse = true) { dst.reserve(dst.size() + polylines.size()); for (const Polyline &polyline : polylines) - if (polyline.is_valid()) { - ExtrusionPath* extrusion_path = can_reverse ? new ExtrusionPath(role, mm3_per_mm, width, height) : new ExtrusionPathOriented(role, mm3_per_mm, width, height); - dst.push_back(extrusion_path); - extrusion_path->polyline = polyline; - } + if (polyline.is_valid()) + dst.emplace_back(can_reverse ? new ExtrusionPath(polyline, attributes) : new ExtrusionPathOriented(polyline, attributes)); } -inline void extrusion_entities_append_paths(ExtrusionEntitiesPtr &dst, Polylines &&polylines, ExtrusionRole role, double mm3_per_mm, float width, float height, bool can_reverse = true) +inline void extrusion_entities_append_paths(ExtrusionEntitiesPtr &dst, Polylines &&polylines, const ExtrusionAttributes &attributes, bool can_reverse = true) { dst.reserve(dst.size() + polylines.size()); for (Polyline &polyline : polylines) - if (polyline.is_valid()) { - ExtrusionPath *extrusion_path = can_reverse ? new ExtrusionPath(role, mm3_per_mm, width, height) : new ExtrusionPathOriented(role, mm3_per_mm, width, height); - dst.push_back(extrusion_path); - extrusion_path->polyline = std::move(polyline); - } + if (polyline.is_valid()) + dst.emplace_back(can_reverse ? + new ExtrusionPath(std::move(polyline), attributes) : + new ExtrusionPathOriented(std::move(polyline), attributes)); polylines.clear(); } -inline void extrusion_entities_append_loops(ExtrusionEntitiesPtr &dst, Polygons &&loops, ExtrusionRole role, double mm3_per_mm, float width, float height) +inline void extrusion_entities_append_loops(ExtrusionEntitiesPtr &dst, Polygons &&loops, const ExtrusionAttributes &attributes) { dst.reserve(dst.size() + loops.size()); for (Polygon &poly : loops) { if (poly.is_valid()) { - ExtrusionPath path(role, mm3_per_mm, width, height); + ExtrusionPath path(attributes); path.polyline.points = std::move(poly.points); path.polyline.points.push_back(path.polyline.points.front()); dst.emplace_back(new ExtrusionLoop(std::move(path))); @@ -320,22 +386,14 @@ inline void extrusion_entities_append_loops(ExtrusionEntitiesPtr &dst, Polygons loops.clear(); } -inline void extrusion_entities_append_loops_and_paths(ExtrusionEntitiesPtr &dst, Polylines &&polylines, ExtrusionRole role, double mm3_per_mm, float width, float height) +inline void extrusion_entities_append_loops_and_paths(ExtrusionEntitiesPtr &dst, Polylines &&polylines, const ExtrusionAttributes &attributes) { dst.reserve(dst.size() + polylines.size()); - for (Polyline &polyline : polylines) { - if (polyline.is_valid()) { - if (polyline.is_closed()) { - ExtrusionPath extrusion_path(role, mm3_per_mm, width, height); - extrusion_path.polyline = std::move(polyline); - dst.emplace_back(new ExtrusionLoop(std::move(extrusion_path))); - } else { - ExtrusionPath *extrusion_path = new ExtrusionPath(role, mm3_per_mm, width, height); - extrusion_path->polyline = std::move(polyline); - dst.emplace_back(extrusion_path); - } - } - } + for (Polyline &polyline : polylines) + if (polyline.is_valid()) + dst.emplace_back(polyline.is_closed() ? + static_cast(new ExtrusionLoop(ExtrusionPath{ std::move(polyline), attributes })) : + static_cast(new ExtrusionPath(std::move(polyline), attributes))); polylines.clear(); } diff --git a/src/libslic3r/ExtrusionEntityCollection.cpp b/src/libslic3r/ExtrusionEntityCollection.cpp index 5516786..67d84fb 100644 --- a/src/libslic3r/ExtrusionEntityCollection.cpp +++ b/src/libslic3r/ExtrusionEntityCollection.cpp @@ -6,6 +6,7 @@ namespace Slic3r { +#if 0 void filter_by_extrusion_role_in_place(ExtrusionEntitiesPtr &extrusion_entities, ExtrusionRole role) { if (role != ExtrusionRole::Mixed) { @@ -17,6 +18,7 @@ void filter_by_extrusion_role_in_place(ExtrusionEntitiesPtr &extrusion_entities, last); } } +#endif ExtrusionEntityCollection::ExtrusionEntityCollection(const ExtrusionPaths &paths) : no_sort(false) @@ -83,17 +85,6 @@ void ExtrusionEntityCollection::remove(size_t i) this->entities.erase(this->entities.begin() + i); } -ExtrusionEntityCollection ExtrusionEntityCollection::chained_path_from(const ExtrusionEntitiesPtr& extrusion_entities, const Point &start_near, ExtrusionRole role) -{ - // Return a filtered copy of the collection. - ExtrusionEntityCollection out; - out.entities = filter_by_extrusion_role(extrusion_entities, role); - // Clone the extrusion entities. - for (auto &ptr : out.entities) - ptr = ptr->clone(); - chain_and_reorder_extrusion_entities(out.entities, &start_near); - return out; -} void ExtrusionEntityCollection::polygons_covered_by_width(Polygons &out, const float scaled_epsilon) const { diff --git a/src/libslic3r/ExtrusionEntityCollection.hpp b/src/libslic3r/ExtrusionEntityCollection.hpp index 676bdd8..3d6ffba 100644 --- a/src/libslic3r/ExtrusionEntityCollection.hpp +++ b/src/libslic3r/ExtrusionEntityCollection.hpp @@ -7,6 +7,7 @@ namespace Slic3r { +#if 0 // Remove those items from extrusion_entities, that do not match role. // Do nothing if role is mixed. // Removed elements are NOT being deleted. @@ -21,6 +22,7 @@ inline ExtrusionEntitiesPtr filter_by_extrusion_role(const ExtrusionEntitiesPtr filter_by_extrusion_role_in_place(out, role); return out; } +#endif class ExtrusionEntityCollection : public ExtrusionEntity { @@ -96,9 +98,6 @@ public: } void replace(size_t i, const ExtrusionEntity &entity); void remove(size_t i); - static ExtrusionEntityCollection chained_path_from(const ExtrusionEntitiesPtr &extrusion_entities, const Point &start_near, ExtrusionRole role = ExtrusionRole::Mixed); - ExtrusionEntityCollection chained_path_from(const Point &start_near, ExtrusionRole role = ExtrusionRole::Mixed) const - { return this->no_sort ? *this : chained_path_from(this->entities, start_near, role); } void reverse() override; const Point& first_point() const override { return this->entities.front()->first_point(); } const Point& last_point() const override { return this->entities.back()->last_point(); } diff --git a/src/libslic3r/ExtrusionRole.hpp b/src/libslic3r/ExtrusionRole.hpp index 986c139..af1d79b 100644 --- a/src/libslic3r/ExtrusionRole.hpp +++ b/src/libslic3r/ExtrusionRole.hpp @@ -82,6 +82,7 @@ struct ExtrusionRole : public ExtrusionRoleModifiers bool is_external_perimeter() const { return this->is_perimeter() && this->is_external(); } bool is_infill() const { return this->ExtrusionRoleModifiers::has(ExtrusionRoleModifier::Infill); } bool is_solid_infill() const { return this->is_infill() && this->ExtrusionRoleModifiers::has(ExtrusionRoleModifier::Solid); } + bool is_sparse_infill() const { return this->is_infill() && ! this->ExtrusionRoleModifiers::has(ExtrusionRoleModifier::Solid); } bool is_external() const { return this->ExtrusionRoleModifiers::has(ExtrusionRoleModifier::External); } bool is_bridge() const { return this->ExtrusionRoleModifiers::has(ExtrusionRoleModifier::Bridge); } @@ -89,6 +90,7 @@ struct ExtrusionRole : public ExtrusionRoleModifiers bool is_support_base() const { return this->is_support() && ! this->is_external(); } bool is_support_interface() const { return this->is_support() && this->is_external(); } bool is_mixed() const { return this->ExtrusionRoleModifiers::has(ExtrusionRoleModifier::Mixed); } + bool is_skirt() const { return this->ExtrusionRoleModifiers::has(ExtrusionRoleModifier::Skirt); } }; // Special flags describing loop diff --git a/src/libslic3r/ExtrusionSimulator.cpp b/src/libslic3r/ExtrusionSimulator.cpp index 6b1f76a..a08b4d8 100644 --- a/src/libslic3r/ExtrusionSimulator.cpp +++ b/src/libslic3r/ExtrusionSimulator.cpp @@ -957,9 +957,9 @@ void ExtrusionSimulator::extrude_to_accumulator(const ExtrusionPath &path, const polyline.reserve(path.polyline.points.size()); float scalex = float(viewport.size().x()) / float(bbox.size().x()); float scaley = float(viewport.size().y()) / float(bbox.size().y()); - float w = scale_(path.width) * scalex; + float w = scale_(path.width()) * scalex; //float h = scale_(path.height) * scalex; - w = scale_(path.mm3_per_mm / path.height) * scalex; + w = scale_(path.mm3_per_mm() / path.height()) * scalex; // printf("scalex: %f, scaley: %f\n", scalex, scaley); // printf("bbox: %d,%d %d,%d\n", bbox.min.x(), bbox.min.y, bbox.max.x(), bbox.max.y); for (Points::const_iterator it = path.polyline.points.begin(); it != path.polyline.points.end(); ++ it) { diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index bec8eef..55518cf 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -578,9 +578,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: flow_width = new_flow.width(); } // Save into layer. - ExtrusionEntityCollection* eec = nullptr; + ExtrusionEntityCollection *eec = new ExtrusionEntityCollection(); auto fill_begin = uint32_t(layerm.fills().size()); - layerm.m_fills.entities.push_back(eec = new ExtrusionEntityCollection()); // Only concentric fills are not sorted. eec->no_sort = f->no_sort(); if (params.use_arachne) { @@ -597,12 +596,18 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: } } + if (!eec->empty()) + layerm.m_fills.entities.push_back(eec); + else + delete eec; thick_polylines.clear(); } else { extrusion_entities_append_paths( eec->entities, std::move(polylines), - surface_fill.params.extrusion_role, - flow_mm3_per_mm, float(flow_width), surface_fill.params.flow.height()); + ExtrusionAttributes{ surface_fill.params.extrusion_role, + ExtrusionFlow{ flow_mm3_per_mm, float(flow_width), surface_fill.params.flow.height() } + }); + layerm.m_fills.entities.push_back(eec); } insert_fills_into_islands(*this, uint32_t(surface_fill.region_id), fill_begin, uint32_t(layerm.fills().size())); } @@ -947,8 +952,9 @@ void Layer::make_ironing() eec->no_sort = true; extrusion_entities_append_paths( eec->entities, std::move(polylines), - ExtrusionRole::Ironing, - flow_mm3_per_mm, extrusion_width, float(extrusion_height)); + ExtrusionAttributes{ ExtrusionRole::Ironing, + ExtrusionFlow{ flow_mm3_per_mm, extrusion_width, float(extrusion_height) } + }); insert_fills_into_islands(*this, ironing_params.region_id, fill_begin, uint32_t(ironing_params.layerm->fills().size())); } } diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index d29bcbe..6600573 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -36,6 +36,10 @@ namespace pt = boost::property_tree; #include "miniz_extension.hpp" #include "TextConfiguration.hpp" +#include "EmbossShape.hpp" +#include "ExPolygonSerialize.hpp" + +#include "NSVGUtils.hpp" #include @@ -157,12 +161,11 @@ static constexpr const char *FONT_DESCRIPTOR_TYPE_ATTR = "font_descriptor_type"; static constexpr const char *CHAR_GAP_ATTR = "char_gap"; static constexpr const char *LINE_GAP_ATTR = "line_gap"; static constexpr const char *LINE_HEIGHT_ATTR = "line_height"; -static constexpr const char *DEPTH_ATTR = "depth"; -static constexpr const char *USE_SURFACE_ATTR = "use_surface"; static constexpr const char *BOLDNESS_ATTR = "boldness"; static constexpr const char *SKEW_ATTR = "skew"; -static constexpr const char *DISTANCE_ATTR = "distance"; -static constexpr const char *ANGLE_ATTR = "angle"; +static constexpr const char *PER_GLYPH_ATTR = "per_glyph"; +static constexpr const char *HORIZONTAL_ALIGN_ATTR = "horizontal"; +static constexpr const char *VERTICAL_ALIGN_ATTR = "vertical"; static constexpr const char *COLLECTION_NUMBER_ATTR = "collection"; static constexpr const char *FONT_FAMILY_ATTR = "family"; @@ -170,6 +173,15 @@ static constexpr const char *FONT_FACE_NAME_ATTR = "face_name"; static constexpr const char *FONT_STYLE_ATTR = "style"; static constexpr const char *FONT_WEIGHT_ATTR = "weight"; +static constexpr const char *SHAPE_TAG = "slic3rpe:shape"; +static constexpr const char *SHAPE_SCALE_ATTR = "scale"; +static constexpr const char *UNHEALED_ATTR = "unhealed"; +static constexpr const char *SVG_FILE_PATH_ATTR = "filepath"; +static constexpr const char *SVG_FILE_PATH_IN_3MF_ATTR = "filepath3mf"; + +// EmbossProjection +static constexpr const char *DEPTH_ATTR = "depth"; +static constexpr const char *USE_SURFACE_ATTR = "use_surface"; const unsigned int VALID_OBJECT_TYPES_COUNT = 1; const char* VALID_OBJECT_TYPES[] = { @@ -416,6 +428,7 @@ namespace Slic3r { MetadataList metadata; RepairedMeshErrors mesh_stats; std::optional text_configuration; + std::optional shape_configuration; VolumeMetadata(unsigned int first_triangle_id, unsigned int last_triangle_id) : first_triangle_id(first_triangle_id) , last_triangle_id(last_triangle_id) @@ -454,6 +467,7 @@ namespace Slic3r { typedef std::map> IdToSlaSupportPointsMap; typedef std::map> IdToSlaDrainHolesMap; + using PathToEmbossShapeFileMap = std::map>; // Version of the 3mf file unsigned int m_version; bool m_check_version; @@ -483,6 +497,7 @@ namespace Slic3r { IdToLayerConfigRangesMap m_layer_config_ranges; IdToSlaSupportPointsMap m_sla_support_points; IdToSlaDrainHolesMap m_sla_drain_holes; + PathToEmbossShapeFileMap m_path_to_emboss_shape_files; std::string m_curr_metadata_name; std::string m_curr_characters; std::string m_name; @@ -509,6 +524,7 @@ namespace Slic3r { } bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); + bool _is_svg_shape_file(const std::string &filename) const; bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); @@ -520,6 +536,7 @@ namespace Slic3r { void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); + void _extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat); // handlers to parse the .model file void _handle_start_model_xml_element(const char* name, const char** attributes); @@ -570,6 +587,7 @@ namespace Slic3r { bool _handle_end_metadata(); bool _handle_start_text_configuration(const char** attributes, unsigned int num_attributes); + bool _handle_start_shape_configuration(const char **attributes, unsigned int num_attributes); bool _create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter); @@ -747,6 +765,9 @@ namespace Slic3r { return false; } } + else if (_is_svg_shape_file(name)) { + _extract_embossed_svg_shape_file(name, archive, stat); + } } } @@ -921,6 +942,9 @@ namespace Slic3r { return true; } + bool _3MF_Importer::_is_svg_shape_file(const std::string &name) const { + return boost::starts_with(name, MODEL_FOLDER) && boost::ends_with(name, ".svg"); + } bool _3MF_Importer::_extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) { if (stat.m_uncomp_size == 0) { @@ -1353,6 +1377,31 @@ namespace Slic3r { } } + void _3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){ + assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end()); + auto file = std::make_unique(stat.m_uncomp_size, '\0'); + mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void *) file->data(), stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading svg shape for emboss"); + return; + } + + // store for case svg is loaded before volume + m_path_to_emboss_shape_files[filename] = std::move(file); + + // find embossed volume, for case svg is loaded after volume + for (const ModelObject* object : m_model->objects) + for (ModelVolume *volume : object->volumes) { + std::optional &es = volume->emboss_shape; + if (!es.has_value()) + continue; + std::optional &svg = es->svg_file; + if (!svg.has_value()) + continue; + if (filename.compare(svg->path_in_3mf) == 0) + svg->file_data = m_path_to_emboss_shape_files[filename]; + } + } bool _3MF_Importer::_extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model) { if (stat.m_uncomp_size == 0) { @@ -1552,6 +1601,8 @@ namespace Slic3r { res = _handle_start_config_volume_mesh(attributes, num_attributes); else if (::strcmp(METADATA_TAG, name) == 0) res = _handle_start_config_metadata(attributes, num_attributes); + else if (::strcmp(SHAPE_TAG, name) == 0) + res = _handle_start_shape_configuration(attributes, num_attributes); else if (::strcmp(TEXT_TAG, name) == 0) res = _handle_start_text_configuration(attributes, num_attributes); @@ -1905,7 +1956,14 @@ namespace Slic3r { public: TextConfigurationSerialization() = delete; - static const boost::bimap type_to_name; + using TypeToName = boost::bimap; + static const TypeToName type_to_name; + + using HorizontalAlignToName = boost::bimap; + static const HorizontalAlignToName horizontal_align_to_name; + + using VerticalAlignToName = boost::bimap; + static const VerticalAlignToName vertical_align_to_name; static EmbossStyle::Type get_type(std::string_view type) { const auto& to_type = TextConfigurationSerialization::type_to_name.right; @@ -1924,8 +1982,8 @@ namespace Slic3r { } static void to_xml(std::stringstream &stream, const TextConfiguration &tc); - static void create_fix_and_store(std::stringstream &stream, TextConfiguration tc, const ModelVolume& volume); static std::optional read(const char **attributes, unsigned int num_attributes); + static EmbossShape read_old(const char **attributes, unsigned int num_attributes); }; bool _3MF_Importer::_handle_start_text_configuration(const char **attributes, unsigned int num_attributes) @@ -1941,7 +1999,54 @@ namespace Slic3r { } ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); volume.text_configuration = TextConfigurationSerialization::read(attributes, num_attributes); - return volume.text_configuration.has_value(); + if (!volume.text_configuration.has_value()) + return false; + + // Is 3mf version with shapes? + if (volume.shape_configuration.has_value()) + return true; + + // Back compatibility for 3mf version without shapes + volume.shape_configuration = TextConfigurationSerialization::read_old(attributes, num_attributes); + return true; + } + + // Definition of read/write method for EmbossShape + static void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume &volume, mz_zip_archive &archive); + static std::optional read_emboss_shape(const char **attributes, unsigned int num_attributes); + + bool _3MF_Importer::_handle_start_shape_configuration(const char **attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Can not assign volume mesh to a valid object"); + return false; + } + auto &volumes = object->second.volumes; + if (volumes.empty()) { + add_error("Can not assign mesh to a valid volume"); + return false; + } + ObjectMetadata::VolumeMetadata &volume = volumes.back(); + volume.shape_configuration = read_emboss_shape(attributes, num_attributes); + if (!volume.shape_configuration.has_value()) + return false; + + // Fill svg file content into shape_configuration + std::optional &svg = volume.shape_configuration->svg_file; + if (!svg.has_value()) + return true; // do not contain svg file + + const std::string &path = svg->path_in_3mf; + if (path.empty()) + return true; // do not contain svg file + + auto it = m_path_to_emboss_shape_files.find(path); + if (it == m_path_to_emboss_shape_files.end()) + return true; // svg file is not loaded yet + + svg->file_data = it->second; + return true; } bool _3MF_Importer::_create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter) @@ -2224,8 +2329,9 @@ namespace Slic3r { volume->supported_facets.shrink_to_fit(); volume->seam_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit(); - auto &tc = volume_data.text_configuration; - if (tc.has_value()) { + if (auto &es = volume_data.shape_configuration; es.has_value()) + volume->emboss_shape = std::move(es); + if (auto &tc = volume_data.text_configuration; tc.has_value()) volume->text_configuration = std::move(tc); //// Transformation before store to 3mf @@ -2239,7 +2345,6 @@ namespace Slic3r { //volume->text_configuration->fix_3mf_tr = // pre_trmat.inverse() * // volume->get_transformation().get_matrix(); - } // apply the remaining volume's metadata for (const Metadata& metadata : volume_data.metadata) { @@ -2371,6 +2476,7 @@ namespace Slic3r { bool save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64); static void add_transformation(std::stringstream &stream, const Transform3d &tr); private: + void _publish(Model &model); bool _save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data); bool _add_content_types_file_to_archive(mz_zip_archive& archive); bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data); @@ -3315,10 +3421,13 @@ namespace Slic3r { stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; } + if (const std::optional &es = volume->emboss_shape; + es.has_value()) + to_xml(stream, *es, *volume, archive); // stores volume's text data - const auto &tc = volume->text_configuration; - if (tc.has_value()) - TextConfigurationSerialization::create_fix_and_store(stream, *tc, *volume); + if (const std::optional &tc = volume->text_configuration; + tc.has_value()) + TextConfigurationSerialization::to_xml(stream, *tc); // stores mesh's statistics const RepairedMeshErrors& stats = volume->mesh().stats().repaired_errors; @@ -3489,27 +3598,71 @@ bool store_3mf(const char* path, Model* model, const DynamicPrintConfig* config, return res; } +namespace{ + +// Conversion with bidirectional map +// F .. first, S .. second +template +F bimap_cvt(const boost::bimap &bmap, S s, const F & def_value) { + const auto &map = bmap.right; + auto found_item = map.find(s); + + // only for back and forward compatibility + assert(found_item != map.end()); + if (found_item == map.end()) + return def_value; + + return found_item->second; +} + +template +S bimap_cvt(const boost::bimap &bmap, F f, const S &def_value) +{ + const auto &map = bmap.left; + auto found_item = map.find(f); + + // only for back and forward compatibility + assert(found_item != map.end()); + if (found_item == map.end()) + return def_value; + + return found_item->second; +} + +} // namespace /// /// TextConfiguration serialization /// -using TypeToName = boost::bimap; -const TypeToName TextConfigurationSerialization::type_to_name = +const TextConfigurationSerialization::TypeToName TextConfigurationSerialization::type_to_name = boost::assign::list_of (EmbossStyle::Type::file_path, "file_name") (EmbossStyle::Type::wx_win_font_descr, "wxFontDescriptor_Windows") (EmbossStyle::Type::wx_lin_font_descr, "wxFontDescriptor_Linux") (EmbossStyle::Type::wx_mac_font_descr, "wxFontDescriptor_MacOsX"); +const TextConfigurationSerialization::HorizontalAlignToName TextConfigurationSerialization::horizontal_align_to_name = + boost::assign::list_of + (FontProp::HorizontalAlign::left, "left") + (FontProp::HorizontalAlign::center, "center") + (FontProp::HorizontalAlign::right, "right"); + +const TextConfigurationSerialization::VerticalAlignToName TextConfigurationSerialization::vertical_align_to_name = + boost::assign::list_of + (FontProp::VerticalAlign::top, "top") + (FontProp::VerticalAlign::center, "middle") + (FontProp::VerticalAlign::bottom, "bottom"); void TextConfigurationSerialization::to_xml(std::stringstream &stream, const TextConfiguration &tc) { stream << " <" << TEXT_TAG << " "; stream << TEXT_DATA_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(tc.text) << "\" "; // font item - const EmbossStyle &fi = tc.style; - stream << STYLE_NAME_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(fi.name) << "\" "; - stream << FONT_DESCRIPTOR_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(fi.path) << "\" "; - stream << FONT_DESCRIPTOR_TYPE_ATTR << "=\"" << TextConfigurationSerialization::get_name(fi.type) << "\" "; + const EmbossStyle &style = tc.style; + stream << STYLE_NAME_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(style.name) << "\" "; + stream << FONT_DESCRIPTOR_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(style.path) << "\" "; + constexpr std::string_view dafault_type{"undefined"}; + std::string_view style_type = bimap_cvt(type_to_name, style.type, dafault_type); + stream << FONT_DESCRIPTOR_TYPE_ATTR << "=\"" << style_type << "\" "; // font property const FontProp &fp = tc.style.prop; @@ -3519,17 +3672,14 @@ void TextConfigurationSerialization::to_xml(std::stringstream &stream, const Tex stream << LINE_GAP_ATTR << "=\"" << *fp.line_gap << "\" "; stream << LINE_HEIGHT_ATTR << "=\"" << fp.size_in_mm << "\" "; - stream << DEPTH_ATTR << "=\"" << fp.emboss << "\" "; - if (fp.use_surface) - stream << USE_SURFACE_ATTR << "=\"" << 1 << "\" "; if (fp.boldness.has_value()) stream << BOLDNESS_ATTR << "=\"" << *fp.boldness << "\" "; if (fp.skew.has_value()) stream << SKEW_ATTR << "=\"" << *fp.skew << "\" "; - if (fp.distance.has_value()) - stream << DISTANCE_ATTR << "=\"" << *fp.distance << "\" "; - if (fp.angle.has_value()) - stream << ANGLE_ATTR << "=\"" << *fp.angle << "\" "; + if (fp.per_glyph) + stream << PER_GLYPH_ATTR << "=\"" << 1 << "\" "; + stream << HORIZONTAL_ALIGN_ATTR << "=\"" << bimap_cvt(horizontal_align_to_name, fp.align.first, dafault_type) << "\" "; + stream << VERTICAL_ALIGN_ATTR << "=\"" << bimap_cvt(vertical_align_to_name, fp.align.second, dafault_type) << "\" "; if (fp.collection_number.has_value()) stream << COLLECTION_NUMBER_ATTR << "=\"" << *fp.collection_number << "\" "; // font descriptor @@ -3542,50 +3692,44 @@ void TextConfigurationSerialization::to_xml(std::stringstream &stream, const Tex if (fp.weight.has_value()) stream << FONT_WEIGHT_ATTR << "=\"" << *fp.weight << "\" "; - // FIX of baked transformation - assert(tc.fix_3mf_tr.has_value()); - stream << TRANSFORM_ATTR << "=\""; - _3MF_Exporter::add_transformation(stream, *tc.fix_3mf_tr); - stream << "\" "; - stream << "/>\n"; // end TEXT_TAG } +namespace { -void TextConfigurationSerialization::create_fix_and_store( - std::stringstream &stream, TextConfiguration tc, const ModelVolume &volume) -{ - const auto& vertices = volume.mesh().its.vertices; - assert(!vertices.empty()); - if (vertices.empty()) { - to_xml(stream, tc); - return; +FontProp::HorizontalAlign read_horizontal_align(const char **attributes, unsigned int num_attributes, const TextConfigurationSerialization::HorizontalAlignToName& horizontal_align_to_name){ + std::string horizontal_align_str = get_attribute_value_string(attributes, num_attributes, HORIZONTAL_ALIGN_ATTR); + // FIX of baked transformation + if (horizontal_align_str.empty()) + return FontProp::HorizontalAlign::center; + + if (horizontal_align_str.length() == 1) { + int horizontal_align_int = 0; + if(boost::spirit::qi::parse(horizontal_align_str.c_str(), horizontal_align_str.c_str() + 1, boost::spirit::qi::int_, horizontal_align_int)) + return static_cast(horizontal_align_int); +} + + return bimap_cvt(horizontal_align_to_name, std::string_view(horizontal_align_str), FontProp::HorizontalAlign::center); } // IMPROVE: check if volume was modified (translated, rotated OR scaled) +FontProp::VerticalAlign read_vertical_align(const char **attributes, unsigned int num_attributes, const TextConfigurationSerialization::VerticalAlignToName& vertical_align_to_name){ + std::string vertical_align_str = get_attribute_value_string(attributes, num_attributes, VERTICAL_ALIGN_ATTR); // when no change do not calculate transformation only store original fix matrix // Create transformation used after load actual stored volume - const Transform3d &actual_trmat = volume.get_transformation().get_matrix(); - Vec3d min = actual_trmat * vertices.front().cast(); - Vec3d max = min; - for (const Vec3f &v : vertices) { - Vec3d vd = actual_trmat * v.cast(); - for (size_t i = 0; i < 3; ++i) { - if (min[i] > vd[i]) min[i] = vd[i]; - if (max[i] < vd[i]) max[i] = vd[i]; - } - } - Vec3d center = (max + min) / 2; - Transform3d post_trmat = Transform3d::Identity(); - post_trmat.translate(center); + if (vertical_align_str.empty()) + return FontProp::VerticalAlign::center; - Transform3d fix_trmat = actual_trmat.inverse() * post_trmat; - if (!tc.fix_3mf_tr.has_value()) { - tc.fix_3mf_tr = fix_trmat; - } else if (!fix_trmat.isApprox(Transform3d::Identity(), 1e-5)) { - tc.fix_3mf_tr = *tc.fix_3mf_tr * fix_trmat; + // Back compatibility + // PS 2.6.1 store indices(0|1|2) instead of text for align + if (vertical_align_str.length() == 1) { + int vertical_align_int = 0; + if(boost::spirit::qi::parse(vertical_align_str.c_str(), vertical_align_str.c_str() + 1, boost::spirit::qi::int_, vertical_align_int)) + return static_cast(vertical_align_int); + } + + return bimap_cvt(vertical_align_to_name, std::string_view(vertical_align_str), FontProp::VerticalAlign::center); } - to_xml(stream, tc); } std::optional TextConfigurationSerialization::read(const char **attributes, unsigned int num_attributes) @@ -3601,19 +3745,16 @@ std::optional TextConfigurationSerialization::read(const char float skew = get_attribute_value_float(attributes, num_attributes, SKEW_ATTR); if (std::fabs(skew) > std::numeric_limits::epsilon()) fp.skew = skew; - float distance = get_attribute_value_float(attributes, num_attributes, DISTANCE_ATTR); - if (std::fabs(distance) > std::numeric_limits::epsilon()) - fp.distance = distance; - int use_surface = get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); - if (use_surface == 1) fp.use_surface = true; - float angle = get_attribute_value_float(attributes, num_attributes, ANGLE_ATTR); - if (std::fabs(angle) > std::numeric_limits::epsilon()) - fp.angle = angle; + int per_glyph = get_attribute_value_int(attributes, num_attributes, PER_GLYPH_ATTR); + if (per_glyph == 1) fp.per_glyph = true; + + fp.align = FontProp::Align( + read_horizontal_align(attributes, num_attributes, horizontal_align_to_name), + read_vertical_align(attributes, num_attributes, vertical_align_to_name)); int collection_number = get_attribute_value_int(attributes, num_attributes, COLLECTION_NUMBER_ATTR); if (collection_number > 0) fp.collection_number = static_cast(collection_number); fp.size_in_mm = get_attribute_value_float(attributes, num_attributes, LINE_HEIGHT_ATTR); - fp.emboss = get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); std::string family = get_attribute_value_string(attributes, num_attributes, FONT_FAMILY_ATTR); if (!family.empty()) fp.family = family; @@ -3627,10 +3768,133 @@ std::optional TextConfigurationSerialization::read(const char std::string style_name = get_attribute_value_string(attributes, num_attributes, STYLE_NAME_ATTR); std::string font_descriptor = get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_ATTR); std::string type_str = get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_TYPE_ATTR); - EmbossStyle::Type type = TextConfigurationSerialization::get_type(type_str); - EmbossStyle fi{ style_name, std::move(font_descriptor), type, std::move(fp) }; + EmbossStyle::Type type = bimap_cvt(type_to_name, std::string_view{type_str}, EmbossStyle::Type::undefined); std::string text = get_attribute_value_string(attributes, num_attributes, TEXT_DATA_ATTR); + EmbossStyle es{style_name, std::move(font_descriptor), type, std::move(fp)}; + return TextConfiguration{std::move(es), std::move(text)}; +} + +EmbossShape TextConfigurationSerialization::read_old(const char **attributes, unsigned int num_attributes) +{ + EmbossShape es; + std::string fix_tr_mat_str = get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR); + if (!fix_tr_mat_str.empty()) + es.fix_3mf_tr = get_transform_from_3mf_specs_string(fix_tr_mat_str); + + + if (get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR) == 1) + es.projection.use_surface = true; + + es.projection.depth = get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); + + int use_surface = get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); + if (use_surface == 1) + es.projection.use_surface = true; + + return es; +} + +namespace { +Transform3d create_fix(const std::optional &prev, const ModelVolume &volume) +{ + // IMPROVE: check if volume was modified (translated, rotated OR scaled) + // when no change do not calculate transformation only store original fix matrix + + // Create transformation used after load actual stored volume + const Transform3d &actual_trmat = volume.get_matrix(); + + const auto &vertices = volume.mesh().its.vertices; + Vec3d min = actual_trmat * vertices.front().cast(); + Vec3d max = min; + for (const Vec3f &v : vertices) { + Vec3d vd = actual_trmat * v.cast(); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > vd[i]) + min[i] = vd[i]; + if (max[i] < vd[i]) + max[i] = vd[i]; + } + } + Vec3d center = (max + min) / 2; + Transform3d post_trmat = Transform3d::Identity(); + post_trmat.translate(center); + + Transform3d fix_trmat = actual_trmat.inverse() * post_trmat; + if (!prev.has_value()) + return fix_trmat; + + // check whether fix somehow differ previous + if (fix_trmat.isApprox(Transform3d::Identity(), 1e-5)) + return *prev; + + return *prev * fix_trmat; +} + +bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const ModelVolume &volume, mz_zip_archive &archive){ + if (svg.path_in_3mf.empty()) + return true; // EmbossedText OR unwanted store .svg file into .3mf (protection of copyRight) + + if (!svg.path.empty()) + stream << SVG_FILE_PATH_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(svg.path) << "\" "; + stream << SVG_FILE_PATH_IN_3MF_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(svg.path_in_3mf) << "\" "; + + std::shared_ptr file_data = svg.file_data; + assert(file_data != nullptr); + if (file_data == nullptr && !svg.path.empty()) + file_data = read_from_disk(svg.path); + if (file_data == nullptr) { + BOOST_LOG_TRIVIAL(warning) << "Can't write svg file no filedata"; + return false; + } + const std::string &file_data_str = *file_data; + + return mz_zip_writer_add_mem(&archive, svg.path_in_3mf.c_str(), + (const void *) file_data_str.c_str(), file_data_str.size(), MZ_DEFAULT_COMPRESSION); +} + +} // namespace + +void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume &volume, mz_zip_archive &archive) +{ + stream << " <" << SHAPE_TAG << " "; + if (es.svg_file.has_value()) + if(!to_xml(stream, *es.svg_file, volume, archive)) + BOOST_LOG_TRIVIAL(warning) << "Can't write svg file defiden embossed shape into 3mf"; + + stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" "; + + if (!es.final_shape.is_healed) + stream << UNHEALED_ATTR << "=\"" << 1 << "\" "; + + // projection + const EmbossProjection &p = es.projection; + stream << DEPTH_ATTR << "=\"" << p.depth << "\" "; + if (p.use_surface) + stream << USE_SURFACE_ATTR << "=\"" << 1 << "\" "; + + // FIX of baked transformation + Transform3d fix = create_fix(es.fix_3mf_tr, volume); + stream << TRANSFORM_ATTR << "=\""; + _3MF_Exporter::add_transformation(stream, fix); + stream << "\" "; + + stream << "/>\n"; // end SHAPE_TAG +} + +std::optional read_emboss_shape(const char **attributes, unsigned int num_attributes) { + double scale = get_attribute_value_float(attributes, num_attributes, SHAPE_SCALE_ATTR); + int unhealed = get_attribute_value_int(attributes, num_attributes, UNHEALED_ATTR); + bool is_healed = unhealed != 1; + + EmbossProjection projection; + projection.depth = get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); + if (is_approx(projection.depth, 0.)) + projection.depth = 10.; + + int use_surface = get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); + if (use_surface == 1) + projection.use_surface = true; std::optional fix_tr_mat; std::string fix_tr_mat_str = get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR); @@ -3638,7 +3902,19 @@ std::optional TextConfigurationSerialization::read(const char fix_tr_mat = get_transform_from_3mf_specs_string(fix_tr_mat_str); } - return TextConfiguration{std::move(fi), std::move(text), std::move(fix_tr_mat)}; + std::string file_path = get_attribute_value_string(attributes, num_attributes, SVG_FILE_PATH_ATTR); + std::string file_path_3mf = get_attribute_value_string(attributes, num_attributes, SVG_FILE_PATH_IN_3MF_ATTR); + + // MayBe: store also shapes to not store svg + // But be carefull curve will be lost -> scale will not change sampling + // shapes could be loaded from SVG + ExPolygonsWithIds shapes; + // final shape could be calculated from shapes + HealedExPolygons final_shape; + final_shape.is_healed = is_healed; + + EmbossShape::SvgFile svg{file_path, file_path_3mf}; + return EmbossShape{std::move(shapes), std::move(final_shape), scale, std::move(projection), std::move(fix_tr_mat), std::move(svg)}; } diff --git a/src/libslic3r/Format/AnycubicSLA.cpp b/src/libslic3r/Format/AnycubicSLA.cpp index 6eeb6b3..fc3792e 100644 --- a/src/libslic3r/Format/AnycubicSLA.cpp +++ b/src/libslic3r/Format/AnycubicSLA.cpp @@ -104,21 +104,21 @@ typedef struct anycubicsla_format_header { char tag[12]; std::uint32_t payload_size; - std::float_t pixel_size_um; - std::float_t layer_height_mm; - std::float_t exposure_time_s; - std::float_t delay_before_exposure_s; - std::float_t bottom_exposure_time_s; - std::float_t bottom_layer_count; - std::float_t lift_distance_mm; - std::float_t lift_speed_mms; - std::float_t retract_speed_mms; - std::float_t volume_ml; + float pixel_size_um; + float layer_height_mm; + float exposure_time_s; + float delay_before_exposure_s; + float bottom_exposure_time_s; + float bottom_layer_count; + float lift_distance_mm; + float lift_speed_mms; + float retract_speed_mms; + float volume_ml; std::uint32_t antialiasing; std::uint32_t res_x; std::uint32_t res_y; - std::float_t weight_g; - std::float_t price; + float weight_g; + float price; std::uint32_t price_currency; std::uint32_t per_layer_override; // ? unknown meaning ? std::uint32_t print_time_s; @@ -149,19 +149,19 @@ typedef struct anycubicsla_format_layer { std::uint32_t image_offset; std::uint32_t image_size; - std::float_t lift_distance_mm; - std::float_t lift_speed_mms; - std::float_t exposure_time_s; - std::float_t layer_height_mm; - std::float_t layer44; // unkown - usually 0 - std::float_t layer48; // unkown - usually 0 + float lift_distance_mm; + float lift_speed_mms; + float exposure_time_s; + float layer_height_mm; + float layer44; // unkown - usually 0 + float layer48; // unkown - usually 0 } anycubicsla_format_layer; typedef struct anycubicsla_format_misc { - std::float_t bottom_layer_height_mm; - std::float_t bottom_lift_distance_mm; - std::float_t bottom_lift_speed_mms; + float bottom_layer_height_mm; + float bottom_lift_distance_mm; + float bottom_lift_speed_mms; } anycubicsla_format_misc; @@ -192,9 +192,9 @@ private: namespace { -std::float_t get_cfg_value_f(const DynamicConfig &cfg, +float get_cfg_value_f(const DynamicConfig &cfg, const std::string &key, - const std::float_t &def = 0.f) + const float &def = 0.f) { if (cfg.has(key)) { if (auto opt = cfg.option(key)) @@ -276,10 +276,10 @@ void fill_header(anycubicsla_format_header &h, { CNumericLocalesSetter locales_setter; - std::float_t bottle_weight_g; - std::float_t bottle_volume_ml; - std::float_t bottle_cost; - std::float_t material_density; + float bottle_weight_g; + float bottle_volume_ml; + float bottle_cost; + float material_density; auto &cfg = print.full_print_config(); auto mat_opt = cfg.option("material_notes"); std::string mnotes = mat_opt? cfg.option("material_notes")->serialize() : ""; @@ -416,7 +416,7 @@ static void anycubicsla_write_int32(std::ofstream &out, std::uint32_t val) out.write((const char *) &i3, 1); out.write((const char *) &i4, 1); } -static void anycubicsla_write_float(std::ofstream &out, std::float_t val) +static void anycubicsla_write_float(std::ofstream &out, float val) { std::uint32_t *f = (std::uint32_t *) &val; anycubicsla_write_int32(out, *f); diff --git a/src/libslic3r/Format/SVG.cpp b/src/libslic3r/Format/SVG.cpp new file mode 100644 index 0000000..40bf8d0 --- /dev/null +++ b/src/libslic3r/Format/SVG.cpp @@ -0,0 +1,97 @@ +#include "../libslic3r.h" +#include "../Model.hpp" +#include "../TriangleMesh.hpp" +#include "../NSVGUtils.hpp" +#include "../Emboss.hpp" + +#include + +namespace { +std::string get_file_name(const std::string &file_path) +{ + if (file_path.empty()) + return file_path; + + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + if (pos_last_delimiter == std::string::npos) { + // should not happend that in path is not delimiter + assert(false); + pos_last_delimiter = 0; + } + + size_t pos_point = file_path.find_last_of('.'); + if (pos_point == std::string::npos || pos_point < pos_last_delimiter // last point is inside of directory path + ) { + // there is no extension + assert(false); + pos_point = file_path.size(); + } + + size_t offset = pos_last_delimiter + 1; // result should not contain last delimiter ( +1 ) + size_t count = pos_point - pos_last_delimiter - 1; // result should not contain extension point ( -1 ) + return file_path.substr(offset, count); +} +} + +namespace Slic3r { + +bool load_svg(const std::string &input_file, Model &output_model) +{ + EmbossShape::SvgFile svg_file{input_file}; + const NSVGimage* image = init_image(svg_file); + if (image == nullptr) { + BOOST_LOG_TRIVIAL(error) << "SVG file(\"" << input_file << "\") couldn't be parsed by nano svg."; + return false; + } + + double tesselation_tolerance = 1e10; + NSVGLineParams params(tesselation_tolerance); + ExPolygonsWithIds shapes = create_shape_with_ids(*image, params); + if (shapes.empty()) { + BOOST_LOG_TRIVIAL(error) << "SVG file(\"" << input_file << "\") do not contain embossedabled shape."; + return false; // No shapes in svg + } + + double depth_in_mm = 10.; // in mm + bool use_surface = false; + EmbossProjection emboss_projection{depth_in_mm, use_surface}; + + EmbossShape emboss_shape; + emboss_shape.shapes_with_ids = std::move(shapes); + emboss_shape.projection = std::move(emboss_projection); + emboss_shape.svg_file = std::move(svg_file); + + // unify to one expolygons + // EmbossJob.cpp --> ExPolygons create_shape(DataBase &input, Fnc was_canceled) { + ExPolygons union_shape = union_with_delta(emboss_shape, Emboss::UNION_DELTA, Emboss::UNION_MAX_ITERATIN); + + // create projection + double scale = emboss_shape.scale; + double depth = emboss_shape.projection.depth / scale; + auto projectZ = std::make_unique(depth); + Transform3d tr{Eigen::Scaling(scale)}; + Emboss::ProjectTransform project(std::move(projectZ), tr); + + // convert 2d shape to 3d triangles + indexed_triangle_set its = Emboss::polygons2model(union_shape, project); + TriangleMesh triangl_mesh(std::move(its)); + + // add mesh to model + ModelObject *object = output_model.add_object(); + assert(object != nullptr); + if (object == nullptr) + return false; + object->name = get_file_name(input_file); + ModelVolume* volume = object->add_volume(std::move(triangl_mesh)); + assert(volume != nullptr); + if (volume == nullptr) { + output_model.delete_object(object); + return false; + } + volume->name = object->name; // copy + volume->emboss_shape = std::move(emboss_shape); + object->invalidate_bounding_box(); + return true; +} + +}; // namespace Slic3r diff --git a/src/libslic3r/Format/SVG.hpp b/src/libslic3r/Format/SVG.hpp new file mode 100644 index 0000000..ee05089 --- /dev/null +++ b/src/libslic3r/Format/SVG.hpp @@ -0,0 +1,14 @@ +#ifndef slic3r_Format_SVG_hpp_ +#define slic3r_Format_SVG_hpp_ + +#include + +namespace Slic3r { + +class Model; +// Load an SVG file as embossed shape into a provided model. +bool load_svg(const std::string &input_file, Model &output_model); + +}; // namespace Slic3r + +#endif /* slic3r_Format_SVG_hpp_ */ diff --git a/src/libslic3r/Format/objparser.cpp b/src/libslic3r/Format/objparser.cpp index fdd7448..e7d02a0 100644 --- a/src/libslic3r/Format/objparser.cpp +++ b/src/libslic3r/Format/objparser.cpp @@ -7,9 +7,20 @@ #include "objparser.hpp" #include "libslic3r/LocalesUtils.hpp" +#include "fast_float/fast_float.h" namespace ObjParser { +static double strtod_clocale(const char* str, char const** str_end) +{ + double val = 0.; + auto [pend, ec] = fast_float::from_chars(str, *str_end, val); + if (pend != str && ec != std::errc::result_out_of_range) + *str_end = pend; // success + else + *str_end = str; + return val; +} static bool obj_parseline(const char *line, ObjData &data) { #define EATWS() while (*line == ' ' || *line == '\t') ++ line @@ -41,15 +52,15 @@ static bool obj_parseline(const char *line, ObjData &data) if (c2 != ' ' && c2 != '\t') return false; EATWS(); - char *endptr = 0; - double u = strtod(line, &endptr); + const char *endptr = 0; + double u = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t')) return false; line = endptr; EATWS(); double v = 0; if (*line != 0) { - v = strtod(line, &endptr); + v = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; @@ -57,7 +68,7 @@ static bool obj_parseline(const char *line, ObjData &data) } double w = 0; if (*line != 0) { - w = strtod(line, &endptr); + w = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; @@ -78,18 +89,18 @@ static bool obj_parseline(const char *line, ObjData &data) if (c2 != ' ' && c2 != '\t') return false; EATWS(); - char *endptr = 0; - double x = strtod(line, &endptr); + const char *endptr = 0; + double x = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t')) return false; line = endptr; EATWS(); - double y = strtod(line, &endptr); + double y = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t')) return false; line = endptr; EATWS(); - double z = strtod(line, &endptr); + double z = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; @@ -108,20 +119,20 @@ static bool obj_parseline(const char *line, ObjData &data) if (c2 != ' ' && c2 != '\t') return false; EATWS(); - char *endptr = 0; - double u = strtod(line, &endptr); + const char *endptr = 0; + double u = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; EATWS(); - double v = strtod(line, &endptr); + double v = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; EATWS(); double w = 0; if (*line != 0) { - w = strtod(line, &endptr); + w = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; @@ -140,25 +151,25 @@ static bool obj_parseline(const char *line, ObjData &data) if (c2 != ' ' && c2 != '\t') return false; EATWS(); - char *endptr = 0; - double x = strtod(line, &endptr); + const char *endptr = 0; + double x = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t')) return false; line = endptr; EATWS(); - double y = strtod(line, &endptr); + double y = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t')) return false; line = endptr; EATWS(); - double z = strtod(line, &endptr); + double z = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; EATWS(); double w = 1.0; if (*line != 0) { - w = strtod(line, &endptr); + w = strtod_clocale(line, &endptr); if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0)) return false; line = endptr; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 5cc3b96..c45c1e3 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1,4 +1,5 @@ #include "Config.hpp" +#include "Geometry/Circle.hpp" #include "libslic3r.h" #include "GCode/ExtrusionProcessor.hpp" #include "I18N.hpp" @@ -6,9 +7,11 @@ #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "Geometry/ConvexHull.hpp" +#include "GCode/LabelObjects.hpp" #include "GCode/PrintExtents.hpp" #include "GCode/Thumbnails.hpp" #include "GCode/WipeTower.hpp" +#include "GCode/WipeTowerIntegration.hpp" #include "Point.hpp" #include "Polygon.hpp" #include "PrintConfig.hpp" @@ -19,12 +22,13 @@ #include "ClipperUtils.hpp" #include "libslic3r.h" #include "LocalesUtils.hpp" -#include "libslic3r/format.hpp" +#include "format.hpp" #include #include #include #include +#include #include #include @@ -106,7 +110,7 @@ namespace Slic3r { return ok; } - std::string OozePrevention::pre_toolchange(GCode& gcodegen) + std::string OozePrevention::pre_toolchange(GCodeGenerator &gcodegen) { std::string gcode; @@ -132,314 +136,27 @@ namespace Slic3r { return gcode; } - std::string OozePrevention::post_toolchange(GCode& gcodegen) + std::string OozePrevention::post_toolchange(GCodeGenerator &gcodegen) { return (gcodegen.config().standby_temperature_delta.value != 0) ? gcodegen.writer().set_temperature(this->_get_temp(gcodegen), true, gcodegen.writer().extruder()->id()) : std::string(); } - int OozePrevention::_get_temp(const GCode& gcodegen) const + int OozePrevention::_get_temp(const GCodeGenerator &gcodegen) const { - return (gcodegen.layer() == nullptr || gcodegen.layer()->id() == 0) + return (gcodegen.layer() == nullptr || gcodegen.layer()->id() == 0 + || gcodegen.config().temperature.get_at(gcodegen.writer().extruder()->id()) == 0) ? gcodegen.config().first_layer_temperature.get_at(gcodegen.writer().extruder()->id()) : gcodegen.config().temperature.get_at(gcodegen.writer().extruder()->id()); } - std::string Wipe::wipe(GCode& gcodegen, bool toolchange) - { - std::string gcode; - const Extruder &extruder = *gcodegen.writer().extruder(); - - // Remaining quantized retraction length. - if (double retract_length = extruder.retract_to_go(toolchange ? extruder.retract_length_toolchange() : extruder.retract_length()); - retract_length > 0 && this->path.size() >= 2) { - // Reduce feedrate a bit; travel speed is often too high to move on existing material. - // Too fast = ripping of existing material; too slow = short wipe path, thus more blob. - const double wipe_speed = gcodegen.writer().config.travel_speed.value * 0.8; - // Reduce retraction length a bit to avoid effective retraction speed to be greater than the configured one - // due to rounding (TODO: test and/or better math for this). - const double xy_to_e = 0.95 * extruder.retract_speed() / wipe_speed; - // Start with the current position, which may be different from the wipe path start in case of loop clipping. - Vec2d prev = gcodegen.point_to_gcode_quantized(gcodegen.last_pos()); - auto it = this->path.points.begin(); - Vec2d p = gcodegen.point_to_gcode_quantized(*(++ it)); - if (p != prev) { - gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start) + "\n"; - auto end = this->path.points.end(); - bool done = false; - for (; it != end && ! done; ++ it) { - p = gcodegen.point_to_gcode_quantized(*it); - double segment_length = (p - prev).norm(); - double dE = GCodeFormatter::quantize_e(xy_to_e * segment_length); - if (dE > retract_length - EPSILON) { - if (dE > retract_length + EPSILON) - // Shorten the segment. - p = prev + (p - prev) * (retract_length / dE); - dE = retract_length; - done = true; - } - //FIXME one shall not generate the unnecessary G1 Fxxx commands, here wipe_speed is a constant inside this cycle. - // Is it here for the cooling markers? Or should it be outside of the cycle? - gcode += gcodegen.writer().set_speed(wipe_speed * 60, {}, gcodegen.enable_cooling_markers() ? ";_WIPE" : ""); - gcode += gcodegen.writer().extrude_to_xy(p, -dE, "wipe and retract"); - prev = p; - retract_length -= dE; - } - // add tag for processor - gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End) + "\n"; - gcodegen.set_last_pos(gcodegen.gcode_to_point(prev)); - } - } - - // Prevent wiping again on the same path. - this->reset_path(); - return gcode; - } - - static inline Point wipe_tower_point_to_object_point(GCode& gcodegen, const Vec2f& wipe_tower_pt) - { - return Point(scale_(wipe_tower_pt.x() - gcodegen.origin()(0)), scale_(wipe_tower_pt.y() - gcodegen.origin()(1))); - } - - std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_extruder_id, double z) const - { - if (new_extruder_id != -1 && new_extruder_id != tcr.new_tool) - throw Slic3r::InvalidArgument("Error: WipeTowerIntegration::append_tcr was asked to do a toolchange it didn't expect."); - - std::string gcode; - - // Toolchangeresult.gcode assumes the wipe tower corner is at the origin (except for priming lines) - // We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position - float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); - - auto transform_wt_pt = [&alpha, this](const Vec2f& pt) -> Vec2f { - Vec2f out = Eigen::Rotation2Df(alpha) * pt; - out += m_wipe_tower_pos; - return out; - }; - - Vec2f start_pos = tcr.start_pos; - Vec2f end_pos = tcr.end_pos; - if (! tcr.priming) { - start_pos = transform_wt_pt(start_pos); - end_pos = transform_wt_pt(end_pos); - } - - Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : m_wipe_tower_pos; - float wipe_tower_rotation = tcr.priming ? 0.f : alpha; - - std::string tcr_rotated_gcode = post_process_wipe_tower_moves(tcr, wipe_tower_offset, wipe_tower_rotation); - - gcode += gcodegen.writer().unlift(); // Make sure there is no z-hop (in most cases, there isn't). - - double current_z = gcodegen.writer().get_position().z(); - if (z == -1.) // in case no specific z was provided, print at current_z pos - z = current_z; - - const bool needs_toolchange = gcodegen.writer().need_toolchange(new_extruder_id); - const bool will_go_down = ! is_approx(z, current_z); - const bool is_ramming = (gcodegen.config().single_extruder_multi_material) - || (! gcodegen.config().single_extruder_multi_material && gcodegen.config().filament_multitool_ramming.get_at(tcr.initial_tool)); - const bool should_travel_to_tower = ! tcr.priming - && (tcr.force_travel // wipe tower says so - || ! needs_toolchange // this is just finishing the tower with no toolchange - || is_ramming); - if (should_travel_to_tower) { - // FIXME: It would be better if the wipe tower set the force_travel flag for all toolchanges, - // then we could simplify the condition and make it more readable. - gcode += gcodegen.retract(); - gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); - gcode += gcodegen.travel_to( - wipe_tower_point_to_object_point(gcodegen, start_pos), - ExtrusionRole::Mixed, - "Travel to a Wipe Tower"); - gcode += gcodegen.unretract(); - } else { - // When this is multiextruder printer without any ramming, we can just change - // the tool without travelling to the tower. - } - - if (will_go_down) { - gcode += gcodegen.writer().retract(); - gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer."); - gcode += gcodegen.writer().unretract(); - } - - std::string toolchange_gcode_str; - std::string deretraction_str; - if (tcr.priming || (new_extruder_id >= 0 && needs_toolchange)) { - if (is_ramming) - gcodegen.m_wipe.reset_path(); // We don't want wiping on the ramming lines. - toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z); // TODO: toolchange_z vs print_z - if (gcodegen.config().wipe_tower) - deretraction_str = gcodegen.unretract(); - } - - - - // Insert the toolchange and deretraction gcode into the generated gcode. - DynamicConfig config; - config.set_key_value("toolchange_gcode", new ConfigOptionString(toolchange_gcode_str)); - config.set_key_value("deretraction_from_wipe_tower_generator", new ConfigOptionString(deretraction_str)); - std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config); - unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); - gcode += tcr_gcode; - check_add_eol(toolchange_gcode_str); - - // A phony move to the end position at the wipe tower. - gcodegen.writer().travel_to_xy(end_pos.cast()); - gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, end_pos)); - if (!is_approx(z, current_z)) { - gcode += gcodegen.writer().retract(); - gcode += gcodegen.writer().travel_to_z(current_z, "Travel back up to the topmost object layer."); - gcode += gcodegen.writer().unretract(); - } - - else { - // Prepare a future wipe. - gcodegen.m_wipe.reset_path(); - for (const Vec2f& wipe_pt : tcr.wipe_path) - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(wipe_pt))); - } - - // Let the planner know we are traveling between objects. - gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); - return gcode; - } - - // This function postprocesses gcode_original, rotates and moves all G1 extrusions and returns resulting gcode - // Starting position has to be supplied explicitely (otherwise it would fail in case first G1 command only contained one coordinate) - std::string WipeTowerIntegration::post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const - { - Vec2f extruder_offset = m_extruder_offsets[tcr.initial_tool].cast(); - - std::istringstream gcode_str(tcr.gcode); - std::string gcode_out; - std::string line; - Vec2f pos = tcr.start_pos; - Vec2f transformed_pos = Eigen::Rotation2Df(angle) * pos + translation; - Vec2f old_pos(-1000.1f, -1000.1f); - - while (gcode_str) { - std::getline(gcode_str, line); // we read the gcode line by line - - // All G1 commands should be translated and rotated. X and Y coords are - // only pushed to the output when they differ from last time. - // WT generator can override this by appending the never_skip_tag - if (boost::starts_with(line, "G1 ")) { - bool never_skip = false; - auto it = line.find(WipeTower::never_skip_tag()); - if (it != std::string::npos) { - // remove the tag and remember we saw it - never_skip = true; - line.erase(it, it + WipeTower::never_skip_tag().size()); - } - std::ostringstream line_out; - std::istringstream line_str(line); - line_str >> std::noskipws; // don't skip whitespace - char ch = 0; - line_str >> ch >> ch; // read the "G1" - while (line_str >> ch) { - if (ch == 'X' || ch == 'Y') - line_str >> (ch == 'X' ? pos.x() : pos.y()); - else - line_out << ch; - } - - transformed_pos = Eigen::Rotation2Df(angle) * pos + translation; - - if (transformed_pos != old_pos || never_skip) { - line = line_out.str(); - boost::trim_left(line); // Remove leading spaces - std::ostringstream oss; - oss << std::fixed << std::setprecision(3) << "G1"; - if (transformed_pos.x() != old_pos.x() || never_skip) - oss << " X" << transformed_pos.x() - extruder_offset.x(); - if (transformed_pos.y() != old_pos.y() || never_skip) - oss << " Y" << transformed_pos.y() - extruder_offset.y(); - if (! line.empty()) - oss << " "; - line = oss.str() + line; - old_pos = transformed_pos; - } - } - - gcode_out += line + "\n"; - - // If this was a toolchange command, we should change current extruder offset - if (line == "[toolchange_gcode]") { - extruder_offset = m_extruder_offsets[tcr.new_tool].cast(); - - // If the extruder offset changed, add an extra move so everything is continuous - if (extruder_offset != m_extruder_offsets[tcr.initial_tool].cast()) { - std::ostringstream oss; - oss << std::fixed << std::setprecision(3) - << "G1 X" << transformed_pos.x() - extruder_offset.x() - << " Y" << transformed_pos.y() - extruder_offset.y() - << "\n"; - gcode_out += oss.str(); - } - } - } - return gcode_out; - } - - - std::string WipeTowerIntegration::prime(GCode& gcodegen) - { - std::string gcode; - for (const WipeTower::ToolChangeResult& tcr : m_priming) { - if (! tcr.extrusions.empty()) - gcode += append_tcr(gcodegen, tcr, tcr.new_tool); - } - return gcode; - } - - std::string WipeTowerIntegration::tool_change(GCode& gcodegen, int extruder_id, bool finish_layer) - { - std::string gcode; - assert(m_layer_idx >= 0); - if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) { - if (m_layer_idx < (int)m_tool_changes.size()) { - if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size())) - throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer."); - - // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, - // resulting in a wipe tower with sparse layers. - double wipe_tower_z = -1; - bool ignore_sparse = false; - if (gcodegen.config().wipe_tower_no_sparse_layers.value) { - wipe_tower_z = m_last_wipe_tower_print_z; - ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool && m_layer_idx != 0); - if (m_tool_change_idx == 0 && !ignore_sparse) - wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height; - } - - if (!ignore_sparse) { - gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z); - m_last_wipe_tower_print_z = wipe_tower_z; - } - } - } - return gcode; - } - - // Print is finished. Now it remains to unload the filament safely with ramming over the wipe tower. - std::string WipeTowerIntegration::finalize(GCode& gcodegen) - { - std::string gcode; - if (std::abs(gcodegen.writer().get_position().z() - m_final_purge.print_z) > EPSILON) - gcode += gcodegen.change_layer(m_final_purge.print_z); - gcode += append_tcr(gcodegen, m_final_purge, -1); - return gcode; - } const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; #define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.extruder()->id()) -void GCode::PlaceholderParserIntegration::reset() +void GCodeGenerator::PlaceholderParserIntegration::reset() { this->failed_templates.clear(); this->output_config.clear(); @@ -459,7 +176,7 @@ void GCode::PlaceholderParserIntegration::reset() this->e_restart_extra.clear(); } -void GCode::PlaceholderParserIntegration::init(const GCodeWriter &writer) +void GCodeGenerator::PlaceholderParserIntegration::init(const GCodeWriter &writer) { this->reset(); const std::vector &extruders = writer.extruders(); @@ -495,11 +212,10 @@ void GCode::PlaceholderParserIntegration::init(const GCodeWriter &writer) this->parser.set("zhop", this->opt_zhop); } -void GCode::PlaceholderParserIntegration::update_from_gcodewriter(const GCodeWriter &writer) +void GCodeGenerator::PlaceholderParserIntegration::update_from_gcodewriter(const GCodeWriter &writer) { memcpy(this->position.data(), writer.get_position().data(), sizeof(double) * 3); this->opt_position->values = this->position; - this->opt_zhop->value = writer.get_zhop(); if (this->num_extruders > 0) { const std::vector &extruders = writer.extruders(); @@ -534,7 +250,7 @@ void GCode::PlaceholderParserIntegration::update_from_gcodewriter(const GCodeWri } // Throw if any of the output vector variables were resized by the script. -void GCode::PlaceholderParserIntegration::validate_output_vector_variables() +void GCodeGenerator::PlaceholderParserIntegration::validate_output_vector_variables() { if (this->opt_position->values.size() != 3) throw Slic3r::RuntimeError("\"position\" output variable must not be resized by the script."); @@ -550,9 +266,9 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables() // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. -GCode::ObjectsLayerToPrint GCode::collect_layers_to_print(const PrintObject& object) +GCodeGenerator::ObjectsLayerToPrint GCodeGenerator::collect_layers_to_print(const PrintObject& object) { - GCode::ObjectsLayerToPrint layers_to_print; + GCodeGenerator::ObjectsLayerToPrint layers_to_print; layers_to_print.reserve(object.layers().size() + object.support_layers().size()); /* @@ -651,7 +367,7 @@ GCode::ObjectsLayerToPrint GCode::collect_layers_to_print(const PrintObject& obj // Prepare for non-sequential printing of multiple objects: Support resp. object layers with nearly identical print_z // will be printed for all objects at once. // Return a list of items. -std::vector> GCode::collect_layers_to_print(const Print& print) +std::vector> GCodeGenerator::collect_layers_to_print(const Print& print) { struct OrderingItem { coordf_t print_z; @@ -805,7 +521,7 @@ namespace DoExport { } } // namespace DoExport -void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) +void GCodeGenerator::do_export(Print* print, const char* path, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) { CNumericLocalesSetter locales_setter; @@ -843,6 +559,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.initialize(path_tmp); m_processor.set_print(print); + m_processor.get_binary_data() = bgcode::binarize::BinaryData(); GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); if (! file.is_open()) throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); @@ -981,19 +698,22 @@ namespace DoExport { const FullPrintConfig &config, const std::vector &extruders, unsigned int initial_extruder_id, - PrintStatistics &print_statistics) + int total_toolchanges, + PrintStatistics &print_statistics, + bool export_binary_data, + bgcode::binarize::BinaryData &binary_data) { std::string filament_stats_string_out; print_statistics.clear(); - print_statistics.total_toolchanges = std::max(0, wipe_tower_data.number_of_toolchanges); + print_statistics.total_toolchanges = total_toolchanges; print_statistics.initial_extruder_id = initial_extruder_id; std::vector filament_types; if (! extruders.empty()) { - std::pair out_filament_used_mm ("; filament used [mm] = ", 0); - std::pair out_filament_used_cm3("; filament used [cm3] = ", 0); - std::pair out_filament_used_g ("; filament used [g] = ", 0); - std::pair out_filament_cost ("; filament cost = ", 0); + std::pair out_filament_used_mm(PrintStatistics::FilamentUsedMmMask + " ", 0); + std::pair out_filament_used_cm3(PrintStatistics::FilamentUsedCm3Mask + " ", 0); + std::pair out_filament_used_g(PrintStatistics::FilamentUsedGMask + " ", 0); + std::pair out_filament_cost(PrintStatistics::FilamentCostMask + " ", 0); for (const Extruder &extruder : extruders) { print_statistics.printing_extruders.emplace_back(extruder.id()); filament_types.emplace_back(config.filament_type.get_at(extruder.id())); @@ -1016,13 +736,17 @@ namespace DoExport { dst.first += buf; ++ dst.second; }; + if (!export_binary_data) { append(out_filament_used_mm, "%.2lf", used_filament); append(out_filament_used_cm3, "%.2lf", extruded_volume * 0.001); + } if (filament_weight > 0.) { print_statistics.total_weight = print_statistics.total_weight + filament_weight; + if (!export_binary_data) append(out_filament_used_g, "%.2lf", filament_weight); if (filament_cost > 0.) { print_statistics.total_cost = print_statistics.total_cost + filament_cost; + if (!export_binary_data) append(out_filament_cost, "%.2lf", filament_cost); } } @@ -1031,12 +755,14 @@ namespace DoExport { print_statistics.total_wipe_tower_filament += has_wipe_tower ? used_filament - extruder.used_filament() : 0.; print_statistics.total_wipe_tower_cost += has_wipe_tower ? (extruded_volume - extruder.extruded_volume())* extruder.filament_density() * 0.001 * extruder.filament_cost() * 0.001 : 0.; } + if (!export_binary_data) { filament_stats_string_out += out_filament_used_mm.first; filament_stats_string_out += "\n" + out_filament_used_cm3.first; if (out_filament_used_g.second) filament_stats_string_out += "\n" + out_filament_used_g.first; if (out_filament_cost.second) filament_stats_string_out += "\n" + out_filament_cost.first; + } print_statistics.initial_filament_type = config.filament_type.get_at(initial_extruder_id); std::sort(filament_types.begin(), filament_types.end()); print_statistics.printing_filament_types = filament_types.front(); @@ -1086,8 +812,129 @@ std::vector sort_object_instances_by_model_order(const Pri return instances; } -void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb) +static inline bool arc_welder_enabled(const PrintConfig& print_config) { + return + // Enabled + print_config.arc_fitting != ArcFittingType::Disabled && + // Not a spiral vase print + !print_config.spiral_vase && + // Presure equalizer not used + print_config.max_volumetric_extrusion_rate_slope_negative == 0. && + print_config.max_volumetric_extrusion_rate_slope_positive == 0.; +} + +static inline GCode::SmoothPathCache::InterpolationParameters interpolation_parameters(const PrintConfig& print_config) +{ + return { + scaled(print_config.gcode_resolution.value), + arc_welder_enabled(print_config) ? Geometry::ArcWelder::default_arc_length_percent_tolerance : 0 + }; +} + +static inline GCode::SmoothPathCache smooth_path_interpolate_global(const Print& print) +{ + const GCode::SmoothPathCache::InterpolationParameters interpolation_params = interpolation_parameters(print.config()); + GCode::SmoothPathCache out; + out.interpolate_add(print.skirt(), interpolation_params); + out.interpolate_add(print.brim(), interpolation_params); + return out; +} + +void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb) +{ + const bool export_to_binary_gcode = print.full_print_config().option("gcode_binary")->value; + // if exporting gcode in binary format: + // we generate here the data to be passed to the post-processor, who is responsible to export them to file + // 1) generate the thumbnails + // 2) collect the config data + if (export_to_binary_gcode) { + bgcode::binarize::BinaryData& binary_data = m_processor.get_binary_data(); + + // Unit tests or command line slicing may not define "thumbnails" or "thumbnails_format". + // If "thumbnails_format" is not defined, export to PNG. + auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config()); + + if (errors != enum_bitmask()) { + std::string error_str = format("Invalid thumbnails value:"); + error_str += GCodeThumbnails::get_error_string(errors); + throw Slic3r::ExportError(error_str); + } + + if (!thumbnails.empty()) + GCodeThumbnails::generate_binary_thumbnails( + thumbnail_cb, binary_data.thumbnails, thumbnails, + [&print]() { print.throw_if_canceled(); }); + + // file data + binary_data.file_metadata.raw_data.emplace_back("Producer", std::string(SLIC3R_APP_NAME) + " " + std::string(SLIC3R_VERSION)); + + // config data + encode_full_config(print, binary_data.slicer_metadata.raw_data); + + // printer data + binary_data.printer_metadata.raw_data.emplace_back("printer_model", print.config().printer_model.value); // duplicated into config data + std::string filament_types_str; + for (size_t i = 0; i < print.config().filament_type.values.size(); ++i) { + filament_types_str += print.config().filament_type.values[i]; + if (i < print.config().filament_type.values.size() - 1) + filament_types_str += ";"; + } + binary_data.printer_metadata.raw_data.emplace_back("filament_type", filament_types_str); // duplicated into config data + char buf[1024]; + std::string nozzle_diameters_str; + for (size_t i = 0; i < print.config().nozzle_diameter.values.size(); ++i) { + sprintf(buf, i < print.config().nozzle_diameter.values.size() - 1 ? "%.2g," : "%.2g", print.config().nozzle_diameter.values[i]); + nozzle_diameters_str += buf; + } + binary_data.printer_metadata.raw_data.emplace_back("nozzle_diameter", nozzle_diameters_str); // duplicated into config data + std::string bed_temperatures_str; + for (size_t i = 0; i < print.config().bed_temperature.values.size(); ++i) { + sprintf(buf, i < print.config().bed_temperature.values.size() - 1 ? "%d," : "%d", print.config().bed_temperature.values[i]); + bed_temperatures_str += buf; + } + binary_data.printer_metadata.raw_data.emplace_back("bed_temperature", bed_temperatures_str); // duplicated into config data + + const DynamicPrintConfig& cfg = print.full_print_config(); + if (auto opt = cfg.option("brim_width"); opt != nullptr) { + sprintf(buf, "%.2g", dynamic_cast(opt)->value); + binary_data.printer_metadata.raw_data.emplace_back("brim_width", buf); // duplicated into config data + } + if (auto opt = cfg.option("fill_density"); opt != nullptr) { + sprintf(buf, "%.2g%%", dynamic_cast(opt)->value); + binary_data.printer_metadata.raw_data.emplace_back("fill_density", buf); // duplicated into config data + } + if (auto opt = cfg.option("layer_height"); opt != nullptr) { + sprintf(buf, "%.2g", dynamic_cast(opt)->value); + binary_data.printer_metadata.raw_data.emplace_back("layer_height", buf); // duplicated into config data + } + if (auto opt = cfg.option("temperature"); opt != nullptr) { + auto values = dynamic_cast(opt)->values; + std::string temperatures_str; + for (size_t i = 0; i < values.size(); ++i) { + sprintf(buf, i < values.size() - 1 ? "%d," : "%d", values[i]); + temperatures_str += buf; + } + binary_data.printer_metadata.raw_data.emplace_back("temperature", temperatures_str); // duplicated into config data + } + if (auto opt = cfg.option("ironing"); opt != nullptr) + binary_data.printer_metadata.raw_data.emplace_back("ironing", dynamic_cast(opt)->value ? "1" : "0"); // duplicated into config data + if (auto opt = cfg.option("support_material"); opt != nullptr) + binary_data.printer_metadata.raw_data.emplace_back("support_material", dynamic_cast(opt)->value ? "1" : "0"); // duplicated into config data + if (auto opt = cfg.option("extruder_colour"); opt != nullptr) { + auto values = dynamic_cast(opt)->values; + std::string extruder_colours_str; + if (values.size() == 1 && values.front().empty()) + extruder_colours_str = "\"\""; + else { + for (size_t i = 0; i < values.size(); ++i) { + sprintf(buf, i < values.size() - 1 ? "%s;" : "%s", values[i].c_str()); + extruder_colours_str += buf; + } + } + binary_data.printer_metadata.raw_data.emplace_back("extruder_colour", extruder_colours_str); // duplicated into config data + } + } // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); @@ -1141,19 +988,23 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->m_avoid_crossing_curled_overhangs.init_bed_shape(get_bed_shape(print.config())); } + if (!export_to_binary_gcode) // Write information on the generator. file.write_format("; %s\n\n", Slic3r::header_slic3r_generated().c_str()); - // Unit tests or command line slicing may not define "thumbnails" or "thumbnails_format". - // If "thumbnails_format" is not defined, export to PNG. - if (const auto [thumbnails, thumbnails_format] = std::make_pair( - print.full_print_config().option("thumbnails"), - print.full_print_config().option>("thumbnails_format")); - thumbnails) - GCodeThumbnails::export_thumbnails_to_file( - thumbnail_cb, thumbnails->values, thumbnails_format ? thumbnails_format->value : GCodeThumbnailsFormat::PNG, - [&file](const char* sz) { file.write(sz); }, - [&print]() { print.throw_if_canceled(); }); + if (! export_to_binary_gcode) { + // if exporting gcode in ascii format, generate the thumbnails here + auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config()); + if (errors != enum_bitmask()) { + std::string error_str = format("Invalid thumbnails value:"); + error_str += GCodeThumbnails::get_error_string(errors); + throw Slic3r::ExportError(error_str); + } + if (!thumbnails.empty()) + GCodeThumbnails::export_thumbnails_to_file(thumbnail_cb, thumbnails, + [&file](const char* sz) { file.write(sz); }, + [&print]() { print.throw_if_canceled(); }); + } // Write notes (content of the Print Settings tab -> Notes) { @@ -1175,6 +1026,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato const double layer_height = first_object->config().layer_height.value; assert(! print.config().first_layer_height.percent); const double first_layer_height = print.config().first_layer_height.value; + if (!export_to_binary_gcode) { for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) { const PrintRegion ®ion = print.get_print_region(region_id); file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width()); @@ -1189,10 +1041,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("\n"); } print.throw_if_canceled(); - //B17 - // // adds tags for time estimators - // if (print.config().remaining_times.value) - // file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str()); + } + // adds tags for time estimators + if (print.config().remaining_times.value) + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str()); // Starting now, the G-code find / replace post-processor will be enabled. file.find_replace_enable(); @@ -1267,6 +1119,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Emit machine envelope limits for the Marlin firmware. this->print_machine_envelope(file, print); + // Label all objects so printer knows about them since the start. + m_label_objects.init(print); + file.write(m_label_objects.all_objects_header()); // Update output variables after the extruders were initialized. m_placeholder_parser_integration.init(m_writer); // Let the start-up script prime the 1st printing tool. @@ -1280,9 +1135,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // For the start / end G-code to do the priming and final filament pull in case there is no wipe tower provided. this->placeholder_parser().set("has_wipe_tower", has_wipe_tower); this->placeholder_parser().set("has_single_extruder_multi_material_priming", has_wipe_tower && print.config().single_extruder_multi_material_priming); - this->placeholder_parser().set("total_toolchanges", std::max(0, print.wipe_tower_data().number_of_toolchanges)); // Check for negative toolchanges (single extruder mode) and set to 0 (no tool change). + this->placeholder_parser().set("total_toolchanges", tool_ordering.toolchanges_count()); { BoundingBoxf bbox(print.config().bed_shape.values); + assert(bbox.defined); + if (! bbox.defined) + // This should not happen, but let's make the compiler happy. + bbox.min = bbox.max = Vec2d::Zero(); this->placeholder_parser().set("print_bed_min", new ConfigOptionFloats({ bbox.min.x(), bbox.min.y() })); this->placeholder_parser().set("print_bed_max", new ConfigOptionFloats({ bbox.max.x(), bbox.max.y() })); this->placeholder_parser().set("print_bed_size", new ConfigOptionFloats({ bbox.size().x(), bbox.size().y() })); @@ -1356,6 +1215,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write(this->set_extruder(initial_extruder_id, 0.)); } + GCode::SmoothPathCache smooth_path_cache_global = smooth_path_interpolate_global(print); // Do all objects for each layer. if (print.config().complete_objects.value) { size_t finished_objects = 0; @@ -1379,7 +1239,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // This happens before Z goes down to layer 0 again, so that no collision happens hopefully. m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); - file.write(this->retract()); + file.write(this->retract_and_wipe()); file.write(this->travel_to(Point(0, 0), ExtrusionRole::None, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. @@ -1400,7 +1260,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Process all layers of a single object instance (sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. - this->process_layers(print, tool_ordering, collect_layers_to_print(object), *print_object_instance_sequential_active - object.instances().data(), file); + this->process_layers(print, tool_ordering, collect_layers_to_print(object), + *print_object_instance_sequential_active - object.instances().data(), + smooth_path_cache_global, file); ++ finished_objects; // Flag indicating whether the nozzle temperature changes from 1st to 2nd layer were performed. // Reset it when starting another object from 1st layer. @@ -1413,7 +1275,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato std::vector> layers_to_print = collect_layers_to_print(print); // QIDI Multi-Material wipe tower. if (has_wipe_tower && ! layers_to_print.empty()) { - m_wipe_tower.reset(new WipeTowerIntegration(print.config(), *print.wipe_tower_data().priming.get(), print.wipe_tower_data().tool_changes, *print.wipe_tower_data().final_purge.get())); + m_wipe_tower = std::make_unique(print.config(), *print.wipe_tower_data().priming.get(), print.wipe_tower_data().tool_changes, *print.wipe_tower_data().final_purge.get()); file.write(m_writer.travel_to_z(first_layer_height + m_config.z_offset.value, "Move to the first layer height")); if (print.config().single_extruder_multi_material_priming) { file.write(m_wipe_tower->prime(*this)); @@ -1428,7 +1290,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato bool overlap = bbox_prime.overlap(bbox_print); if (print.config().gcode_flavor == gcfMarlinLegacy || print.config().gcode_flavor == gcfMarlinFirmware) { - file.write(this->retract()); + file.write(this->retract_and_wipe()); file.write("M300 S800 P500\n"); // Beep for 500ms, tone 800Hz. if (overlap) { // Wait for the user to remove the priming extrusions. @@ -1456,21 +1318,22 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Process all layers of all objects (non-sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. - this->process_layers(print, tool_ordering, print_object_instances_ordering, layers_to_print, file); + this->process_layers(print, tool_ordering, print_object_instances_ordering, layers_to_print, + smooth_path_cache_global, file); if (m_wipe_tower) // Purge the extruder, pull out the active filament. file.write(m_wipe_tower->finalize(*this)); } // Write end commands to file. - file.write(this->retract()); + file.write(this->retract_and_wipe()); - //B38 - { - std::string gcode; - m_writer.add_object_change_labels(gcode); - file.write(gcode); - } + ////B38 + //{ + // std::string gcode; + // m_writer.add_object_change_labels(gcode); + // file.write(gcode); + //} file.write(m_writer.set_fan(0)); @@ -1513,66 +1376,120 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato print.throw_if_canceled(); // Get filament stats. - file.write(DoExport::update_print_stats_and_format_filament_stats( + const std::string filament_stats_string_out = DoExport::update_print_stats_and_format_filament_stats( // Const inputs has_wipe_tower, print.wipe_tower_data(), this->config(), m_writer.extruders(), initial_extruder_id, + tool_ordering.toolchanges_count(), // Modifies - print.m_print_statistics)); + print.m_print_statistics, + export_to_binary_gcode, + m_processor.get_binary_data() + ); + + if (!export_to_binary_gcode) + file.write(filament_stats_string_out); + if (export_to_binary_gcode) { + bgcode::binarize::BinaryData& binary_data = m_processor.get_binary_data(); + if (print.m_print_statistics.total_toolchanges > 0) + binary_data.print_metadata.raw_data.emplace_back("total toolchanges", std::to_string(print.m_print_statistics.total_toolchanges)); + char buf[1024]; + sprintf(buf, "%.2lf", m_max_layer_z); + binary_data.printer_metadata.raw_data.emplace_back("max_layer_z", buf); + } + else { + // if exporting gcode in ascii format, statistics export is done here file.write("\n"); - file.write_format("; total filament used [g] = %.2lf\n", print.m_print_statistics.total_weight); - file.write_format("; total filament cost = %.2lf\n", print.m_print_statistics.total_cost); + file.write_format(PrintStatistics::TotalFilamentUsedGValueMask.c_str(), print.m_print_statistics.total_weight); + file.write_format(PrintStatistics::TotalFilamentCostValueMask.c_str(), print.m_print_statistics.total_cost); if (print.m_print_statistics.total_toolchanges > 0) file.write_format("; total toolchanges = %i\n", print.m_print_statistics.total_toolchanges); file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); // Append full config, delimited by two 'phony' configuration keys qidislicer_config = begin and qidislicer_config = end. // The delimiters are structured as configuration key / value pairs to be parsable by older versions of QIDISlicer G-code viewer. - { + { file.write("\n; qidislicer_config = begin\n"); std::string full_config; append_full_config(print, full_config); if (!full_config.empty()) file.write(full_config); file.write("; qidislicer_config = end\n"); + } } print.throw_if_canceled(); } +// Fill in cache of smooth paths for perimeters, fills and supports of the given object layers. +// Based on params, the paths are either decimated to sparser polylines, or interpolated with circular arches. +void GCodeGenerator::smooth_path_interpolate( + const ObjectLayerToPrint &object_layer_to_print, + const GCode::SmoothPathCache::InterpolationParameters ¶ms, + GCode::SmoothPathCache &out) +{ + if (const Layer *layer = object_layer_to_print.object_layer; layer) { + for (const LayerRegion *layerm : layer->regions()) { + out.interpolate_add(layerm->perimeters(), params); + out.interpolate_add(layerm->fills(), params); + } + } + if (const SupportLayer *layer = object_layer_to_print.support_layer; layer) + out.interpolate_add(layer->support_fills, params); +} // Process all layers of all objects (non-sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. -void GCode::process_layers( +void GCodeGenerator::process_layers( const Print &print, const ToolOrdering &tool_ordering, const std::vector &print_object_instances_ordering, const std::vector> &layers_to_print, + const GCode::SmoothPathCache &smooth_path_cache_global, GCodeOutputStream &output_stream) { - // The pipeline is variable: The vase mode filter is optional. size_t layer_to_print_idx = 0; - const auto generator = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print, &layer_to_print_idx](tbb::flow_control& fc) -> LayerResult { + const GCode::SmoothPathCache::InterpolationParameters interpolation_params = interpolation_parameters(print.config()); + const auto smooth_path_interpolator = tbb::make_filter>(slic3r_tbb_filtermode::serial_in_order, + [this, &print, &layers_to_print, &layer_to_print_idx, &interpolation_params](tbb::flow_control &fc) -> std::pair { if (layer_to_print_idx >= layers_to_print.size()) { - if ((!m_pressure_equalizer && layer_to_print_idx == layers_to_print.size()) || (m_pressure_equalizer && layer_to_print_idx == (layers_to_print.size() + 1))) { + if (layer_to_print_idx == layers_to_print.size() + (m_pressure_equalizer ? 1 : 0)) { fc.stop(); return {}; } else { // Pressure equalizer need insert empty input. Because it returns one layer back. // Insert NOP (no operation) layer; - ++layer_to_print_idx; - return LayerResult::make_nop_layer_result(); + return { layer_to_print_idx ++, {} }; } } else { - const std::pair &layer = layers_to_print[layer_to_print_idx++]; + print.throw_if_canceled(); + size_t idx = layer_to_print_idx ++; + GCode::SmoothPathCache smooth_path_cache; + for (const ObjectLayerToPrint &l : layers_to_print[idx].second) + GCodeGenerator::smooth_path_interpolate(l, interpolation_params, smooth_path_cache); + return { idx, std::move(smooth_path_cache) }; + } + }); + const auto generator = tbb::make_filter, LayerResult>(slic3r_tbb_filtermode::serial_in_order, + [this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print, &smooth_path_cache_global]( + std::pair in) -> LayerResult { + size_t layer_to_print_idx = in.first; + if (layer_to_print_idx == layers_to_print.size()) { + // Pressure equalizer need insert empty input. Because it returns one layer back. + // Insert NOP (no operation) layer; + return LayerResult::make_nop_layer_result(); + } else { + const std::pair &layer = layers_to_print[layer_to_print_idx]; const LayerTools& layer_tools = tool_ordering.tools_for_layer(layer.first); if (m_wipe_tower && layer_tools.has_wipe_tower) m_wipe_tower->next_layer(); print.throw_if_canceled(); - return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, size_t(-1)); + return this->process_layer(print, layer.second, layer_tools, + GCode::SmoothPathCaches{ smooth_path_cache_global, in.second }, + &layer == &layers_to_print.back(), &print_object_instances_ordering, size_t(-1)); } }); + // The pipeline is variable: The vase mode filter is optional. const auto spiral_vase = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [spiral_vase = this->m_spiral_vase.get()](LayerResult in) -> LayerResult { if (in.nop_layer_result) @@ -1600,61 +1517,73 @@ void GCode::process_layers( [&output_stream](std::string s) { output_stream.write(s); } ); + tbb::filter pipeline_to_layerresult = smooth_path_interpolator & generator; + if (m_spiral_vase) + pipeline_to_layerresult = pipeline_to_layerresult & spiral_vase; + if (m_pressure_equalizer) + pipeline_to_layerresult = pipeline_to_layerresult & pressure_equalizer; + + tbb::filter pipeline_to_string = cooling; + if (m_find_replace) + pipeline_to_string = pipeline_to_string & find_replace; // It registers a handler that sets locales to "C" before any TBB thread starts participating in tbb::parallel_pipeline. // Handler is unregistered when the destructor is called. TBBLocalesSetter locales_setter; // The pipeline elements are joined using const references, thus no copying is performed. output_stream.find_replace_supress(); - if (m_spiral_vase && m_find_replace && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & find_replace & output); - else if (m_spiral_vase && m_find_replace) - tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output); - else if (m_spiral_vase && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & output); - else if (m_find_replace && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & find_replace & output); - else if (m_spiral_vase) - tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output); - else if (m_find_replace) - tbb::parallel_pipeline(12, generator & cooling & find_replace & output); - else if (m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & output); - else - tbb::parallel_pipeline(12, generator & cooling & output); + tbb::parallel_pipeline(12, pipeline_to_layerresult & pipeline_to_string & output); output_stream.find_replace_enable(); } // Process all layers of a single object instance (sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. -void GCode::process_layers( +void GCodeGenerator::process_layers( const Print &print, const ToolOrdering &tool_ordering, ObjectsLayerToPrint layers_to_print, const size_t single_object_idx, + const GCode::SmoothPathCache &smooth_path_cache_global, GCodeOutputStream &output_stream) { - // The pipeline is variable: The vase mode filter is optional. size_t layer_to_print_idx = 0; - const auto generator = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [this, &print, &tool_ordering, &layers_to_print, &layer_to_print_idx, single_object_idx](tbb::flow_control& fc) -> LayerResult { + const GCode::SmoothPathCache::InterpolationParameters interpolation_params = interpolation_parameters(print.config()); + const auto smooth_path_interpolator = tbb::make_filter> (slic3r_tbb_filtermode::serial_in_order, + [this, &print, &layers_to_print, &layer_to_print_idx, interpolation_params](tbb::flow_control &fc) -> std::pair { if (layer_to_print_idx >= layers_to_print.size()) { - if ((!m_pressure_equalizer && layer_to_print_idx == layers_to_print.size()) || (m_pressure_equalizer && layer_to_print_idx == (layers_to_print.size() + 1))) { + if (layer_to_print_idx == layers_to_print.size() + (m_pressure_equalizer ? 1 : 0)) { fc.stop(); return {}; } else { // Pressure equalizer need insert empty input. Because it returns one layer back. // Insert NOP (no operation) layer; - ++layer_to_print_idx; - return LayerResult::make_nop_layer_result(); + return { layer_to_print_idx ++, {} }; } } else { - ObjectLayerToPrint &layer = layers_to_print[layer_to_print_idx ++]; print.throw_if_canceled(); - return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, single_object_idx); + size_t idx = layer_to_print_idx ++; + GCode::SmoothPathCache smooth_path_cache; + GCodeGenerator::smooth_path_interpolate(layers_to_print[idx], interpolation_params, smooth_path_cache); + return { idx, std::move(smooth_path_cache) }; } }); + const auto generator = tbb::make_filter, LayerResult>(slic3r_tbb_filtermode::serial_in_order, + [this, &print, &tool_ordering, &layers_to_print, &smooth_path_cache_global, single_object_idx](std::pair in) -> LayerResult { + size_t layer_to_print_idx = in.first; + if (layer_to_print_idx == layers_to_print.size()) { + // Pressure equalizer need insert empty input. Because it returns one layer back. + // Insert NOP (no operation) layer; + return LayerResult::make_nop_layer_result(); + } else { + ObjectLayerToPrint &layer = layers_to_print[layer_to_print_idx]; + print.throw_if_canceled(); + return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), + GCode::SmoothPathCaches{ smooth_path_cache_global, in.second }, + &layer == &layers_to_print.back(), nullptr, single_object_idx); + } + }); + // The pipeline is variable: The vase mode filter is optional. const auto spiral_vase = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [spiral_vase = this->m_spiral_vase.get()](LayerResult in)->LayerResult { if (in.nop_layer_result) @@ -1680,37 +1609,56 @@ void GCode::process_layers( [&output_stream](std::string s) { output_stream.write(s); } ); + tbb::filter pipeline_to_layerresult = smooth_path_interpolator & generator; + if (m_spiral_vase) + pipeline_to_layerresult = pipeline_to_layerresult & spiral_vase; + if (m_pressure_equalizer) + pipeline_to_layerresult = pipeline_to_layerresult & pressure_equalizer; + + tbb::filter pipeline_to_string = cooling; + if (m_find_replace) + pipeline_to_string = pipeline_to_string & find_replace; // It registers a handler that sets locales to "C" before any TBB thread starts participating in tbb::parallel_pipeline. // Handler is unregistered when the destructor is called. TBBLocalesSetter locales_setter; // The pipeline elements are joined using const references, thus no copying is performed. output_stream.find_replace_supress(); - if (m_spiral_vase && m_find_replace && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & find_replace & output); - else if (m_spiral_vase && m_find_replace) - tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output); - else if (m_spiral_vase && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & spiral_vase & pressure_equalizer & cooling & output); - else if (m_find_replace && m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & find_replace & output); - else if (m_spiral_vase) - tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output); - else if (m_find_replace) - tbb::parallel_pipeline(12, generator & cooling & find_replace & output); - else if (m_pressure_equalizer) - tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & output); - else - tbb::parallel_pipeline(12, generator & cooling & output); + tbb::parallel_pipeline(12, pipeline_to_layerresult & pipeline_to_string & output); output_stream.find_replace_enable(); } -std::string GCode::placeholder_parser_process( +std::string GCodeGenerator::placeholder_parser_process( const std::string &name, const std::string &templ, unsigned int current_extruder_id, const DynamicConfig *config_override) { +#ifndef NDEBUG // CHECK_CUSTOM_GCODE_PLACEHOLDERS + if (config_override) { + const auto& custom_gcode_placeholders = custom_gcode_specific_placeholders(); + + // 1-st check: custom G-code "name" have to be present in s_CustomGcodeSpecificOptions; + //if (custom_gcode_placeholders.count(name) > 0) { + // const auto& placeholders = custom_gcode_placeholders.at(name); + if (auto it = custom_gcode_placeholders.find(name); it != custom_gcode_placeholders.end()) { + const auto& placeholders = it->second; + + for (const std::string& key : config_override->keys()) { + // 2-nd check: "key" have to be present in s_CustomGcodeSpecificOptions for "name" custom G-code ; + if (std::find(placeholders.begin(), placeholders.end(), key) == placeholders.end()) + throw Slic3r::PlaceholderParserError(format("\"%s\" placeholder for \"%s\" custom G-code \n" + "needs to be added to s_CustomGcodeSpecificOptions", key.c_str(), name.c_str())); + // 3-rd check: "key" have to be present in CustomGcodeSpecificConfigDef for "key" placeholder; + if (!custom_gcode_specific_config_def.has(key)) + throw Slic3r::PlaceholderParserError(format("Definition of \"%s\" placeholder \n" + "needs to be added to CustomGcodeSpecificConfigDef", key.c_str())); + } + } + else + throw Slic3r::PlaceholderParserError(format("\"%s\" custom G-code needs to be added to s_CustomGcodeSpecificOptions", name.c_str())); + } +#endif PlaceholderParserIntegration &ppi = m_placeholder_parser_integration; try { ppi.update_from_gcodewriter(m_writer); @@ -1819,7 +1767,7 @@ static bool custom_gcode_sets_temperature(const std::string &gcode, const int mc // Print the machine envelope G-code for the Marlin firmware based on the "machine_max_xxx" parameters. // Do not process this piece of G-code by the time estimator, it already knows the values through another sources. -void GCode::print_machine_envelope(GCodeOutputStream &file, Print &print) +void GCodeGenerator::print_machine_envelope(GCodeOutputStream &file, Print &print) { const GCodeFlavor flavor = print.config().gcode_flavor.value; if ( (flavor == gcfMarlinLegacy || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware) @@ -1880,7 +1828,7 @@ void GCode::print_machine_envelope(GCodeOutputStream &file, Print &print) // Only do that if the start G-code does not already contain any M-code controlling an extruder temperature. // M140 - Set Extruder Temperature // M190 - Set Extruder Temperature and Wait -void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) +void GCodeGenerator::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) { bool autoemit = print.config().autoemit_temperature_commands; // Initial bed temperature based on the first extruder. @@ -1902,7 +1850,7 @@ void GCode::_print_first_layer_bed_temperature(GCodeOutputStream &file, Print &p // M104 - Set Extruder Temperature // M109 - Set Extruder Temperature and Wait // RepRapFirmware: G10 Sxx -void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) +void GCodeGenerator::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait) { bool autoemit = print.config().autoemit_temperature_commands; // Is the bed temperature set by the provided custom G-code? @@ -1940,7 +1888,7 @@ void GCode::_print_first_layer_extruder_temperatures(GCodeOutputStream &file, Pr } } -std::vector GCode::sort_print_object_instances( +std::vector GCodeGenerator::sort_print_object_instances( const std::vector &object_layers, // Ordering must be defined for normal (non-sequential print). const std::vector *ordering, @@ -1981,7 +1929,7 @@ namespace ProcessLayer { static std::string emit_custom_gcode_per_print_z( - GCode &gcodegen, + GCodeGenerator &gcodegen, const CustomGCode::Item *custom_gcode, unsigned int current_extruder_id, // ID of the first extruder printing this layer. @@ -2129,16 +2077,31 @@ namespace Skirt { } // namespace Skirt +bool GCodeGenerator::line_distancer_is_required(const std::vector& extruder_ids) { + for (const unsigned id : extruder_ids) { + const double travel_slope{this->m_config.travel_slope.get_at(id)}; + if ( + this->m_config.travel_lift_before_obstacle.get_at(id) + && this->m_config.travel_max_lift.get_at(id) > 0 + && travel_slope > 0 + && travel_slope < 90 + ) { + return true; + } + } + return false; +} // In sequential mode, process_layer is called once per each object and its copy, // therefore layers will contain a single entry and single_object_instance_idx will point to the copy of the object. // In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated. // For multi-material prints, this routine minimizes extruder switches by gathering extruder specific extrusion paths // and performing the extruder specific extrusions together. -LayerResult GCode::process_layer( +LayerResult GCodeGenerator::process_layer( const Print &print, // Set of object & print layers of the same PrintObject and with the same print_z. const ObjectsLayerToPrint &layers, const LayerTools &layer_tools, + const GCode::SmoothPathCaches &smooth_path_caches, const bool last_layer, // Pairs of PrintObject index and its instance index. const std::vector *ordering, @@ -2214,6 +2177,7 @@ LayerResult GCode::process_layer( + float_to_string_decimal_point(height) + "\n"; // update caches + const coordf_t previous_layer_z{m_last_layer_z}; m_last_layer_z = static_cast(print_z); m_max_layer_z = std::max(m_max_layer_z, m_last_layer_z); m_last_height = height; @@ -2228,8 +2192,11 @@ LayerResult GCode::process_layer( print.config().before_layer_gcode.value, m_writer.extruder()->id(), &config) + "\n"; } - gcode += this->change_layer(print_z); // this will increase m_layer_index + gcode += this->change_layer(previous_layer_z, print_z); // this will increase m_layer_index m_layer = &layer; + if (this->line_distancer_is_required(layer_tools.extruders) && this->m_layer != nullptr && this->m_layer->lower_layer != nullptr) { + this->m_previous_layer_distancer = GCode::Impl::get_expolygons_distancer(m_layer->lower_layer->lslices); + } m_object_layer_over_raft = false; if (! print.config().layer_gcode.value.empty()) { DynamicConfig config; @@ -2287,9 +2254,6 @@ LayerResult GCode::process_layer( } } - for (const ObjectLayerToPrint &layer_to_print : layers) { - m_extrusion_quality_estimator.prepare_for_new_layer(layer_to_print.object_layer); - } // Extrude the skirt, brim, support, perimeters, infill ordered by the extruders. for (unsigned int extruder_id : layer_tools.extruders) @@ -2310,13 +2274,11 @@ LayerResult GCode::process_layer( double mm3_per_mm = layer_skirt_flow.mm3_per_mm(); for (size_t i = loops.first; i < loops.second; ++i) { // Adjust flow according to this layer's layer height. - ExtrusionLoop loop = *dynamic_cast(print.skirt().entities[i]); - for (ExtrusionPath &path : loop.paths) { - path.height = layer_skirt_flow.height(); - path.mm3_per_mm = mm3_per_mm; - } //FIXME using the support_material_speed of the 1st object printed. - gcode += this->extrude_loop(loop, "skirt"sv, m_config.support_material_speed.value); + gcode += this->extrude_skirt(dynamic_cast(*print.skirt().entities[i]), + // Override of skirt extrusion parameters. extrude_skirt() will fill in the extrusion width. + ExtrusionFlow{ mm3_per_mm, 0., layer_skirt_flow.height() }, + smooth_path_caches.global(), "skirt"sv, m_config.support_material_speed.value); } m_avoid_crossing_perimeters.use_external_mp(false); // Allow a straight travel move to the first object point if this is the first layer (but don't in next layers). @@ -2328,9 +2290,8 @@ LayerResult GCode::process_layer( if (! m_brim_done) { this->set_origin(0., 0.); m_avoid_crossing_perimeters.use_external_mp(); - for (const ExtrusionEntity *ee : print.brim().entities) { - gcode += this->extrude_entity(*ee, "brim"sv, m_config.support_material_speed.value); - } + for (const ExtrusionEntity *ee : print.brim().entities) + gcode += this->extrude_entity({ *ee, false }, smooth_path_caches.global(), "brim"sv, m_config.support_material_speed.value); m_brim_done = true; m_avoid_crossing_perimeters.use_external_mp(false); // Allow a straight travel move to the first object point. @@ -2347,7 +2308,7 @@ LayerResult GCode::process_layer( for (const InstanceToPrint &instance : instances_to_print) this->process_layer_single_object( gcode, extruder_id, instance, - layers[instance.object_layer_to_print_id], layer_tools, + layers[instance.object_layer_to_print_id], layer_tools, smooth_path_caches.layer_local(), is_anything_overridden, true /* print_wipe_extrusions */); if (gcode_size_old < gcode.size()) gcode+="; PURGING FINISHED\n"; @@ -2356,7 +2317,7 @@ LayerResult GCode::process_layer( for (const InstanceToPrint &instance : instances_to_print) this->process_layer_single_object( gcode, extruder_id, instance, - layers[instance.object_layer_to_print_id], layer_tools, + layers[instance.object_layer_to_print_id], layer_tools, smooth_path_caches.layer_local(), is_anything_overridden, false /* print_wipe_extrusions */); } @@ -2374,7 +2335,7 @@ static inline bool comment_is_perimeter(const std::string_view comment) { return comment.data() == comment_perimeter.data() && comment.size() == comment_perimeter.size(); } -void GCode::process_layer_single_object( +void GCodeGenerator::process_layer_single_object( // output std::string &gcode, // Index of the extruder currently active. @@ -2385,15 +2346,17 @@ void GCode::process_layer_single_object( const ObjectLayerToPrint &layer_to_print, // Container for extruder overrides (when wiping into object or infill). const LayerTools &layer_tools, + // Optional smooth path interpolating extrusion polylines. + const GCode::SmoothPathCache &smooth_path_cache, // Is any extrusion possibly marked as wiping extrusion? const bool is_anything_overridden, // Round 1 (wiping into object or infill) or round 2 (normal extrusions). const bool print_wipe_extrusions) { bool first = true; - int object_id = 0; + int object_id = 0; // Delay layer initialization as many layers may not print with all extruders. - auto init_layer_delayed = [this, &print_instance, &layer_to_print, &first, &object_id, &gcode]() { + auto init_layer_delayed = [this, &print_instance, &layer_to_print, &first,&object_id, & gcode]() { if (first) { first = false; const PrintObject &print_object = print_instance.print_object; @@ -2409,7 +2372,7 @@ void GCode::process_layer_single_object( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - if (this->config().gcode_label_objects) { + if ((this->config().gcode_label_objects) != LabelObjectsStyle::Disabled ) { for (const PrintObject *po : print_object.print()->objects()) if (po == &print_object) break; @@ -2435,7 +2398,6 @@ void GCode::process_layer_single_object( const PrintObject &print_object = print_instance.print_object; const Print &print = *print_object.print(); - m_extrusion_quality_estimator.set_current_object(&print_object); if (! print_wipe_extrusions && layer_to_print.support_layer != nullptr) if (const SupportLayer &support_layer = *layer_to_print.support_layer; ! support_layer.support_fills.entities.empty()) { @@ -2469,9 +2431,16 @@ void GCode::process_layer_single_object( init_layer_delayed(); m_layer = layer_to_print.support_layer; m_object_layer_over_raft = false; - gcode += this->extrude_support( - // support_extrusion_role is ExtrusionRole::SupportMaterial, ExtrusionRole::SupportMaterialInterface or ExtrusionRole::Mixed for all extrusion paths. - support_layer.support_fills.chained_path_from(m_last_pos, extrude_support ? (extrude_interface ? ExtrusionRole::Mixed : ExtrusionRole::SupportMaterial) : ExtrusionRole::SupportMaterialInterface)); + ExtrusionEntitiesPtr entities_cache; + const ExtrusionEntitiesPtr &entities = extrude_support && extrude_interface ? support_layer.support_fills.entities : entities_cache; + if (! extrude_support || ! extrude_interface) { + auto role = extrude_support ? ExtrusionRole::SupportMaterial : ExtrusionRole::SupportMaterialInterface; + entities_cache.reserve(support_layer.support_fills.entities.size()); + for (ExtrusionEntity *ee : support_layer.support_fills.entities) + if (ee->role() == role) + entities_cache.emplace_back(ee); + } + gcode += this->extrude_support(chain_extrusion_references(entities), smooth_path_cache); } } @@ -2529,16 +2498,13 @@ void GCode::process_layer_single_object( if (! temp_fill_extrusions.empty()) { init_layer_delayed(); m_config.apply(region.config()); - //FIXME The source extrusions may be reversed, thus modifying the extrusions! Is it a problem? How about the initial G-code preview? - // Will parallel access of initial G-code preview to these extrusions while reordering them at backend cause issues? - chain_and_reorder_extrusion_entities(temp_fill_extrusions, &m_last_pos); const auto extrusion_name = ironing ? "ironing"sv : "infill"sv; - for (const ExtrusionEntity *fill : temp_fill_extrusions) - if (auto *eec = dynamic_cast(fill); eec) { - for (const ExtrusionEntity *ee : eec->chained_path_from(m_last_pos).entities) - gcode += this->extrude_entity(*ee, extrusion_name); + for (const ExtrusionEntityReference &fill : chain_extrusion_references(temp_fill_extrusions, &m_last_pos)) + if (auto *eec = dynamic_cast(&fill.extrusion_entity()); eec) { + for (const ExtrusionEntityReference &ee : chain_extrusion_references(*eec, &m_last_pos, fill.flipped())) + gcode += this->extrude_entity(ee, smooth_path_cache, extrusion_name); } else - gcode += this->extrude_entity(*fill, extrusion_name); + gcode += this->extrude_entity(fill, smooth_path_cache, extrusion_name); } }; @@ -2552,6 +2518,8 @@ void GCode::process_layer_single_object( const PrintRegion ®ion = print.get_print_region(layerm.region().print_region_id()); bool first = true; for (uint32_t perimeter_id : island.perimeters) { + // Extrusions inside islands are expected to be ordered already. + // Don't reorder them. assert(dynamic_cast(layerm.perimeters().entities[perimeter_id])); if (const auto *eec = static_cast(layerm.perimeters().entities[perimeter_id]); shall_print_this_extrusion_collection(eec, region)) { @@ -2563,7 +2531,8 @@ void GCode::process_layer_single_object( m_config.apply(region.config()); } for (const ExtrusionEntity *ee : *eec) - gcode += this->extrude_entity(*ee, comment_perimeter, -1.); + // Don't reorder, don't flip. + gcode += this->extrude_entity({ *ee, false }, smooth_path_cache, comment_perimeter, -1.); } } }; @@ -2601,7 +2570,7 @@ void GCode::process_layer_single_object( } } - if (!first && this->config().gcode_label_objects) { + if (!first && ((this->config().gcode_label_objects) != LabelObjectsStyle::Disabled)) { //B38 //B41 if (this->config().gcode_flavor == gcfKlipper) { if (!m_writer.is_object_start_str_empty()) { @@ -2623,14 +2592,23 @@ void GCode::process_layer_single_object( } -void GCode::apply_print_config(const PrintConfig &print_config) +void GCodeGenerator::apply_print_config(const PrintConfig &print_config) { m_writer.apply_print_config(print_config); m_config.apply(print_config); m_scaled_resolution = scaled(print_config.gcode_resolution.value); } -void GCode::append_full_config(const Print &print, std::string &str) +void GCodeGenerator::append_full_config(const Print &print, std::string &str) +{ + std::vector> config; + encode_full_config(print, config); + for (const auto& [key, value] : config) { + str += "; " + key + " = " + value + "\n"; + } +} + +void GCodeGenerator::encode_full_config(const Print& print, std::vector>& config) { const DynamicPrintConfig &cfg = print.full_print_config(); // Sorted list of config keys, which shall not be stored into the G-code. Initializer list. @@ -2646,37 +2624,31 @@ void GCode::append_full_config(const Print &print, std::string &str) auto is_banned = [](const std::string &key) { return std::binary_search(banned_keys.begin(), banned_keys.end(), key); }; - for (const std::string &key : cfg.keys()) + config.reserve(config.size() + cfg.keys().size()); + for (const std::string& key : cfg.keys()) { if (! is_banned(key) && ! cfg.option(key)->is_nil()) - str += "; " + key + " = " + cfg.opt_serialize(key) + "\n"; + config.emplace_back(key, cfg.opt_serialize(key)); + } + config.shrink_to_fit(); } -void GCode::set_extruders(const std::vector &extruder_ids) +void GCodeGenerator::set_extruders(const std::vector &extruder_ids) { m_writer.set_extruders(extruder_ids); - // enable wipe path generation if any extruder has wipe enabled - m_wipe.enable = false; - for (auto id : extruder_ids) - if (m_config.wipe.get_at(id)) { - m_wipe.enable = true; - break; - } + m_wipe.init(this->config(), extruder_ids); } -void GCode::set_origin(const Vec2d &pointf) +void GCodeGenerator::set_origin(const Vec2d &pointf) { // if origin increases (goes towards right), last_pos decreases because it goes towards left - const Point translate( - scale_(m_origin(0) - pointf(0)), - scale_(m_origin(1) - pointf(1)) - ); - m_last_pos += translate; - m_wipe.path.translate(translate); + const auto offset = Point::new_scale(m_origin - pointf); + m_last_pos += offset; + m_wipe.offset_path(offset); m_origin = pointf; } -std::string GCode::preamble() +std::string GCodeGenerator::preamble() { std::string gcode = m_writer.preamble(); @@ -2689,226 +2661,319 @@ std::string GCode::preamble() return gcode; } -// called by GCode::process_layer() -std::string GCode::change_layer(coordf_t print_z) +namespace GCode::Impl { +Polygon generate_regular_polygon( + const Point& centroid, + const Point& start_point, + const unsigned points_count +) { + Points points; + points.reserve(points_count); + const double part_angle{2*M_PI / points_count}; + for (unsigned i = 0; i < points_count; ++i) { + const double current_angle{i * part_angle}; + points.emplace_back(scaled(std::cos(current_angle)), scaled(std::sin(current_angle))); + } + + Polygon regular_polygon{points}; + const Vec2d current_vector{unscaled(regular_polygon.points.front())}; + const Vec2d expected_vector{unscaled(start_point) - unscaled(centroid)}; + + const double current_scale = current_vector.norm(); + const double expected_scale = expected_vector.norm(); + regular_polygon.scale(expected_scale / current_scale); + + regular_polygon.rotate(angle(current_vector, expected_vector)); + + regular_polygon.translate(centroid); + + return regular_polygon; +} +Bed::Bed(const std::vector& shape, const double padding): + inner_offset(get_inner_offset(shape, padding)), + centroid(unscaled(inner_offset.centroid())) +{} + +bool Bed::contains_within_padding(const Vec2d& point) const { + return inner_offset.contains(scaled(point)); +} +Polygon Bed::get_inner_offset(const std::vector& shape, const double padding) { + Points shape_scaled; + shape_scaled.reserve(shape.size()); + using std::begin, std::end, std::back_inserter, std::transform; + transform(begin(shape), end(shape), back_inserter(shape_scaled), [](const Vec2d& point){ + return scaled(point); + }); + return shrink({Polygon{shape_scaled}}, scaled(padding)).front(); +} + } + +std::optional GCodeGenerator::get_helical_layer_change_gcode( + const coordf_t previous_layer_z, + const coordf_t print_z, + const std::string& comment +) { + + if (!this->last_pos_defined()) { + return std::nullopt; + } + //B38 + //m_writer.add_object_change_labels(gcode); + const double circle_radius{2}; + const unsigned n_gon_points_count{16}; + + const Point n_gon_start_point{this->last_pos()}; + + static GCode::Impl::Bed bed{ + this->m_config.bed_shape.values, + circle_radius + }; + if (!bed.contains_within_padding(this->point_to_gcode(n_gon_start_point))) { + return std::nullopt; + } + + const Point n_gon_centeroid{ + n_gon_start_point + + scaled(Vec2d{ + (bed.centroid - unscaled(n_gon_start_point)).normalized() + * circle_radius + }) + }; + + const Polygon n_gon{GCode::Impl::generate_regular_polygon( + n_gon_centeroid, + n_gon_start_point, + n_gon_points_count + )}; + + const double n_gon_circumference = unscaled(n_gon.length()); + + const double z_change{print_z - previous_layer_z}; + Points3 helix{GCode::Impl::generate_elevated_travel( + n_gon.points, + {}, + previous_layer_z, + [&](const double distance){ + return distance / n_gon_circumference * z_change; + } + )}; + helix.emplace_back(to_3d(this->last_pos(), scaled(print_z))); + + return this->generate_travel_gcode(helix, comment); +} + +// called by GCodeGenerator::process_layer() +std::string GCodeGenerator::change_layer(coordf_t previous_layer_z, coordf_t print_z) { std::string gcode; if (m_layer_count > 0) // Increment a progress bar indicator. gcode += m_writer.update_progress(++ m_layer_index, m_layer_count); - coordf_t z = print_z + m_config.z_offset.value; // in unscaled coordinates - if (EXTRUDER_CONFIG(retract_layer_change) && m_writer.will_move_z(z)) { - gcode += this->retract(); - } - //B38 - m_writer.add_object_change_labels(gcode); + if (EXTRUDER_CONFIG(retract_layer_change)) + gcode += this->retract_and_wipe(); + const std::string comment{"move to next layer (" + std::to_string(m_layer_index) + ")"}; - { - std::ostringstream comment; - comment << "move to next layer (" << m_layer_index << ")"; - gcode += m_writer.travel_to_z(z, comment.str()); - } + bool helical_layer_change{ + (!this->m_spiral_vase || !this->m_spiral_vase->is_enabled()) + && print_z > previous_layer_z + && EXTRUDER_CONFIG(travel_ramping_lift) + && EXTRUDER_CONFIG(travel_slope) > 0 && EXTRUDER_CONFIG(travel_slope) < 90 + }; + const std::optional helix_gcode{ + helical_layer_change ? + this->get_helical_layer_change_gcode( + m_config.z_offset.value + previous_layer_z, + m_config.z_offset.value + print_z, + comment + ) : + std::nullopt + }; + gcode += ( + helix_gcode ? + *helix_gcode : + m_writer.travel_to_z(m_config.z_offset.value + print_z, comment) + ); // forget last wiping path as wiping after raising Z is pointless m_wipe.reset_path(); - return gcode; } -std::string GCode::extrude_loop(ExtrusionLoop loop, const std::string_view description, double speed) +#ifndef NDEBUG +static inline bool validate_smooth_path(const GCode::SmoothPath &smooth_path, bool loop) +{ + for (auto it = std::next(smooth_path.begin()); it != smooth_path.end(); ++ it) { + assert(it->path.size() >= 2); + assert(std::prev(it)->path.back().point == it->path.front().point); + } + assert(! loop || smooth_path.front().path.front().point == smooth_path.back().path.back().point); + return true; +} +#endif //NDEBUG +static constexpr const double min_gcode_segment_length = 0.002; +std::string GCodeGenerator::extrude_loop(const ExtrusionLoop &loop_src, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed) { - // get a copy; don't modify the orientation of the original loop object otherwise - // next copies (if any) would not detect the correct orientation - - // extrude all loops ccw - bool was_clockwise = loop.make_counter_clockwise(); - - // find the point of the loop that is closest to the current extruder position - // or randomize if requested - Point last_pos = this->last_pos(); + // Extrude all loops CCW. + bool is_hole = loop_src.is_clockwise(); + Point seam_point = this->last_pos(); if (! m_config.spiral_vase && comment_is_perimeter(description)) { assert(m_layer != nullptr); - m_seam_placer.place_seam(m_layer, loop, m_config.external_perimeters_first, this->last_pos()); - } else + seam_point = m_seam_placer.place_seam(m_layer, loop_src, m_config.external_perimeters_first, this->last_pos()); + } // Because the G-code export has 1um resolution, don't generate segments shorter than 1.5 microns, // thus empty path segments will not be produced by G-code export. - loop.split_at(last_pos, false, scaled(0.0015)); + GCode::SmoothPath smooth_path = smooth_path_cache.resolve_or_fit_split_with_seam( + loop_src, is_hole, m_scaled_resolution, seam_point, scaled(0.0015)); - for (auto it = std::next(loop.paths.begin()); it != loop.paths.end(); ++it) { - assert(it->polyline.points.size() >= 2); - assert(std::prev(it)->polyline.last_point() == it->polyline.first_point()); - } - assert(loop.paths.front().first_point() == loop.paths.back().last_point()); - - // clip the path to avoid the extruder to get exactly on the first point of the loop; + // Clip the path to avoid the extruder to get exactly on the first point of the loop; // if polyline was shorter than the clipping distance we'd get a null polyline, so - // we discard it in that case - double clip_length = m_enable_loop_clipping ? - scale_(EXTRUDER_CONFIG(nozzle_diameter)) * LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER : - 0; + // we discard it in that case. + if (m_enable_loop_clipping) + clip_end(smooth_path, scaled(EXTRUDER_CONFIG(nozzle_diameter)) * LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER, scaled(min_gcode_segment_length)); - // get paths - ExtrusionPaths paths; - loop.clip_end(clip_length, &paths); - if (paths.empty()) return ""; + if (smooth_path.empty()) + return {}; + assert(validate_smooth_path(smooth_path, ! m_enable_loop_clipping)); - // apply the small perimeter speed - if (paths.front().role().is_perimeter() && loop.length() <= SMALL_PERIMETER_LENGTH && speed == -1) + // Apply the small perimeter speed. + if (loop_src.paths.front().role().is_perimeter() && loop_src.length() <= SMALL_PERIMETER_LENGTH && speed == -1) speed = m_config.small_perimeter_speed.get_abs_value(m_config.perimeter_speed); - // extrude along the path + // Extrude along the smooth path. std::string gcode; - for (ExtrusionPath &path : paths) { - path.simplify(m_scaled_resolution); - gcode += this->_extrude(path, description, speed); - } + for (const GCode::SmoothPathElement &el : smooth_path) + gcode += this->_extrude(el.path_attributes, el.path, description, speed); // reset acceleration - gcode += m_writer.set_print_acceleration((unsigned int)(m_config.default_acceleration.value + 0.5)); + gcode += m_writer.set_print_acceleration(fast_round_up(m_config.default_acceleration.value)); - if (m_wipe.enable) { - m_wipe.path = paths.front().polyline; + if (m_wipe.enabled()) { - for (auto it = std::next(paths.begin()); it != paths.end(); ++it) { - if (it->role().is_bridge()) - break; // Don't perform a wipe on bridges. + // Wipe will hide the seam. + m_wipe.set_path(std::move(smooth_path), false); + } else if (loop_src.paths.back().role().is_external_perimeter() && m_layer != nullptr && m_config.perimeters.value > 1) { - assert(it->polyline.points.size() >= 2); - assert(m_wipe.path.points.back() == it->polyline.first_point()); - if (m_wipe.path.points.back() != it->polyline.first_point()) - break; // ExtrusionLoop is interrupted in some place. + // Only wipe inside if the wipe along the perimeter is disabled. + // Make a little move inwards before leaving loop. + if (std::optional pt = wipe_hide_seam(smooth_path, is_hole, scale_(EXTRUDER_CONFIG(nozzle_diameter))); pt) { - m_wipe.path.points.insert(m_wipe.path.points.end(), it->polyline.points.begin() + 1, it->polyline.points.end()); + // Generate the seam hiding travel move. + gcode += m_writer.travel_to_xy(this->point_to_gcode(*pt), "move inwards before travel"); + this->set_last_pos(*pt); } } - // make a little move inwards before leaving loop - if (paths.back().role().is_external_perimeter() && m_layer != NULL && m_config.perimeters.value > 1 && paths.front().size() >= 2 && paths.back().polyline.points.size() >= 3) { - // detect angle between last and first segment - // the side depends on the original winding order of the polygon (left for contours, right for holes) - //FIXME improve the algorithm in case the loop is tiny. - //FIXME improve the algorithm in case the loop is split into segments with a low number of points (see the Point b query). - // Angle from the 2nd point to the last point. - double angle_inside = angle(paths.front().polyline.points[1] - paths.front().first_point(), - *(paths.back().polyline.points.end()-3) - paths.front().first_point()); - assert(angle_inside >= -M_PI && angle_inside <= M_PI); - // 3rd of this angle will be taken, thus make the angle monotonic before interpolation. - if (was_clockwise) { - if (angle_inside > 0) - angle_inside -= 2.0 * M_PI; - } else { - if (angle_inside < 0) - angle_inside += 2.0 * M_PI; - } - - // create the destination point along the first segment and rotate it - // we make sure we don't exceed the segment length because we don't know - // the rotation of the second segment so we might cross the object boundary - Vec2d p1 = paths.front().polyline.points.front().cast(); - Vec2d p2 = paths.front().polyline.points[1].cast(); - Vec2d v = p2 - p1; - double nd = scale_(EXTRUDER_CONFIG(nozzle_diameter)); - double l2 = v.squaredNorm(); - // Shift by no more than a nozzle diameter. - //FIXME Hiding the seams will not work nicely for very densely discretized contours! - Point pt = ((nd * nd >= l2) ? p2 : (p1 + v * (nd / sqrt(l2)))).cast(); - // Rotate pt inside around the seam point. - pt.rotate(angle_inside / 3., paths.front().polyline.points.front()); - // generate the travel move - gcode += m_writer.travel_to_xy(this->point_to_gcode(pt), "move inwards before travel"); - } - return gcode; } - -std::string GCode::extrude_multi_path(ExtrusionMultiPath multipath, const std::string_view description, double speed) +std::string GCodeGenerator::extrude_skirt( + const ExtrusionLoop &loop_src, const ExtrusionFlow &extrusion_flow_override, + const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed) { + assert(loop_src.is_counter_clockwise()); + GCode::SmoothPath smooth_path = smooth_path_cache.resolve_or_fit_split_with_seam( + loop_src, false, m_scaled_resolution, this->last_pos(), scaled(0.0015)); + // Clip the path to avoid the extruder to get exactly on the first point of the loop; + // if polyline was shorter than the clipping distance we'd get a null polyline, so + // we discard it in that case. + if (m_enable_loop_clipping) + clip_end(smooth_path, scale_(EXTRUDER_CONFIG(nozzle_diameter)) * LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER, scaled(min_gcode_segment_length)); + if (smooth_path.empty()) + return {}; + assert(validate_smooth_path(smooth_path, ! m_enable_loop_clipping)); + + // Extrude along the smooth path. + std::string gcode; + for (GCode::SmoothPathElement &el : smooth_path) { + // Override extrusion parameters. + el.path_attributes.mm3_per_mm = extrusion_flow_override.mm3_per_mm; + el.path_attributes.height = extrusion_flow_override.height; + gcode += this->_extrude(el.path_attributes, el.path, description, speed); + } + // reset acceleration + gcode += m_writer.set_print_acceleration(fast_round_up(m_config.default_acceleration.value)); + if (m_wipe.enabled()) + // Wipe will hide the seam. + m_wipe.set_path(std::move(smooth_path), false); + + return gcode; +} + +std::string GCodeGenerator::extrude_multi_path(const ExtrusionMultiPath &multipath, bool reverse, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed) +{ +#ifndef NDEBUG for (auto it = std::next(multipath.paths.begin()); it != multipath.paths.end(); ++it) { assert(it->polyline.points.size() >= 2); assert(std::prev(it)->polyline.last_point() == it->polyline.first_point()); } +#endif // NDEBUG + GCode::SmoothPath smooth_path = smooth_path_cache.resolve_or_fit(multipath, reverse, m_scaled_resolution); // extrude along the path + std::string gcode; - for (ExtrusionPath path : multipath.paths) { - path.simplify(m_scaled_resolution); - gcode += this->_extrude(path, description, speed); - } - if (m_wipe.enable) { - m_wipe.path = std::move(multipath.paths.back().polyline); - m_wipe.path.reverse(); - - for (auto it = std::next(multipath.paths.rbegin()); it != multipath.paths.rend(); ++it) { - if (it->role().is_bridge()) - break; // Do not perform a wipe on bridges. - - assert(it->polyline.points.size() >= 2); - assert(m_wipe.path.points.back() == it->polyline.last_point()); - if (m_wipe.path.points.back() != it->polyline.last_point()) - break; // ExtrusionMultiPath is interrupted in some place. - - m_wipe.path.points.insert(m_wipe.path.points.end(), it->polyline.points.rbegin() + 1, it->polyline.points.rend()); - } - } + for (GCode::SmoothPathElement &el : smooth_path) + gcode += this->_extrude(el.path_attributes, el.path, description, speed); + m_wipe.set_path(std::move(smooth_path), true); // reset acceleration gcode += m_writer.set_print_acceleration((unsigned int)floor(m_config.default_acceleration.value + 0.5)); return gcode; } -std::string GCode::extrude_entity(const ExtrusionEntity &entity, const std::string_view description, double speed) +std::string GCodeGenerator::extrude_entity(const ExtrusionEntityReference &entity, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed) { - if (const ExtrusionPath* path = dynamic_cast(&entity)) - return this->extrude_path(*path, description, speed); - else if (const ExtrusionMultiPath* multipath = dynamic_cast(&entity)) - return this->extrude_multi_path(*multipath, description, speed); - else if (const ExtrusionLoop* loop = dynamic_cast(&entity)) - return this->extrude_loop(*loop, description, speed); + if (const ExtrusionPath *path = dynamic_cast(&entity.extrusion_entity())) + return this->extrude_path(*path, entity.flipped(), smooth_path_cache, description, speed); + else if (const ExtrusionMultiPath *multipath = dynamic_cast(&entity.extrusion_entity())) + return this->extrude_multi_path(*multipath, entity.flipped(), smooth_path_cache, description, speed); + else if (const ExtrusionLoop *loop = dynamic_cast(&entity.extrusion_entity())) + return this->extrude_loop(*loop, smooth_path_cache, description, speed); else throw Slic3r::InvalidArgument("Invalid argument supplied to extrude()"); - return ""; + return {}; } -std::string GCode::extrude_path(ExtrusionPath path, std::string_view description, double speed) +std::string GCodeGenerator::extrude_path(const ExtrusionPath &path, bool reverse, const GCode::SmoothPathCache &smooth_path_cache, std::string_view description, double speed) { - path.simplify(m_scaled_resolution); - std::string gcode = this->_extrude(path, description, speed); - if (m_wipe.enable) { - m_wipe.path = std::move(path.polyline); - m_wipe.path.reverse(); - } + Geometry::ArcWelder::Path smooth_path = smooth_path_cache.resolve_or_fit(path, reverse, m_scaled_resolution); + std::string gcode = this->_extrude(path.attributes(), smooth_path, description, speed); + Geometry::ArcWelder::reverse(smooth_path); + m_wipe.set_path(std::move(smooth_path)); // reset acceleration gcode += m_writer.set_print_acceleration((unsigned int)floor(m_config.default_acceleration.value + 0.5)); return gcode; } -std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fills) +std::string GCodeGenerator::extrude_support(const ExtrusionEntityReferences &support_fills, const GCode::SmoothPathCache &smooth_path_cache) { static constexpr const auto support_label = "support material"sv; static constexpr const auto support_interface_label = "support material interface"sv; std::string gcode; - if (! support_fills.entities.empty()) { + if (! support_fills.empty()) { const double support_speed = m_config.support_material_speed.value; const double support_interface_speed = m_config.support_material_interface_speed.get_abs_value(support_speed); - for (const ExtrusionEntity *ee : support_fills.entities) { - ExtrusionRole role = ee->role(); + for (const ExtrusionEntityReference &eref : support_fills) { + ExtrusionRole role = eref.extrusion_entity().role(); assert(role == ExtrusionRole::SupportMaterial || role == ExtrusionRole::SupportMaterialInterface); const auto label = (role == ExtrusionRole::SupportMaterial) ? support_label : support_interface_label; const double speed = (role == ExtrusionRole::SupportMaterial) ? support_speed : support_interface_speed; - const ExtrusionPath *path = dynamic_cast(ee); + const ExtrusionPath *path = dynamic_cast(&eref.extrusion_entity()); if (path) - gcode += this->extrude_path(*path, label, speed); - else { - const ExtrusionMultiPath *multipath = dynamic_cast(ee); - if (multipath) - gcode += this->extrude_multi_path(*multipath, label, speed); + gcode += this->extrude_path(*path, eref.flipped(), smooth_path_cache, label, speed); + else if (const ExtrusionMultiPath *multipath = dynamic_cast(&eref.extrusion_entity()); multipath) + gcode += this->extrude_multi_path(*multipath, eref.flipped(), smooth_path_cache, label, speed); else { - const ExtrusionEntityCollection *eec = dynamic_cast(ee); + const ExtrusionEntityCollection *eec = dynamic_cast(&eref.extrusion_entity()); assert(eec); - if (eec) - gcode += this->extrude_support(*eec); + if (eec) { + //FIXME maybe order the support here? + ExtrusionEntityReferences refs; + refs.reserve(eec->entities.size()); + std::transform(eec->entities.begin(), eec->entities.end(), std::back_inserter(refs), + [flipped = eref.flipped()](const ExtrusionEntity *ee) { return ExtrusionEntityReference{ *ee, flipped }; }); + gcode += this->extrude_support(refs, smooth_path_cache); } } } @@ -2916,17 +2981,17 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill return gcode; } -bool GCode::GCodeOutputStream::is_error() const +bool GCodeGenerator::GCodeOutputStream::is_error() const { return ::ferror(this->f); } -void GCode::GCodeOutputStream::flush() +void GCodeGenerator::GCodeOutputStream::flush() { ::fflush(this->f); } -void GCode::GCodeOutputStream::close() +void GCodeGenerator::GCodeOutputStream::close() { if (this->f) { ::fclose(this->f); @@ -2934,7 +2999,7 @@ void GCode::GCodeOutputStream::close() } } -void GCode::GCodeOutputStream::write(const char *what) +void GCodeGenerator::GCodeOutputStream::write(const char *what) { if (what != nullptr) { //FIXME don't allocate a string, maybe process a batch of lines? @@ -2945,13 +3010,13 @@ void GCode::GCodeOutputStream::write(const char *what) } } -void GCode::GCodeOutputStream::writeln(const std::string &what) +void GCodeGenerator::GCodeOutputStream::writeln(const std::string &what) { if (! what.empty()) this->write(what.back() == '\n' ? what : what + '\n'); } -void GCode::GCodeOutputStream::write_format(const char* format, ...) +void GCodeGenerator::GCodeOutputStream::write_format(const char* format, ...) { va_list args; va_start(args, format); @@ -2983,22 +3048,31 @@ void GCode::GCodeOutputStream::write_format(const char* format, ...) va_end(args); } -std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view description, double speed) +std::string GCodeGenerator::_extrude( + const ExtrusionAttributes &path_attr, + const Geometry::ArcWelder::Path &path, + const std::string_view description, + double speed) { std::string gcode; - const std::string_view description_bridge = path.role().is_bridge() ? " (bridge)"sv : ""sv; + const std::string_view description_bridge = path_attr.role.is_bridge() ? " (bridge)"sv : ""sv; // go to first point of extrusion path - if (!m_last_pos_defined || m_last_pos != path.first_point()) { + if (!m_last_pos_defined) { + const double z = this->m_last_layer_z + this->m_config.z_offset.value; + const std::string comment{"move to print after unknown position"}; + gcode += this->retract_and_wipe(); + gcode += this->m_writer.travel_to_xy(this->point_to_gcode(path.front().point), comment); + gcode += this->m_writer.get_travel_to_z_gcode(z, comment); + } else if ( m_last_pos != path.front().point) { std::string comment = "move to first "; comment += description; comment += description_bridge; comment += " point"; - gcode += this->travel_to(path.first_point(), path.role(), comment); + gcode += this->travel_to(path.front().point, path_attr.role, comment); } //B38 - m_writer.add_object_change_labels(gcode); // compensate retraction gcode += this->unretract(); @@ -3010,17 +3084,17 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view de acceleration = m_config.first_layer_acceleration.value; } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { acceleration = m_config.first_layer_acceleration_over_raft.value; - } else if (m_config.bridge_acceleration.value > 0 && path.role().is_bridge()) { + } else if (m_config.bridge_acceleration.value > 0 && path_attr.role.is_bridge()) { acceleration = m_config.bridge_acceleration.value; - } else if (m_config.top_solid_infill_acceleration > 0 && path.role() == ExtrusionRole::TopSolidInfill) { + } else if (m_config.top_solid_infill_acceleration > 0 && path_attr.role == ExtrusionRole::TopSolidInfill) { acceleration = m_config.top_solid_infill_acceleration.value; - } else if (m_config.solid_infill_acceleration > 0 && path.role().is_solid_infill()) { + } else if (m_config.solid_infill_acceleration > 0 && path_attr.role.is_solid_infill()) { acceleration = m_config.solid_infill_acceleration.value; - } else if (m_config.infill_acceleration.value > 0 && path.role().is_infill()) { + } else if (m_config.infill_acceleration.value > 0 && path_attr.role.is_infill()) { acceleration = m_config.infill_acceleration.value; - } else if (m_config.external_perimeter_acceleration > 0 && path.role().is_external_perimeter()) { + } else if (m_config.external_perimeter_acceleration > 0 && path_attr.role.is_external_perimeter()) { acceleration = m_config.external_perimeter_acceleration.value; - } else if (m_config.perimeter_acceleration.value > 0 && path.role().is_perimeter()) { + } else if (m_config.perimeter_acceleration.value > 0 && path_attr.role.is_perimeter()) { acceleration = m_config.perimeter_acceleration.value; } else { acceleration = m_config.default_acceleration.value; @@ -3029,41 +3103,41 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view de } // calculate extrusion length per distance unit - double e_per_mm = m_writer.extruder()->e_per_mm3() * path.mm3_per_mm; + double e_per_mm = m_writer.extruder()->e_per_mm3() * path_attr.mm3_per_mm; if (m_writer.extrusion_axis().empty()) // gcfNoExtrusion e_per_mm = 0; // set speed if (speed == -1) { - if (path.role() == ExtrusionRole::Perimeter) { + if (path_attr.role == ExtrusionRole::Perimeter) { speed = m_config.get_abs_value("perimeter_speed"); - } else if (path.role() == ExtrusionRole::ExternalPerimeter) { + } else if (path_attr.role == ExtrusionRole::ExternalPerimeter) { speed = m_config.get_abs_value("external_perimeter_speed"); - } else if (path.role().is_bridge()) { - assert(path.role().is_perimeter() || path.role() == ExtrusionRole::BridgeInfill); + } else if (path_attr.role.is_bridge()) { + assert(path_attr.role.is_perimeter() || path_attr.role == ExtrusionRole::BridgeInfill); speed = m_config.get_abs_value("bridge_speed"); - } else if (path.role() == ExtrusionRole::InternalInfill) { + } else if (path_attr.role == ExtrusionRole::InternalInfill) { speed = m_config.get_abs_value("infill_speed"); - } else if (path.role() == ExtrusionRole::SolidInfill) { + } else if (path_attr.role == ExtrusionRole::SolidInfill) { speed = m_config.get_abs_value("solid_infill_speed"); - } else if (path.role() == ExtrusionRole::TopSolidInfill) { + } else if (path_attr.role == ExtrusionRole::TopSolidInfill) { speed = m_config.get_abs_value("top_solid_infill_speed"); - } else if (path.role() == ExtrusionRole::Ironing) { + } else if (path_attr.role == ExtrusionRole::Ironing) { speed = m_config.get_abs_value("ironing_speed"); - } else if (path.role() == ExtrusionRole::GapFill) { + } else if (path_attr.role == ExtrusionRole::GapFill) { speed = m_config.get_abs_value("gap_fill_speed"); } else { throw Slic3r::InvalidArgument("Invalid speed"); } } if (m_volumetric_speed != 0. && speed == 0) - speed = m_volumetric_speed / path.mm3_per_mm; + speed = m_volumetric_speed / path_attr.mm3_per_mm; //B37 if (this->on_first_layer()) - speed = path.role() == ExtrusionRole::InternalInfill ? + speed = path_attr.role == ExtrusionRole::InternalInfill ? m_config.get_abs_value("first_layer_infill_speed") : - path.role() == ExtrusionRole::SolidInfill ? + path_attr.role == ExtrusionRole::SolidInfill ? m_config.get_abs_value("first_layer_infill_speed") : m_config.get_abs_value("first_layer_speed", speed); else if (this->object_layer_over_raft()) @@ -3072,54 +3146,37 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view de // cap speed with max_volumetric_speed anyway (even if user is not using autospeed) speed = std::min( speed, - m_config.max_volumetric_speed.value / path.mm3_per_mm + m_config.max_volumetric_speed.value / path_attr.mm3_per_mm ); } if (EXTRUDER_CONFIG(filament_max_volumetric_speed) > 0) { // cap speed with max_volumetric_speed anyway (even if user is not using autospeed) speed = std::min( speed, - EXTRUDER_CONFIG(filament_max_volumetric_speed) / path.mm3_per_mm + EXTRUDER_CONFIG(filament_max_volumetric_speed) / path_attr.mm3_per_mm ); } - bool variable_speed_or_fan_speed = false; - std::vector new_points{}; - if ((this->m_config.enable_dynamic_overhang_speeds || this->config().enable_dynamic_fan_speeds.get_at(m_writer.extruder()->id())) && - !this->on_first_layer() && path.role().is_perimeter()) { - std::vector> overhangs_with_speeds = {{100, ConfigOptionFloatOrPercent{speed, false}}}; - if (this->m_config.enable_dynamic_overhang_speeds) { - overhangs_with_speeds = {{0, m_config.overhang_speed_0}, - {25, m_config.overhang_speed_1}, - {50, m_config.overhang_speed_2}, - {75, m_config.overhang_speed_3}, - {100, ConfigOptionFloatOrPercent{speed, false}}}; - } - - std::vector> overhang_w_fan_speeds = {{100, ConfigOptionInts{0}}}; - if (this->m_config.enable_dynamic_fan_speeds.get_at(m_writer.extruder()->id())) { - overhang_w_fan_speeds = {{0, m_config.overhang_fan_speed_0}, - {25, m_config.overhang_fan_speed_1}, - {50, m_config.overhang_fan_speed_2}, - {75, m_config.overhang_fan_speed_3}, - {100, ConfigOptionInts{0}}}; - } + std::pair dynamic_speed_and_fan_speed{-1, -1}; + if (path_attr.overhang_attributes.has_value()) { double external_perim_reference_speed = m_config.get_abs_value("external_perimeter_speed"); if (external_perim_reference_speed == 0) - external_perim_reference_speed = m_volumetric_speed / path.mm3_per_mm; + external_perim_reference_speed = m_volumetric_speed / path_attr.mm3_per_mm; if (m_config.max_volumetric_speed.value > 0) - external_perim_reference_speed = std::min(external_perim_reference_speed, m_config.max_volumetric_speed.value / path.mm3_per_mm); + external_perim_reference_speed = std::min(external_perim_reference_speed, + m_config.max_volumetric_speed.value / path_attr.mm3_per_mm); if (EXTRUDER_CONFIG(filament_max_volumetric_speed) > 0) { external_perim_reference_speed = std::min(external_perim_reference_speed, - EXTRUDER_CONFIG(filament_max_volumetric_speed) / path.mm3_per_mm); + EXTRUDER_CONFIG(filament_max_volumetric_speed) / path_attr.mm3_per_mm); } - new_points = m_extrusion_quality_estimator.estimate_speed_from_extrusion_quality(path, overhangs_with_speeds, overhang_w_fan_speeds, - m_writer.extruder()->id(), external_perim_reference_speed, - speed); - variable_speed_or_fan_speed = std::any_of(new_points.begin(), new_points.end(), - [speed](const ProcessedPoint &p) { return p.speed != speed || p.fan_speed != 0; }); + dynamic_speed_and_fan_speed = ExtrusionProcessor::calculate_overhang_speed(path_attr, this->m_config, m_writer.extruder()->id(), + external_perim_reference_speed, speed); + } + + if (dynamic_speed_and_fan_speed.first > -1) { + speed = dynamic_speed_and_fan_speed.first; } double F = speed * 60; // convert mm/sec to mm/min @@ -3127,7 +3184,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view de // extrude arc or line if (m_enable_extrusion_role_markers) { - if (GCodeExtrusionRole role = extrusion_role_to_gcode_extrusion_role(path.role()); role != m_last_extrusion_role) + if (GCodeExtrusionRole role = extrusion_role_to_gcode_extrusion_role(path_attr.role); role != m_last_extrusion_role) { m_last_extrusion_role = role; if (m_enable_extrusion_role_markers) @@ -3145,29 +3202,29 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view de bool last_was_wipe_tower = (m_last_processor_extrusion_role == GCodeExtrusionRole::WipeTower); assert(is_decimal_separator_point()); - if (GCodeExtrusionRole role = extrusion_role_to_gcode_extrusion_role(path.role()); role != m_last_processor_extrusion_role) { + if (GCodeExtrusionRole role = extrusion_role_to_gcode_extrusion_role(path_attr.role); role != m_last_processor_extrusion_role) { m_last_processor_extrusion_role = role; char buf[64]; sprintf(buf, ";%s%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role).c_str(), gcode_extrusion_role_to_string(m_last_processor_extrusion_role).c_str()); gcode += buf; } - if (last_was_wipe_tower || m_last_width != path.width) { - m_last_width = path.width; + if (last_was_wipe_tower || m_last_width != path_attr.width) { + m_last_width = path_attr.width; gcode += std::string(";") + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Width) + float_to_string_decimal_point(m_last_width) + "\n"; } #if ENABLE_GCODE_VIEWER_DATA_CHECKING - if (last_was_wipe_tower || (m_last_mm3_per_mm != path.mm3_per_mm)) { - m_last_mm3_per_mm = path.mm3_per_mm; + if (last_was_wipe_tower || (m_last_mm3_per_mm != path_attr.mm3_per_mm)) { + m_last_mm3_per_mm = path_attr.mm3_per_mm; gcode += std::string(";") + GCodeProcessor::Mm3_Per_Mm_Tag + float_to_string_decimal_point(m_last_mm3_per_mm) + "\n"; } #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING - 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 - path_attr.height) > EPSILON) { + m_last_height = path_attr.height; gcode += std::string(";") + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + float_to_string_decimal_point(m_last_height) + "\n"; @@ -3175,158 +3232,312 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view de std::string cooling_marker_setspeed_comments; if (m_enable_cooling_markers) { - if (path.role().is_bridge()) + if (path_attr.role.is_bridge()) gcode += ";_BRIDGE_FAN_START\n"; else cooling_marker_setspeed_comments = ";_EXTRUDE_SET_SPEED"; - if (path.role() == ExtrusionRole::ExternalPerimeter) + if (path_attr.role == ExtrusionRole::ExternalPerimeter) cooling_marker_setspeed_comments += ";_EXTERNAL_PERIMETER"; } - if (!variable_speed_or_fan_speed) { // F is mm per minute. gcode += m_writer.set_speed(F, "", cooling_marker_setspeed_comments); + if (dynamic_speed_and_fan_speed.second >= 0) + gcode += ";_SET_FAN_SPEED" + std::to_string(int(dynamic_speed_and_fan_speed.second)) + "\n"; double path_length = 0.; std::string comment; if (m_config.gcode_comments) { comment = description; comment += description_bridge; } - Vec2d prev = this->point_to_gcode_quantized(path.polyline.points.front()); - auto it = path.polyline.points.begin(); - auto end = path.polyline.points.end(); + Vec2d prev_exact = this->point_to_gcode(path.front().point); + Vec2d prev = GCodeFormatter::quantize(prev_exact); + auto it = path.begin(); + auto end = path.end(); for (++ it; it != end; ++ it) { - Vec2d p = this->point_to_gcode_quantized(*it); - const double line_length = (p - prev).norm(); + Vec2d p_exact = this->point_to_gcode(it->point); + Vec2d p = GCodeFormatter::quantize(p_exact); + assert(p != prev); + if (p != prev) { + // Center of the radius to be emitted into the G-code: Either by radius or by center offset. + double radius = 0; + Vec2d ij; + if (it->radius != 0) { + // Extrude an arc. + assert(m_config.arc_fitting == ArcFittingType::EmitCenter); + radius = unscaled(it->radius); + { + // Calculate quantized IJ circle center offset. + ij = GCodeFormatter::quantize(Vec2d( + Geometry::ArcWelder::arc_center(prev_exact.cast(), p_exact.cast(), double(radius), it->ccw()) + - prev)); + if (ij == Vec2d::Zero()) + // Don't extrude a degenerated circle. + radius = 0; + } + } + if (radius == 0) { + // Extrude line segment. + if (const double line_length = (p - prev).norm(); line_length > 0) { path_length += line_length; gcode += m_writer.extrude_to_xy(p, e_per_mm * line_length, comment); - prev = p; } } else { - std::string marked_comment; - if (m_config.gcode_comments) { - marked_comment = description; - marked_comment += description_bridge; + double angle = Geometry::ArcWelder::arc_angle(prev.cast(), p.cast(), double(radius)); + assert(angle > 0); + const double line_length = angle * std::abs(radius); + path_length += line_length; + const double dE = e_per_mm * line_length; + assert(dE > 0); + gcode += m_writer.extrude_to_xy_G2G3IJ(p, ij, it->ccw(), dE, comment); } - double last_set_speed = new_points[0].speed * 60.0; - double last_set_fan_speed = new_points[0].fan_speed; - gcode += m_writer.set_speed(last_set_speed, "", cooling_marker_setspeed_comments); - gcode += "\n;_SET_FAN_SPEED" + std::to_string(int(last_set_fan_speed)) + "\n"; - Vec2d prev = this->point_to_gcode_quantized(new_points[0].p); - for (size_t i = 1; i < new_points.size(); i++) { - const ProcessedPoint &processed_point = new_points[i]; - Vec2d p = this->point_to_gcode_quantized(processed_point.p); - const double line_length = (p - prev).norm(); - gcode += m_writer.extrude_to_xy(p, e_per_mm * line_length, marked_comment); prev = p; - double new_speed = processed_point.speed * 60.0; - if (last_set_speed != new_speed) { - gcode += m_writer.set_speed(new_speed, "", cooling_marker_setspeed_comments); - last_set_speed = new_speed; - } - if (last_set_fan_speed != processed_point.fan_speed) { - last_set_fan_speed = processed_point.fan_speed; - gcode += "\n;_SET_FAN_SPEED" + std::to_string(int(last_set_fan_speed)) + "\n"; + prev_exact = p_exact; } } - gcode += "\n;_RESET_FAN_SPEED\n"; - } if (m_enable_cooling_markers) - gcode += path.role().is_bridge() ? ";_BRIDGE_FAN_END\n" : ";_EXTRUDE_END\n"; + gcode += path_attr.role.is_bridge() ? ";_BRIDGE_FAN_END\n" : ";_EXTRUDE_END\n"; - this->set_last_pos(path.last_point()); + if (dynamic_speed_and_fan_speed.second >= 0) + gcode += ";_RESET_FAN_SPEED\n"; + + this->set_last_pos(path.back().point); return gcode; } -// This method accepts &point in print coordinates. -std::string GCode::travel_to(const Point &point, ExtrusionRole role, std::string comment) -{ - /* Define the travel move as a line between current position and the taget point. - This is expressed in print coordinates, so it will need to be translated by - this->origin in order to get G-code coordinates. */ - Polyline travel { this->last_pos(), point }; +Points3 generate_flat_travel(tcb::span xy_path, const float elevation) { + Points3 result; + result.reserve(xy_path.size() - 1); + for (const Point& point : xy_path.subspan(1)) { + result.emplace_back(point.x(), point.y(), scaled(elevation)); + } + return result; +} +Vec2d place_at_segment(const Vec2d& current_point, const Vec2d& previous_point, const double distance) { + Vec2d direction = (current_point - previous_point).normalized(); + return previous_point + direction * distance; +} - if (this->config().avoid_crossing_curled_overhangs) { - if (m_config.avoid_crossing_perimeters) { - BOOST_LOG_TRIVIAL(warning) - << "Option >avoid crossing curled overhangs< is not compatible with avoid crossing perimeters and it will be ignored!"; +namespace GCode::Impl { +std::vector slice_xy_path(tcb::span xy_path, tcb::span sorted_distances) { + assert(xy_path.size() >= 2); + std::vector result; + result.reserve(xy_path.size() + sorted_distances.size()); + double total_distance{0}; + result.emplace_back(DistancedPoint{xy_path.front(), 0}); + Point previous_point = result.front().point; + std::size_t offset{0}; + for (const Point& point : xy_path.subspan(1)) { + Vec2d unscaled_point{unscaled(point)}; + Vec2d unscaled_previous_point{unscaled(previous_point)}; + const double current_segment_length = (unscaled_point - unscaled_previous_point).norm(); + for (const double distance_to_add : sorted_distances.subspan(offset)) { + if (distance_to_add <= total_distance + current_segment_length) { + Point to_place = scaled(place_at_segment( + unscaled_point, + unscaled_previous_point, + distance_to_add - total_distance + )); + if (to_place != previous_point && to_place != point) { + result.emplace_back(DistancedPoint{to_place, distance_to_add}); + } + ++offset; } else { - Point scaled_origin = Point(scaled(this->origin())); - travel = m_avoid_crossing_curled_overhangs.find_path(this->last_pos() + scaled_origin, point + scaled_origin); - travel.translate(-scaled_origin); + break; + } + } + total_distance += current_segment_length; + result.emplace_back(DistancedPoint{point, total_distance}); + previous_point = point; + } + return result; +} + +struct ElevatedTravelParams { + double lift_height{}; + double slope_end{}; +}; + +struct ElevatedTravelFormula { + double operator()(double distance_from_start) const { + if (distance_from_start < this->params.slope_end) { + const double lift_percent = distance_from_start / this->params.slope_end; + return lift_percent * this->params.lift_height; + } else { + return this->params.lift_height; } } - // check whether a straight travel move would need retraction - bool needs_retraction = this->needs_retraction(travel, role); - // check whether wipe could be disabled without causing visible stringing - bool could_be_wipe_disabled = false; - // Save state of use_external_mp_once for the case that will be needed to call twice m_avoid_crossing_perimeters.travel_to. - const bool used_external_mp_once = m_avoid_crossing_perimeters.used_external_mp_once(); + ElevatedTravelParams params{}; +}; - // if a retraction would be needed, try to use avoid_crossing_perimeters to plan a - // multi-hop travel path inside the configuration space - if (needs_retraction - && m_config.avoid_crossing_perimeters - && ! m_avoid_crossing_perimeters.disabled_once()) { - travel = m_avoid_crossing_perimeters.travel_to(*this, point, &could_be_wipe_disabled); - // check again whether the new travel path still needs a retraction - needs_retraction = this->needs_retraction(travel, role); - //if (needs_retraction && m_layer_index > 1) exit(0); +Points3 generate_elevated_travel( + const tcb::span xy_path, + const std::vector& ensure_points_at_distances, + const double initial_elevation, + const std::function& elevation +) { + Points3 result{}; + + std::vector extended_xy_path = slice_xy_path(xy_path, ensure_points_at_distances); + result.reserve(extended_xy_path.size()); + + for (const DistancedPoint& point : extended_xy_path) { + result.emplace_back(point.point.x(), point.point.y(), scaled(initial_elevation + elevation(point.distance_from_start))); } - // Re-allow avoid_crossing_perimeters for the next travel moves - m_avoid_crossing_perimeters.reset_once_modifiers(); + return result; +} +AABBTreeLines::LinesDistancer get_expolygons_distancer(const ExPolygons& polygons) { + std::vector lines; + for (const ExPolygon& polygon : polygons) { + for (const Line& line : polygon.lines()) { + lines.emplace_back(unscaled(line.a), unscaled(line.b)); + } + } + return AABBTreeLines::LinesDistancer{std::move(lines)}; + } + +std::optional get_first_crossed_line_distance( + tcb::span xy_path, + const AABBTreeLines::LinesDistancer& distancer +) { + assert(!xy_path.empty()); + if (xy_path.empty()) { + return {}; + } + double traversed_distance = 0; + for (const Line& line : xy_path) { + const Linef unscaled_line = {unscaled(line.a), unscaled(line.b)}; + auto intersections = distancer.intersections_with_line(unscaled_line); + if (!intersections.empty()) { + const Vec2d intersection = intersections.front().first; + const double distance = traversed_distance + (unscaled_line.a - intersection).norm(); + if (distance > EPSILON) { + return distance; + } else if (intersections.size() >= 2) { // Edge case + const Vec2d second_intersection = intersections[1].first; + return traversed_distance + (unscaled_line.a - second_intersection).norm(); + } + } + traversed_distance += (unscaled_line.a - unscaled_line.b).norm(); + } + + return {}; +} +std::optional get_obstacle_adjusted_slope_end( + const Lines& xy_path, + const std::optional>& previous_layer_distancer +) { + if (!previous_layer_distancer) { + return std::nullopt; + } + std::optional first_obstacle_distance = get_first_crossed_line_distance( + xy_path, *previous_layer_distancer + ); + if (!first_obstacle_distance) { + return std::nullopt; + } + return *first_obstacle_distance; +} + +ElevatedTravelParams get_elevated_traval_params( + const Lines& xy_path, + const FullPrintConfig& config, + const unsigned extruder_id, + const std::optional>& previous_layer_distancer +) { + ElevatedTravelParams elevation_params{}; + if (!config.travel_ramping_lift.get_at(extruder_id)) { + elevation_params.slope_end = 0; + elevation_params.lift_height = config.retract_lift.get_at(extruder_id); + return elevation_params; + } + elevation_params.lift_height = config.travel_max_lift.get_at(extruder_id); + const double slope_deg = config.travel_slope.get_at(extruder_id); + if (slope_deg >= 90 || slope_deg <= 0) { + elevation_params.slope_end = 0; + } else { + const double slope_rad = slope_deg * (M_PI / 180); // rad + elevation_params.slope_end = elevation_params.lift_height / std::tan(slope_rad); + } + std::optional obstacle_adjusted_slope_end{get_obstacle_adjusted_slope_end( + xy_path, + previous_layer_distancer + )}; + if (obstacle_adjusted_slope_end && obstacle_adjusted_slope_end < elevation_params.slope_end) { + elevation_params.slope_end = *obstacle_adjusted_slope_end; + } + + return elevation_params; +} +Points3 generate_travel_to_extrusion( + const Polyline& xy_path, + const FullPrintConfig& config, + const unsigned extruder_id, + const double initial_elevation, + const std::optional>& previous_layer_distancer +) { + const double upper_limit = config.retract_lift_below.get_at(extruder_id); + const double lower_limit = config.retract_lift_above.get_at(extruder_id); + if ( + (lower_limit > 0 && initial_elevation < lower_limit) + || (upper_limit > 0 && initial_elevation > upper_limit) + ) { + return generate_flat_travel(xy_path.points, initial_elevation); + } + ElevatedTravelParams elevation_params{get_elevated_traval_params( + xy_path.lines(), + config, + extruder_id, + previous_layer_distancer + )}; + + const std::vector ensure_points_at_distances{elevation_params.slope_end}; + + Points3 result{generate_elevated_travel( + xy_path.points, + ensure_points_at_distances, + initial_elevation, + ElevatedTravelFormula{elevation_params} + )}; + result.emplace_back(xy_path.back().x(), xy_path.back().y(), scaled(initial_elevation)); + return result; +} +} + +std::string GCodeGenerator::generate_travel_gcode( + const Points3& travel, + const std::string& comment +) { + std::string gcode; + const unsigned acceleration =(unsigned)(m_config.travel_acceleration.value + 0.5); + + if (travel.empty()) { + return ""; + } // generate G-code for the travel move - std::string gcode; - if (needs_retraction) { - if (m_config.avoid_crossing_perimeters && could_be_wipe_disabled) - m_wipe.reset_path(); - - Point last_post_before_retract = this->last_pos(); - gcode += this->retract(); - // When "Wipe while retracting" is enabled, then extruder moves to another position, and travel from this position can cross perimeters. - // Because of it, it is necessary to call avoid crossing perimeters again with new starting point after calling retraction() - // FIXME Lukas H.: Try to predict if this second calling of avoid crossing perimeters will be needed or not. It could save computations. - if (last_post_before_retract != this->last_pos() && m_config.avoid_crossing_perimeters) { - // If in the previous call of m_avoid_crossing_perimeters.travel_to was use_external_mp_once set to true restore this value for next call. - if (used_external_mp_once) - m_avoid_crossing_perimeters.use_external_mp_once(); - travel = m_avoid_crossing_perimeters.travel_to(*this, point); - // If state of use_external_mp_once was changed reset it to right value. - if (used_external_mp_once) - m_avoid_crossing_perimeters.reset_once_modifiers(); - } - } else - // Reset the wipe path when traveling, so one would not wipe along an old path. - m_wipe.reset_path(); - - //B38 - m_writer.add_object_change_labels(gcode); - - // use G1 because we rely on paths being straight (G0 may make round paths) - if (travel.size() >= 2) { - gcode += m_writer.set_travel_acceleration((unsigned int)(m_config.travel_acceleration.value + 0.5)); + gcode += this->m_writer.set_travel_acceleration(acceleration); - for (size_t i = 1; i < travel.size(); ++ i) - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); + for (const Vec3crd& point : travel) { + gcode += this->m_writer.travel_to_xyz(to_3d(this->point_to_gcode(point.head<2>()), unscaled(point.z())), comment); + this->set_last_pos(point.head<2>()); + } if (! GCodeWriter::supports_separate_travel_acceleration(config().gcode_flavor)) { // In case that this flavor does not support separate print and travel acceleration, // reset acceleration to default. - gcode += m_writer.set_travel_acceleration((unsigned int)(m_config.travel_acceleration.value + 0.5)); + gcode += this->m_writer.set_travel_acceleration(acceleration); } - this->set_last_pos(travel.points.back()); - } return gcode; } -bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role) +bool GCodeGenerator::needs_retraction(const Polyline &travel, ExtrusionRole role) { if (! m_writer.extruder() || travel.length() < scale_(EXTRUDER_CONFIG(retract_before_travel))) { // skip retraction if the move is shorter than the configured threshold @@ -3365,7 +3576,105 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role) return true; } -std::string GCode::retract(bool toolchange) +Polyline GCodeGenerator::generate_travel_xy_path( + const Point& start_point, + const Point& end_point, + const bool needs_retraction, + bool& could_be_wipe_disabled +) { + + const Point scaled_origin{scaled(this->origin())}; + const bool avoid_crossing_perimeters = ( + this->m_config.avoid_crossing_perimeters + && !this->m_avoid_crossing_perimeters.disabled_once() + ); + + Polyline xy_path{start_point, end_point}; + if (m_config.avoid_crossing_curled_overhangs) { + if (avoid_crossing_perimeters) { + BOOST_LOG_TRIVIAL(warning) + << "Option >avoid crossing curled overhangs< is not compatible with avoid crossing perimeters and it will be ignored!"; + } else { + xy_path = this->m_avoid_crossing_curled_overhangs.find_path( + start_point + scaled_origin, + end_point + scaled_origin + ); + xy_path.translate(-scaled_origin); + } + } + + + // if a retraction would be needed, try to use avoid_crossing_perimeters to plan a + // multi-hop travel path inside the configuration space + if ( + needs_retraction + && avoid_crossing_perimeters + ) { + xy_path = this->m_avoid_crossing_perimeters.travel_to(*this, end_point, &could_be_wipe_disabled); + } + + return xy_path; +} + +// This method accepts &point in print coordinates. +std::string GCodeGenerator::travel_to(const Point &point, ExtrusionRole role, std::string comment) +{ + + const Point start_point = this->last_pos(); + + using namespace GCode::Impl; + + // check whether a straight travel move would need retraction + + bool could_be_wipe_disabled {false}; + bool needs_retraction = this->needs_retraction(Polyline{start_point, point}, role); + + Polyline xy_path{generate_travel_xy_path( + start_point, point, needs_retraction, could_be_wipe_disabled + )}; + + needs_retraction = this->needs_retraction(xy_path, role); + + std::string wipe_retract_gcode{}; + if (needs_retraction) { + if (could_be_wipe_disabled) { + m_wipe.reset_path(); + } + + Point position_before_wipe{this->last_pos()}; + wipe_retract_gcode = this->retract_and_wipe(); + + if (this->last_pos() != position_before_wipe) { + xy_path = generate_travel_xy_path( + this->last_pos(), point, needs_retraction, could_be_wipe_disabled + ); + } + } else { + m_wipe.reset_path(); + } + + this->m_avoid_crossing_perimeters.reset_once_modifiers(); + + const unsigned extruder_id = this->m_writer.extruder()->id(); + const double retract_length = this->m_config.retract_length.get_at(extruder_id); + bool can_be_flat{!needs_retraction || retract_length == 0}; + const double initial_elevation = this->m_last_layer_z + this->m_config.z_offset.value; + const Points3 travel = ( + can_be_flat ? + generate_flat_travel(xy_path.points, initial_elevation) : + GCode::Impl::generate_travel_to_extrusion( + xy_path, + this->m_config, + extruder_id, + initial_elevation, + this->m_previous_layer_distancer + ) + ); + + return wipe_retract_gcode + generate_travel_gcode(travel, comment); +} + +std::string GCodeGenerator::retract_and_wipe(bool toolchange) { std::string gcode; @@ -3385,13 +3694,11 @@ std::string GCode::retract(bool toolchange) gcode += toolchange ? m_writer.retract_for_toolchange() : m_writer.retract(); gcode += m_writer.reset_e(); - if (m_writer.extruder()->retract_length() > 0 || m_config.use_firmware_retraction) - gcode += m_writer.lift(); return gcode; } -std::string GCode::set_extruder(unsigned int extruder_id, double print_z) +std::string GCodeGenerator::set_extruder(unsigned int extruder_id, double print_z) { if (!m_writer.need_toolchange(extruder_id)) return ""; @@ -3418,7 +3725,7 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z) } // prepend retraction on the current extruder - std::string gcode = this->retract(true); + std::string gcode = this->retract_and_wipe(true); // Always reset the extrusion path, even if the tool change retract is set to zero. m_wipe.reset_path(); @@ -3428,7 +3735,12 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z) unsigned int old_extruder_id = m_writer.extruder()->id(); const std::string &end_filament_gcode = m_config.end_filament_gcode.get_at(old_extruder_id); if (! end_filament_gcode.empty()) { - gcode += placeholder_parser_process("end_filament_gcode", end_filament_gcode, old_extruder_id); + DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position().z() - m_config.z_offset.value)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); + config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(old_extruder_id))); + gcode += placeholder_parser_process("end_filament_gcode", end_filament_gcode, old_extruder_id, &config); check_add_eol(gcode); } } @@ -3488,11 +3800,11 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z) // Set the new extruder to the operating temperature. if (m_ooze_prevention.enable) gcode += m_ooze_prevention.post_toolchange(*this); - + this->m_last_pos_defined = false; return gcode; } //B41 -std::string GCode::set_object_range(Print &print) +std::string GCodeGenerator::set_object_range(Print &print) { std::string gcode; std::string object_name; @@ -3570,20 +3882,7 @@ std::string GCode::set_object_range(Print &print) } // convert a model-space scaled point into G-code coordinates -Vec2d GCode::point_to_gcode(const Point &point) const -{ - Vec2d extruder_offset = EXTRUDER_CONFIG(extruder_offset); - return unscaled(point) + m_origin - extruder_offset; -} - -Vec2d GCode::point_to_gcode_quantized(const Point &point) const -{ - Vec2d p = this->point_to_gcode(point); - return { GCodeFormatter::quantize_xyzf(p.x()), GCodeFormatter::quantize_xyzf(p.y()) }; -} - -// convert a model-space scaled point into G-code coordinates -Point GCode::gcode_to_point(const Vec2d &point) const +Point GCodeGenerator::gcode_to_point(const Vec2d &point) const { Vec2d pt = point - m_origin; if (const Extruder *extruder = m_writer.extruder(); extruder) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 0f224ae..4e0d8a9 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -5,33 +5,39 @@ #include "JumpPointSearch.hpp" #include "libslic3r.h" #include "ExPolygon.hpp" -#include "GCodeWriter.hpp" #include "Layer.hpp" #include "Point.hpp" #include "PlaceholderParser.hpp" #include "PrintConfig.hpp" +#include "Geometry/ArcWelder.hpp" #include "GCode/AvoidCrossingPerimeters.hpp" #include "GCode/CoolingBuffer.hpp" #include "GCode/FindReplace.hpp" +#include "GCode/GCodeWriter.hpp" +#include "GCode/LabelObjects.hpp" +#include "GCode/PressureEqualizer.hpp" #include "GCode/RetractWhenCrossingPerimeters.hpp" +#include "GCode/SmoothPath.hpp" #include "GCode/SpiralVase.hpp" #include "GCode/ToolOrdering.hpp" -#include "GCode/WipeTower.hpp" +#include "GCode/Wipe.hpp" +#include "GCode/WipeTowerIntegration.hpp" #include "GCode/SeamPlacer.hpp" #include "GCode/GCodeProcessor.hpp" #include "EdgeGrid.hpp" #include "GCode/ThumbnailData.hpp" +#include "tcbspan/span.hpp" #include #include #include -#include "GCode/PressureEqualizer.hpp" +//#include "GCode/PressureEqualizer.hpp" namespace Slic3r { // Forward declarations. -class GCode; +class GCodeGenerator; namespace { struct Item; } struct PrintInstance; @@ -41,71 +47,11 @@ public: bool enable; OozePrevention() : enable(false) {} - std::string pre_toolchange(GCode &gcodegen); - std::string post_toolchange(GCode &gcodegen); - -private: - int _get_temp(const GCode &gcodegen) const; -}; - -class Wipe { -public: - bool enable; - Polyline path; - - Wipe() : enable(false) {} - bool has_path() const { return ! this->path.empty(); } - void reset_path() { this->path.clear(); } - std::string wipe(GCode &gcodegen, bool toolchange); -}; - -class WipeTowerIntegration { -public: - WipeTowerIntegration( - const PrintConfig &print_config, - const std::vector &priming, - const std::vector> &tool_changes, - const WipeTower::ToolChangeResult &final_purge) : - m_left(/*float(print_config.wipe_tower_x.value)*/ 0.f), - m_right(float(/*print_config.wipe_tower_x.value +*/ print_config.wipe_tower_width.value)), - m_wipe_tower_pos(float(print_config.wipe_tower_x.value), float(print_config.wipe_tower_y.value)), - m_wipe_tower_rotation(float(print_config.wipe_tower_rotation_angle)), - m_extruder_offsets(print_config.extruder_offset.values), - m_priming(priming), - m_tool_changes(tool_changes), - m_final_purge(final_purge), - m_layer_idx(-1), - m_tool_change_idx(0) - {} - - std::string prime(GCode &gcodegen); - void next_layer() { ++ m_layer_idx; m_tool_change_idx = 0; } - std::string tool_change(GCode &gcodegen, int extruder_id, bool finish_layer); - std::string finalize(GCode &gcodegen); - std::vector used_filament_length() const; + std::string pre_toolchange(GCodeGenerator &gcodegen); + std::string post_toolchange(GCodeGenerator &gcodegen); private: - WipeTowerIntegration& operator=(const WipeTowerIntegration&); - std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; - - // Postprocesses gcode: rotates and moves G1 extrusions and returns result - std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const; - - // Left / right edges of the wipe tower, for the planning of wipe moves. - const float m_left; - const float m_right; - const Vec2f m_wipe_tower_pos; - const float m_wipe_tower_rotation; - const std::vector m_extruder_offsets; - - // Reference to cached values at the Printer class. - const std::vector &m_priming; - const std::vector> &m_tool_changes; - const WipeTower::ToolChangeResult &m_final_purge; - // Current layer index. - int m_layer_idx; - int m_tool_change_idx; - double m_last_wipe_tower_print_z = 0.f; + int _get_temp(const GCodeGenerator &gcodegen) const; }; class ColorPrintColors @@ -129,9 +75,115 @@ struct LayerResult { static LayerResult make_nop_layer_result() { return {"", std::numeric_limits::max(), false, false, true}; } }; -class GCode { +namespace GCode::Impl { +struct DistancedPoint { + Point point; + double distance_from_start; +}; + +/** + * @brief Takes a path described as a list of points and adds points to it. + * + * @param xy_path A list of points describing a path in xy. + * @param sorted_distances A sorted list of distances along the path. + * @return Sliced path. + * + * The algorithm travels along the path segments and adds points to + * the segments in such a way that the points have specified distances + * from the xy_path start. **Any distances over the xy_path end will + * be simply ignored.** + * + * Example usage - simplified for clarity: + * @code + * std::vector distances{0.5, 1.5}; + * std::vector xy_path{{0, 0}, {1, 0}}; + * // produces + * {{0, 0}, {0, 0.5}, {1, 0}} + * // notice that 1.5 is omitted + * @endcode + */ +std::vector slice_xy_path(tcb::span xy_path, tcb::span sorted_distances); + +/** + * @brief Take xy_path and genrate a travel acording to elevation. + * + * @param xy_path A list of points describing a path in xy. + * @param ensure_points_at_distances See slice_xy_path sorted_distances. + * @param elevation A function taking current distance in mm as input and returning elevation in mm as output. + * + * **Be aweare** that the elevation function operates in mm, while xy_path and returned travel are in + * scaled coordinates. + */ +Points3 generate_elevated_travel( + const tcb::span xy_path, + const std::vector& ensure_points_at_distances, + const double initial_elevation, + const std::function& elevation +); + +/** + * @brief Takes a list o polygons and builds a AABBTree over all unscaled lines. + * + * @param polygons A list of polygons. + * @return AABB Tree over all lines of the polygons. + * + * Unscales the lines in the process! + */ +AABBTreeLines::LinesDistancer get_expolygons_distancer(const ExPolygons& polygons); + +/** + * @brief Given a AABB tree over lines find intersection with xy_path closest to the xy_path start. + * + * @param xy_path A path in 2D. + * @param distancer AABB Tree over lines. + * @return Distance to the first intersection if there is one. + * + * **Ignores intersection with xy_path starting point.** + */ +std::optional get_first_crossed_line_distance( + tcb::span xy_path, + const AABBTreeLines::LinesDistancer& distancer +); + + +/** + * Generates a regular polygon - all angles are the same (e.g. typical hexagon). + * + * @param centroid Central point. + * @param start_point The polygon point are ordered. This is the first point. + * @param points_count Amount of nodes of the polygon (e.g. 6 for haxagon). + * + * Distance between centroid and start point sets the scale of the polygon. + */ +Polygon generate_regular_polygon( + const Point& centroid, + const Point& start_point, + const unsigned points_count +); + +class Bed { + private: + Polygon inner_offset; + static Polygon get_inner_offset(const std::vector& shape, const double padding); + + public: + /** + * Bed shape with inner padding. + */ + Bed(const std::vector& shape, const double padding); + + Vec2d centroid; + + /** + * Returns true if the point is within the bed shape including inner padding. + */ + bool contains_within_padding(const Vec2d& point) const; +}; +} + +class GCodeGenerator { public: - GCode() : + GCodeGenerator() : m_origin(Vec2d::Zero()), m_enable_loop_clipping(true), m_enable_cooling_markers(false), @@ -153,7 +205,7 @@ public: m_silent_time_estimator_enabled(false), m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())) {} - ~GCode() = default; + ~GCodeGenerator() = default; // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). @@ -165,9 +217,19 @@ public: void set_origin(const coordf_t x, const coordf_t y) { this->set_origin(Vec2d(x, y)); } const Point& last_pos() const { return m_last_pos; } // Convert coordinates of the active object to G-code coordinates, possibly adjusted for extruder offset. - Vec2d point_to_gcode(const Point &point) const; + template + Vec2d point_to_gcode(const Eigen::MatrixBase &point) const { + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "GCodeGenerator::point_to_gcode(): first parameter is not a 2D vector"); + return Vec2d(unscaled(point.x()), unscaled(point.y())) + m_origin + - m_config.extruder_offset.get_at(m_writer.extruder()->id()); + } // Convert coordinates of the active object to G-code coordinates, possibly adjusted for extruder offset and quantized to G-code resolution. - Vec2d point_to_gcode_quantized(const Point &point) const; + template + Vec2d point_to_gcode_quantized(const Eigen::MatrixBase &point) const { + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "GCodeGenerator::point_to_gcode_quantized(): first parameter is not a 2D vector"); + Vec2d p = this->point_to_gcode(point); + return { GCodeFormatter::quantize_xyzf(p.x()), GCodeFormatter::quantize_xyzf(p.y()) }; + } Point gcode_to_point(const Vec2d &point) const; const FullPrintConfig &config() const { return m_config; } const Layer* layer() const { return m_layer; } @@ -187,6 +249,8 @@ public: // append full config to the given string static void append_full_config(const Print& print, std::string& str); + // translate full config into a list of items + static void encode_full_config(const Print& print, std::vector>& config); // Object and support extrusions of the same PrintObject at the same print_z. // public, so that it could be accessed by free helper functions from GCode.cpp @@ -250,6 +314,7 @@ private: // Set of object & print layers of the same PrintObject and with the same print_z. const ObjectsLayerToPrint &layers, const LayerTools &layer_tools, + const GCode::SmoothPathCaches &smooth_path_caches, const bool last_layer, // Pairs of PrintObject index and its instance index. const std::vector *ordering, @@ -264,6 +329,7 @@ private: const ToolOrdering &tool_ordering, const std::vector &print_object_instances_ordering, const std::vector> &layers_to_print, + const GCode::SmoothPathCache &smooth_path_cache_global, GCodeOutputStream &output_stream); // Process all layers of a single object instance (sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser @@ -273,17 +339,26 @@ private: const ToolOrdering &tool_ordering, ObjectsLayerToPrint layers_to_print, const size_t single_object_idx, + const GCode::SmoothPathCache &smooth_path_cache_global, GCodeOutputStream &output_stream); void set_last_pos(const Point &pos) { m_last_pos = pos; m_last_pos_defined = true; } bool last_pos_defined() const { return m_last_pos_defined; } void set_extruders(const std::vector &extruder_ids); std::string preamble(); - std::string change_layer(coordf_t print_z); - std::string extrude_entity(const ExtrusionEntity &entity, const std::string_view description, double speed = -1.); - std::string extrude_loop(ExtrusionLoop loop, const std::string_view description, double speed = -1.); - std::string extrude_multi_path(ExtrusionMultiPath multipath, const std::string_view description, double speed = -1.); - std::string extrude_path(ExtrusionPath path, const std::string_view description, double speed = -1.); + std::optional get_helical_layer_change_gcode( + const coordf_t previous_layer_z, + const coordf_t print_z, + const std::string& comment + ); + std::string change_layer(coordf_t previous_layer_z, coordf_t print_z); + std::string extrude_entity(const ExtrusionEntityReference &entity, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed = -1.); + std::string extrude_loop(const ExtrusionLoop &loop, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed = -1.); + std::string extrude_skirt(const ExtrusionLoop &loop_src, const ExtrusionFlow &extrusion_flow_override, + const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed); + + std::string extrude_multi_path(const ExtrusionMultiPath &multipath, bool reverse, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed = -1.); + std::string extrude_path(const ExtrusionPath &path, bool reverse, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed = -1.); struct InstanceToPrint { @@ -317,18 +392,27 @@ private: const ObjectLayerToPrint &layer_to_print, // Container for extruder overrides (when wiping into object or infill). const LayerTools &layer_tools, + // Optional smooth path interpolating extrusion polylines. + const GCode::SmoothPathCache &smooth_path_cache, // Is any extrusion possibly marked as wiping extrusion? const bool is_anything_overridden, // Round 1 (wiping into object or infill) or round 2 (normal extrusions). const bool print_wipe_extrusions); - std::string extrude_support(const ExtrusionEntityCollection &support_fills); + std::string extrude_support(const ExtrusionEntityReferences &support_fills, const GCode::SmoothPathCache &smooth_path_cache); + std::string generate_travel_gcode( + const Points3& travel, + const std::string& comment + ); + Polyline generate_travel_xy_path( + const Point& start, + const Point& end, + const bool needs_retraction, + bool& could_be_wipe_disabled + ); std::string travel_to(const Point &point, ExtrusionRole role, std::string comment); bool needs_retraction(const Polyline &travel, ExtrusionRole role = ExtrusionRole::None); - std::string retract(bool toolchange = false); - std::string unretract() { return m_writer.unlift() + m_writer.unretract(); } - std::string set_extruder(unsigned int extruder_id, double print_z); //B41 std::string set_object_range(Print &print); @@ -338,11 +422,13 @@ private: int unique_id; }; std::unordered_map m_label_data; - + std::string retract_and_wipe(bool toolchange = false); + std::string unretract() { return m_writer.unretract(); } + std::string set_extruder(unsigned int extruder_id, double print_z); + bool line_distancer_is_required(const std::vector& extruder_ids); // Cache for custom seam enforcers/blockers for each layer. SeamPlacer m_seam_placer; - ExtrusionQualityEstimator m_extrusion_quality_estimator; /* Origin of print coordinates expressed in unscaled G-code coordinates. This affects the input arguments supplied to the extrude*() and travel_to() @@ -367,8 +453,8 @@ private: // Input/output from/to custom G-code block, for returning position, retraction etc. DynamicConfig output_config; ConfigOptionFloats *opt_position { nullptr }; - ConfigOptionFloat *opt_zhop { nullptr }; ConfigOptionFloats *opt_e_position { nullptr }; + ConfigOptionFloat *opt_zhop { nullptr }; ConfigOptionFloats *opt_e_retracted { nullptr }; ConfigOptionFloats *opt_e_restart_extra { nullptr }; ConfigOptionFloats *opt_extruded_volume { nullptr }; @@ -384,7 +470,8 @@ private: } m_placeholder_parser_integration; OozePrevention m_ooze_prevention; - Wipe m_wipe; + GCode::Wipe m_wipe; + GCode::LabelObjects m_label_objects; AvoidCrossingPerimeters m_avoid_crossing_perimeters; JPSPathFinder m_avoid_crossing_curled_overhangs; RetractWhenCrossingPerimeters m_retract_when_crossing_perimeters; @@ -407,6 +494,7 @@ private: // In non-sequential mode, all its copies will be printed. const Layer* m_layer; // m_layer is an object layer and it is being printed over raft surface. + std::optional> m_previous_layer_distancer; bool m_object_layer_over_raft; double m_volumetric_speed; // Support for the extrusion role markers. Which marker is active? @@ -427,7 +515,7 @@ private: std::unique_ptr m_spiral_vase; std::unique_ptr m_find_replace; std::unique_ptr m_pressure_equalizer; - std::unique_ptr m_wipe_tower; + std::unique_ptr m_wipe_tower; // Heights (print_z) at which the skirt has already been extruded. std::vector m_skirt_done; @@ -443,7 +531,8 @@ private: // Processor GCodeProcessor m_processor; - std::string _extrude(const ExtrusionPath &path, const std::string_view description, double speed = -1); + std::string _extrude( + const ExtrusionAttributes &attribs, const Geometry::ArcWelder::Path &path, const std::string_view description, double speed = -1); void print_machine_envelope(GCodeOutputStream &file, Print &print); void _print_first_layer_bed_temperature(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); void _print_first_layer_extruder_temperatures(GCodeOutputStream &file, Print &print, const std::string &gcode, unsigned int first_printing_extruder_id, bool wait); @@ -453,8 +542,12 @@ private: // To control print speed of 1st object layer over raft interface. bool object_layer_over_raft() const { return m_object_layer_over_raft; } - friend class Wipe; - friend class WipeTowerIntegration; + // Fill in cache of smooth paths for perimeters, fills and supports of the given object layers. + // Based on params, the paths are either decimated to sparser polylines, or interpolated with circular arches. + static void smooth_path_interpolate(const ObjectLayerToPrint &layers, const GCode::SmoothPathCache::InterpolationParameters ¶ms, GCode::SmoothPathCache &out); + + friend class GCode::Wipe; + friend class GCode::WipeTowerIntegration; friend class PressureEqualizer; }; diff --git a/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp b/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp index c866e13..72e717c 100644 --- a/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp +++ b/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp @@ -730,7 +730,7 @@ static bool any_expolygon_contains(const ExPolygons &ex_polygons, const std::vec return false; } -static bool need_wipe(const GCode &gcodegen, +static bool need_wipe(const GCodeGenerator &gcodegen, const ExPolygons &lslices_offset, const std::vector &lslices_offset_bboxes, const EdgeGrid::Grid &grid_lslices_offset, @@ -738,7 +738,7 @@ static bool need_wipe(const GCode &gcodegen, const Polyline &result_travel, const size_t intersection_count) { - bool z_lift_enabled = gcodegen.config().retract_lift.get_at(gcodegen.writer().extruder()->id()) > 0.; + bool z_lift_enabled = gcodegen.config().travel_max_lift.get_at(gcodegen.writer().extruder()->id()) > 0.; bool wipe_needed = false; // If the original unmodified path doesn't have any intersection with boundary, then it is entirely inside the object otherwise is entirely @@ -1167,7 +1167,7 @@ static void init_boundary(AvoidCrossingPerimeters::Boundary *boundary, Polygons } // Plan travel, which avoids perimeter crossings by following the boundaries of the layer. -Polyline AvoidCrossingPerimeters::travel_to(const GCode &gcodegen, const Point &point, bool *could_be_wipe_disabled) +Polyline AvoidCrossingPerimeters::travel_to(const GCodeGenerator &gcodegen, const Point &point, bool *could_be_wipe_disabled) { // If use_external, then perform the path planning in the world coordinate system (correcting for the gcodegen offset). // Otherwise perform the path planning in the coordinate system of the active object. diff --git a/src/libslic3r/GCode/AvoidCrossingPerimeters.hpp b/src/libslic3r/GCode/AvoidCrossingPerimeters.hpp index eb81c79..5e6d83f 100644 --- a/src/libslic3r/GCode/AvoidCrossingPerimeters.hpp +++ b/src/libslic3r/GCode/AvoidCrossingPerimeters.hpp @@ -8,7 +8,7 @@ namespace Slic3r { // Forward declarations. -class GCode; +class GCodeGenerator; class Layer; class Point; @@ -25,13 +25,13 @@ public: void init_layer(const Layer &layer); - Polyline travel_to(const GCode& gcodegen, const Point& point) + Polyline travel_to(const GCodeGenerator &gcodegen, const Point& point) { bool could_be_wipe_disabled; return this->travel_to(gcodegen, point, &could_be_wipe_disabled); } - Polyline travel_to(const GCode& gcodegen, const Point& point, bool* could_be_wipe_disabled); + Polyline travel_to(const GCodeGenerator &gcodegen, const Point& point, bool* could_be_wipe_disabled); struct Boundary { // Collection of boundaries used for detection of crossing perimeters for travels diff --git a/src/libslic3r/GCode/ConflictChecker.cpp b/src/libslic3r/GCode/ConflictChecker.cpp index 09ef1c8..5c2cdd6 100644 --- a/src/libslic3r/GCode/ConflictChecker.cpp +++ b/src/libslic3r/GCode/ConflictChecker.cpp @@ -89,6 +89,86 @@ inline Grids line_rasterization(const Line &line, int64_t xdist = RasteXDistance } } // namespace RasterizationImpl +static std::vector getFakeExtrusionPathsFromWipeTower(const WipeTowerData& wtd) +{ + float h = wtd.height; + float lh = wtd.first_layer_height; + int d = scale_(wtd.depth); + int w = scale_(wtd.width); + int bd = scale_(wtd.brim_width); + Point minCorner = { -wtd.brim_width, -wtd.brim_width }; + Point maxCorner = { minCorner.x() + w + bd, minCorner.y() + d + bd }; + float width = wtd.width; + float depth = wtd.depth; + float height = wtd.height; + float cone_angle = wtd.cone_angle; + const auto& z_and_depth_pairs = wtd.z_and_depth_pairs; + + const auto [cone_base_R, cone_scale_x] = WipeTower::get_wipe_tower_cone_base(width, height, depth, cone_angle); + + std::vector paths; + for (float hh = 0.f; hh < h; hh += lh) { + + if (hh != 0.f) { + // The wipe tower may be getting smaller. Find the depth for this layer. + size_t i = 0; + for (i=0; i= z_and_depth_pairs[i].first && hh < z_and_depth_pairs[i+1].first) + break; + d = scale_(z_and_depth_pairs[i].second); + minCorner = {0.f, -d/2 + scale_(z_and_depth_pairs.front().second/2.f)}; + maxCorner = { minCorner.x() + w, minCorner.y() + d }; + } + + + ExtrusionPath path({ minCorner, {maxCorner.x(), minCorner.y()}, maxCorner, {minCorner.x(), maxCorner.y()}, minCorner }, + ExtrusionAttributes{ ExtrusionRole::WipeTower, ExtrusionFlow{ 0.0, 0.0, lh } }); + paths.push_back({ path }); + + // We added the border, now add several parallel lines so we can detect an object that is fully inside the tower. + // For now, simply use fixed spacing of 3mm. + for (coord_t y=minCorner.y()+scale_(3.); y 0.) { + path.polyline.clear(); + double r = cone_base_R * (1 - hh/height); + for (double alpha=0; alpha<2.01*M_PI; alpha+=2*M_PI/20.) + path.polyline.points.emplace_back(Point::new_scale(width/2. + r * std::cos(alpha)/cone_scale_x, depth/2. + r * std::sin(alpha))); + paths.back().emplace_back(path); + if (hh == 0.f) { // Cone brim. + for (float bw=wtd.brim_width; bw>0.f; bw-=3.f) { + path.polyline.clear(); + for (double alpha=0; alpha<2.01*M_PI; alpha+=2*M_PI/20.) // see load_wipe_tower_preview, where the same is a bit clearer + path.polyline.points.emplace_back(Point::new_scale( + width/2. + cone_base_R * std::cos(alpha)/cone_scale_x * (1. + cone_scale_x*bw/cone_base_R), + depth/2. + cone_base_R * std::sin(alpha) * (1. + bw/cone_base_R)) + ); + paths.back().emplace_back(path); + } + } + } + + // Only the first layer has brim. + if (hh == 0.f) { + minCorner = minCorner + Point(bd, bd); + maxCorner = maxCorner - Point(bd, bd); + } + } + + // Rotate and translate the tower into the final position. + for (ExtrusionPaths& ps : paths) { + for (ExtrusionPath& p : ps) { + p.polyline.rotate(Geometry::deg2rad(wtd.rotation_angle)); + p.polyline.translate(scale_(wtd.position.x()), scale_(wtd.position.y())); + } + } + + return paths; +} void LinesBucketQueue::emplace_back_bucket(std::vector &&paths, const void *objPtr, Points offsets) { if (_objsPtrToId.find(objPtr) == _objsPtrToId.end()) { @@ -165,14 +245,14 @@ ExtrusionPaths getExtrusionPathsFromLayer(LayerRegionPtrs layerRegionPtrs) return paths; } -ExtrusionPaths getExtrusionPathsFromSupportLayer(SupportLayer *supportLayer) +ExtrusionPaths getExtrusionPathsFromSupportLayer(const SupportLayer *supportLayer) { ExtrusionPaths paths; getExtrusionPathsFromEntity(&supportLayer->support_fills, paths); return paths; } -std::pair, std::vector> getAllLayersExtrusionPathsFromObject(PrintObject *obj) +std::pair, std::vector> getAllLayersExtrusionPathsFromObject(const PrintObject *obj) { std::vector objPaths, supportPaths; @@ -203,17 +283,22 @@ ConflictComputeOpt ConflictChecker::find_inter_of_lines(const LineWithIDs &lines return {}; } -ConflictResultOpt ConflictChecker::find_inter_of_lines_in_diff_objs(PrintObjectPtrs objs, - std::optional wtdptr) // find the first intersection point of lines in different objects +ConflictResultOpt ConflictChecker::find_inter_of_lines_in_diff_objs(SpanOfConstPtrs objs, + const WipeTowerData& wipe_tower_data) // find the first intersection point of lines in different objects { if (objs.empty() || (objs.size() == 1 && objs.front()->instances().size() == 1)) { return {}; } + // The code ported from BS uses void* to identify objects... + // Let's use the address of this variable to represent the wipe tower. + int wtptr = 0; LinesBucketQueue conflictQueue; - if (wtdptr.has_value()) { // wipe tower at 0 by default - std::vector wtpaths = (*wtdptr)->getFakeExtrusionPathsFromWipeTower(); - conflictQueue.emplace_back_bucket(std::move(wtpaths), *wtdptr, Points{Point((*wtdptr)->plate_origin)}); + if (! wipe_tower_data.z_and_depth_pairs.empty()) { + // The wipe tower is being generated. + const Vec2d plate_origin = Vec2d::Zero(); + std::vector wtpaths = getFakeExtrusionPathsFromWipeTower(wipe_tower_data); + conflictQueue.emplace_back_bucket(std::move(wtpaths), &wtptr, Points{Point(plate_origin)}); } - for (PrintObject *obj : objs) { + for (const PrintObject *obj : objs) { std::pair, std::vector> layers = getAllLayersExtrusionPathsFromObject(obj); Points instances_shifts; @@ -256,14 +341,12 @@ ConflictResultOpt ConflictChecker::find_inter_of_lines_in_diff_objs(PrintObjectP const void *ptr1 = conflictQueue.idToObjsPtr(conflict[0].first._obj1); const void *ptr2 = conflictQueue.idToObjsPtr(conflict[0].first._obj2); double conflictHeight = conflict[0].second; - if (wtdptr.has_value()) { - const FakeWipeTower* wtdp = *wtdptr; - if (ptr1 == wtdp || ptr2 == wtdp) { - if (ptr2 == wtdp) { std::swap(ptr1, ptr2); } + if (ptr1 == &wtptr || ptr2 == &wtptr) { + assert(! wipe_tower_data.z_and_depth_pairs.empty()); + if (ptr2 == &wtptr) { std::swap(ptr1, ptr2); } const PrintObject *obj2 = reinterpret_cast(ptr2); return std::make_optional("WipeTower", obj2->model_object()->name, conflictHeight, nullptr, ptr2); } - } const PrintObject *obj1 = reinterpret_cast(ptr1); const PrintObject *obj2 = reinterpret_cast(ptr2); return std::make_optional(obj1->model_object()->name, obj2->model_object()->name, conflictHeight, ptr1, ptr2); diff --git a/src/libslic3r/GCode/ConflictChecker.hpp b/src/libslic3r/GCode/ConflictChecker.hpp index 344018f..f5fe422 100644 --- a/src/libslic3r/GCode/ConflictChecker.hpp +++ b/src/libslic3r/GCode/ConflictChecker.hpp @@ -1,10 +1,7 @@ #ifndef slic3r_ConflictChecker_hpp_ #define slic3r_ConflictChecker_hpp_ -#include "../Utils.hpp" -#include "../Model.hpp" -#include "../Print.hpp" -#include "../Layer.hpp" +#include "libslic3r/Print.hpp" #include #include @@ -43,7 +40,7 @@ public: void raise() { if (valid()) { - if (_piles[_curPileIdx].empty() == false) { _curHeight += _piles[_curPileIdx].front().height; } + if (_piles[_curPileIdx].empty() == false) { _curHeight += _piles[_curPileIdx].front().height(); } _curPileIdx++; } } @@ -119,7 +116,7 @@ using ConflictObjName = std::optional>; struct ConflictChecker { - static ConflictResultOpt find_inter_of_lines_in_diff_objs(PrintObjectPtrs objs, std::optional wtdptr); + static ConflictResultOpt find_inter_of_lines_in_diff_objs(SpanOfConstPtrs objs, const WipeTowerData& wtd); static ConflictComputeOpt find_inter_of_lines(const LineWithIDs &lines); static ConflictComputeOpt line_intersect(const LineWithID &l1, const LineWithID &l2); }; diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 83135e4..167dad0 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -19,7 +19,7 @@ namespace Slic3r { -CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_toolchange_prefix(gcodegen.writer().toolchange_prefix()), m_current_extruder(0) +CoolingBuffer::CoolingBuffer(GCodeGenerator &gcodegen) : m_config(gcodegen.config()), m_toolchange_prefix(gcodegen.writer().toolchange_prefix()), m_current_extruder(0) { this->reset(gcodegen.writer().get_position()); @@ -33,39 +33,45 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t void CoolingBuffer::reset(const Vec3d &position) { - m_current_pos.assign(5, 0.f); - m_current_pos[0] = float(position.x()); - m_current_pos[1] = float(position.y()); - m_current_pos[2] = float(position.z()); - m_current_pos[4] = float(m_config.travel_speed.value); - m_fan_speed = -1; -//Y12 - m_fan_speed = -1; + assert(m_current_pos.size() == 5); + m_current_pos[AxisIdx::X] = float(position.x()); + m_current_pos[AxisIdx::Y] = float(position.y()); + m_current_pos[AxisIdx::Z] = float(position.z()); + m_current_pos[AxisIdx::E] = 0.f; + m_current_pos[AxisIdx::F] = float(m_config.travel_speed.value); m_fan_speed = -1; } struct CoolingLine { - enum Type { + enum Type : uint32_t { TYPE_SET_TOOL = 1 << 0, TYPE_EXTRUDE_END = 1 << 1, TYPE_BRIDGE_FAN_START = 1 << 2, TYPE_BRIDGE_FAN_END = 1 << 3, TYPE_G0 = 1 << 4, TYPE_G1 = 1 << 5, - TYPE_ADJUSTABLE = 1 << 6, - TYPE_EXTERNAL_PERIMETER = 1 << 7, + // G2 or G3: Arc interpolation + TYPE_G2G3 = 1 << 6, + TYPE_ADJUSTABLE = 1 << 7, + TYPE_EXTERNAL_PERIMETER = 1 << 8, + // Arc interpolation, counter-clockwise. + TYPE_G2G3_CCW = 1 << 9, + // Arc interpolation, arc defined by IJ (offset of arc center from its start position). + TYPE_G2G3_IJ = 1 << 10, + // Arc interpolation, arc defined by R (arc radius, positive - smaller, negative - larger). + TYPE_G2G3_R = 1 << 11, // The line sets a feedrate. - TYPE_HAS_F = 1 << 8, - TYPE_WIPE = 1 << 9, - TYPE_G4 = 1 << 10, - TYPE_G92 = 1 << 11, + TYPE_HAS_F = 1 << 12, + TYPE_WIPE = 1 << 13, + TYPE_G4 = 1 << 14, + TYPE_G92 = 1 << 15, // Would be TYPE_ADJUSTABLE, but the block of G-code lines has zero extrusion length, thus the block // cannot have its speed adjusted. This should not happen (sic!). - TYPE_ADJUSTABLE_EMPTY = 1 << 12, + TYPE_ADJUSTABLE_EMPTY = 1 << 16, // Custom fan speed (introduced for overhang fan speed) - TYPE_SET_FAN_SPEED = 1 << 13, - TYPE_RESET_FAN_SPEED = 1 << 14, + TYPE_SET_FAN_SPEED = 1 << 17, + TYPE_RESET_FAN_SPEED = 1 << 18, }; CoolingLine(unsigned int type, size_t line_start, size_t line_end) : @@ -327,7 +333,7 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b // Parse the layer G-code for the moves, which could be adjusted. // Return the list of parsed lines, bucketed by an extruder. -std::vector CoolingBuffer::parse_layer_gcode(const std::string &gcode, std::vector ¤t_pos) const +std::vector CoolingBuffer::parse_layer_gcode(const std::string &gcode, std::array ¤t_pos) const { std::vector per_extruder_adjustments(m_extruder_ids.size()); std::vector map_extruder_to_per_extruder_adjustment(m_num_extruders, 0); @@ -350,7 +356,7 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: // for a sequence of extrusion moves. size_t active_speed_modifier = size_t(-1); - std::vector new_pos; + std::array new_pos; for (; *line_start != 0; line_start = line_end) { while (*line_end != '\n' && *line_end != 0) @@ -365,12 +371,20 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: line.type = CoolingLine::TYPE_G0; else if (boost::starts_with(sline, "G1 ")) line.type = CoolingLine::TYPE_G1; + else if (boost::starts_with(sline, "G2 ")) + // Arc, clockwise. + line.type = CoolingLine::TYPE_G2G3; + else if (boost::starts_with(sline, "G3 ")) + // Arc, counter-clockwise. + line.type = CoolingLine::TYPE_G2G3 | CoolingLine::TYPE_G2G3_CCW; else if (boost::starts_with(sline, "G92 ")) line.type = CoolingLine::TYPE_G92; if (line.type) { - // G0, G1 or G92 + // G0, G1, G2, G3 or G92 + // Initialize current_pos from new_pos, set IJKR to zero. + std::fill(std::copy(std::begin(current_pos), std::end(current_pos), std::begin(new_pos)), + std::end(new_pos), 0.f); // Parse the G-code line. - new_pos = current_pos; for (auto c = sline.begin() + 3;;) { // Skip whitespaces. for (; c != sline.end() && (*c == ' ' || *c == '\t'); ++ c); @@ -379,21 +393,31 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: // Parse the axis. size_t axis = (*c >= 'X' && *c <= 'Z') ? (*c - 'X') : - (*c == extrusion_axis) ? 3 : (*c == 'F') ? 4 : size_t(-1); + (*c == extrusion_axis) ? AxisIdx::E : (*c == 'F') ? AxisIdx::F : + (*c >= 'I' && *c <= 'K') ? int(AxisIdx::I) + (*c - 'I') : + (*c == 'R') ? AxisIdx::R : size_t(-1); if (axis != size_t(-1)) { //auto [pend, ec] = fast_float::from_chars(&*(++ c), sline.data() + sline.size(), new_pos[axis]); - if (axis == 4) { + if (axis == AxisIdx::F) { // Convert mm/min to mm/sec. - new_pos[4] /= 60.f; + new_pos[AxisIdx::F] /= 60.f; if ((line.type & CoolingLine::TYPE_G92) == 0) // This is G0 or G1 line and it sets the feedrate. This mark is used for reducing the duplicate F calls. line.type |= CoolingLine::TYPE_HAS_F; - } + } else if (axis >= AxisIdx::I && axis <= AxisIdx::J) + line.type |= CoolingLine::TYPE_G2G3_IJ; + else if (axis == AxisIdx::R) + line.type |= CoolingLine::TYPE_G2G3_R; } // Skip this word. for (; c != sline.end() && *c != ' ' && *c != '\t'; ++ c); } + // If G2 or G3, then either center of the arc or radius has to be defined. + assert(! (line.type & CoolingLine::TYPE_G2G3) || + (line.type & (CoolingLine::TYPE_G2G3_IJ | CoolingLine::TYPE_G2G3_R))); + // Arc is defined either by IJ or by R, not by both. + assert(! ((line.type & CoolingLine::TYPE_G2G3_IJ) && (line.type & CoolingLine::TYPE_G2G3_R))); bool external_perimeter = boost::contains(sline, ";_EXTERNAL_PERIMETER"); bool wipe = boost::contains(sline, ";_WIPE"); if (external_perimeter) @@ -405,23 +429,41 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: active_speed_modifier = adjustment->lines.size(); } if ((line.type & CoolingLine::TYPE_G92) == 0) { - // G0 or G1. Calculate the duration. + // G0, G1, G2, G3. Calculate the duration. + assert((line.type & CoolingLine::TYPE_G0) != 0 + (line.type & CoolingLine::TYPE_G1) != 0 + (line.type & CoolingLine::TYPE_G2G3) != 0 == 1); if (m_config.use_relative_e_distances.value) // Reset extruder accumulator. - current_pos[3] = 0.f; + current_pos[AxisIdx::E] = 0.f; float dif[4]; for (size_t i = 0; i < 4; ++ i) dif[i] = new_pos[i] - current_pos[i]; - float dxy2 = dif[0] * dif[0] + dif[1] * dif[1]; - float dxyz2 = dxy2 + dif[2] * dif[2]; + float dxy2; + if (line.type & CoolingLine::TYPE_G2G3) { + // Measure arc length. + if (line.type & CoolingLine::TYPE_G2G3_IJ) { + dxy2 = sqr(Geometry::ArcWelder::arc_length( + Vec2d(current_pos[AxisIdx::X], current_pos[AxisIdx::Y]), + Vec2d(new_pos[AxisIdx::X], new_pos[AxisIdx::Y]), + Vec2d(current_pos[AxisIdx::X] + new_pos[AxisIdx::I], current_pos[AxisIdx::Y] + new_pos[AxisIdx::J]), + line.type & CoolingLine::TYPE_G2G3_CCW)); + } else if (line.type & CoolingLine::TYPE_G2G3_R) { + dxy2 = sqr(Geometry::ArcWelder::arc_length( + Vec2d(current_pos[AxisIdx::X], current_pos[AxisIdx::Y]), + Vec2d(new_pos[AxisIdx::X], new_pos[AxisIdx::Y]), + double(new_pos[AxisIdx::R]))); + } else + dxy2 = 0; + } else + dxy2 = sqr(dif[AxisIdx::X]) + sqr(dif[AxisIdx::Y]); + float dxyz2 = dxy2 + sqr(dif[AxisIdx::Z]); if (dxyz2 > 0.f) { // Movement in xyz, calculate time from the xyz Euclidian distance. line.length = sqrt(dxyz2); - } else if (std::abs(dif[3]) > 0.f) { + } else if (std::abs(dif[AxisIdx::E]) > 0.f) { // Movement in the extruder axis. - line.length = std::abs(dif[3]); + line.length = std::abs(dif[AxisIdx::E]); } - line.feedrate = new_pos[4]; + line.feedrate = new_pos[AxisIdx::F]; assert((line.type & CoolingLine::TYPE_ADJUSTABLE) == 0 || line.feedrate > 0.f); if (line.length > 0) { assert(line.feedrate > 0); @@ -433,7 +475,7 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: assert(adjustment->min_print_speed >= 0); line.time_max = (adjustment->min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / adjustment->min_print_speed); } - if (active_speed_modifier < adjustment->lines.size() && (line.type & CoolingLine::TYPE_G1)) { + if (active_speed_modifier < adjustment->lines.size() && (line.type & (CoolingLine::TYPE_G1 | CoolingLine::TYPE_G2G3))) { // Inside the ";_EXTRUDE_SET_SPEED" blocks, there must not be a G1 Fxx entry. assert((line.type & CoolingLine::TYPE_HAS_F) == 0); CoolingLine &sm = adjustment->lines[active_speed_modifier]; @@ -450,7 +492,7 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: line.type = 0; } } - current_pos = std::move(new_pos); + std::copy(std::begin(new_pos), std::begin(new_pos) + 5, std::begin(current_pos)); } else if (boost::starts_with(sline, ";_EXTRUDE_END")) { // Closing a block of non-zero length extrusion moves. line.type = CoolingLine::TYPE_EXTRUDE_END; @@ -508,16 +550,18 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: } else line.time = 0; line.time_max = line.time; - } else if (boost::contains(sline, ";_SET_FAN_SPEED")) { + } + + if (boost::contains(sline, ";_SET_FAN_SPEED")) { auto speed_start = sline.find_last_of('D'); int speed = 0; for (char num : sline.substr(speed_start + 1)) { speed = speed * 10 + (num - '0'); } - line.type = CoolingLine::TYPE_SET_FAN_SPEED; + line.type |= CoolingLine::TYPE_SET_FAN_SPEED; line.fan_speed = speed; } else if (boost::contains(sline, ";_RESET_FAN_SPEED")) { - line.type = CoolingLine::TYPE_RESET_FAN_SPEED; + line.type |= CoolingLine::TYPE_RESET_FAN_SPEED; } if (line.type != 0) diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 85b3ed3..cd985d5 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -7,7 +7,7 @@ namespace Slic3r { -class GCode; +class GCodeGenerator; class Layer; struct PerExtruderAdjustments; @@ -22,7 +22,7 @@ struct PerExtruderAdjustments; // class CoolingBuffer { public: - CoolingBuffer(GCode &gcodegen); + CoolingBuffer(GCodeGenerator &gcodegen); void reset(const Vec3d &position); void set_current_extruder(unsigned int extruder_id) { m_current_extruder = extruder_id; } std::string process_layer(std::string &&gcode, size_t layer_id, bool flush); @@ -31,7 +31,7 @@ public: private: CoolingBuffer& operator=(const CoolingBuffer&) = delete; - std::vector parse_layer_gcode(const std::string &gcode, std::vector ¤t_pos) const; + std::vector parse_layer_gcode(const std::string &gcode, std::array ¤t_pos) const; float calculate_layer_slowdown(std::vector &per_extruder_adjustments); // Apply slow down over G-code lines stored in per_extruder_adjustments, enable fan if needed. // Returns the adjusted G-code. @@ -40,9 +40,11 @@ private: // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. - // X,Y,Z,E,F std::vector m_axis; - std::vector m_current_pos; + enum AxisIdx : int { + X = 0, Y, Z, E, F, I, J, K, R, Count + }; + std::array m_current_pos; // Current known fan speed or -1 if not known yet. int m_fan_speed; //Y12 @@ -54,7 +56,7 @@ private: // Highest of m_extruder_ids plus 1. unsigned int m_num_extruders { 0 }; const std::string m_toolchange_prefix; - // Referencs GCode::m_config, which is FullPrintConfig. While the PrintObjectConfig slice of FullPrintConfig is being modified, + // Referencs GCodeGenerator::m_config, which is FullPrintConfig. While the PrintObjectConfig slice of FullPrintConfig is being modified, // the PrintConfig slice of FullPrintConfig is constant, thus no thread synchronization is required. const PrintConfig &m_config; unsigned int m_current_extruder; diff --git a/src/libslic3r/GCode/DataType.h b/src/libslic3r/GCode/DataType.h deleted file mode 100644 index 1c8a443..0000000 --- a/src/libslic3r/GCode/DataType.h +++ /dev/null @@ -1,144 +0,0 @@ -#ifndef _DATA_TYPE_H_ -#define _DATA_TYPE_H_ - -//#include "framework.h" -#include - -#ifndef null -#define null 0 -#endif -#ifndef NULL -#define NULL 0 -#endif - - -#ifndef FALSE -#define FALSE 0 -#endif -#ifndef TRUE -#define TRUE 1 -#endif -#ifndef U8 -typedef unsigned char U8; -#endif - -#ifndef S8 -typedef signed char S8; -#endif - -#ifndef U16 -typedef unsigned short U16; -#endif - -#ifndef S16 -typedef signed short S16; -#endif - -#ifndef U32 -typedef unsigned int U32; -#endif - -#ifndef S32 -typedef signed int S32; -#endif - -#ifndef S64 -typedef signed long long S64; -#endif - -#ifndef U64 -typedef unsigned long long U64; -#endif - -#ifndef FP32 -typedef float FP32; -#endif - -#ifndef FP64 -typedef double FP64; -#endif - -#ifndef Pixel_t -typedef unsigned short Pixel_t; -#endif - -#ifndef UINT32 -typedef unsigned int UINT32; -#endif - - -#ifndef INT -typedef int INT; -#endif - -// #ifndef INT32 -// typedef int INT32; -// #endif - -typedef unsigned char INT8U; /* Unsigned 8 bit quantity */ -typedef signed char INT8S; /* Signed 8 bit quantity */ -typedef unsigned short INT16U; /* Unsigned 16 bit quantity */ -typedef signed short INT16S; /* Signed 16 bit quantity */ -typedef unsigned int INT32U; /* Unsigned 32 bit quantity */ -typedef signed int INT32S; /* Signed 32 bit quantity */ -typedef unsigned long long INT64U; -typedef signed long long INT64S; - -typedef float FP32; /* Single precision floating point */ -typedef double FP64; /* Double precision floating point */ - - -typedef struct -{ - U16 star; - U16 end; -}PosLaction; -typedef struct -{ - int a0; - int a1; - int a2; - int a3; - int a4; - int a5; - int a6; - int a7; - int a8; - int a9; - int a10; - int a11; - int a12; - int a13; - int a14; - int a15; -}bytes_64Bytes; -typedef struct -{ - bytes_64Bytes a0; - bytes_64Bytes a1; - bytes_64Bytes a2; - bytes_64Bytes a3; -}bytes_256Bytes; -typedef struct -{ - bytes_64Bytes a0; - bytes_64Bytes a1; - bytes_64Bytes a2; - bytes_64Bytes a3; - bytes_64Bytes a4; - bytes_64Bytes a5; - bytes_64Bytes a6; - bytes_64Bytes a7; - bytes_64Bytes a8; - bytes_64Bytes a9; - bytes_64Bytes a10; - bytes_64Bytes a11; - bytes_64Bytes a12; - bytes_64Bytes a13; - bytes_64Bytes a14; - bytes_64Bytes a15; -}bytes_1024Bytes; -#endif - - - diff --git a/src/libslic3r/GCode/ExtrusionProcessor.cpp b/src/libslic3r/GCode/ExtrusionProcessor.cpp new file mode 100644 index 0000000..8ebc5d3 --- /dev/null +++ b/src/libslic3r/GCode/ExtrusionProcessor.cpp @@ -0,0 +1,216 @@ +#include "ExtrusionProcessor.hpp" +#include + +namespace Slic3r { namespace ExtrusionProcessor { + +ExtrusionPaths calculate_and_split_overhanging_extrusions(const ExtrusionPath &path, + const AABBTreeLines::LinesDistancer &unscaled_prev_layer, + const AABBTreeLines::LinesDistancer &prev_layer_curled_lines) +{ + std::vector extended_points = estimate_points_properties(path.polyline.points, + unscaled_prev_layer, path.width()); + std::vector> calculated_distances(extended_points.size()); + + for (size_t i = 0; i < extended_points.size(); i++) { + const ExtendedPoint &curr = extended_points[i]; + const ExtendedPoint &next = extended_points[i + 1 < extended_points.size() ? i + 1 : i]; + + // The following code artifically increases the distance to provide slowdown for extrusions that are over curled lines + float proximity_to_curled_lines = 0.0; + const double dist_limit = 10.0 * path.width(); + { + Vec2d middle = 0.5 * (curr.position + next.position); + auto line_indices = prev_layer_curled_lines.all_lines_in_radius(Point::new_scale(middle), scale_(dist_limit)); + if (!line_indices.empty()) { + double len = (next.position - curr.position).norm(); + // For long lines, there is a problem with the additional slowdown. If by accident, there is small curled line near the middle + // of this long line + // The whole segment gets slower unnecesarily. For these long lines, we do additional check whether it is worth slowing down. + // NOTE that this is still quite rough approximation, e.g. we are still checking lines only near the middle point + // TODO maybe split the lines into smaller segments before running this alg? but can be demanding, and GCode will be huge + if (len > 8) { + Vec2d dir = Vec2d(next.position - curr.position) / len; + Vec2d right = Vec2d(-dir.y(), dir.x()); + + Polygon box_of_influence = { + scaled(Vec2d(curr.position + right * dist_limit)), + scaled(Vec2d(next.position + right * dist_limit)), + scaled(Vec2d(next.position - right * dist_limit)), + scaled(Vec2d(curr.position - right * dist_limit)), + }; + + double projected_lengths_sum = 0; + for (size_t idx : line_indices) { + const CurledLine &line = prev_layer_curled_lines.get_line(idx); + Lines inside = intersection_ln({{line.a, line.b}}, {box_of_influence}); + if (inside.empty()) + continue; + double projected_length = abs(dir.dot(unscaled(Vec2d((inside.back().b - inside.back().a).cast())))); + projected_lengths_sum += projected_length; + } + if (projected_lengths_sum < 0.4 * len) { + line_indices.clear(); + } + } + + for (size_t idx : line_indices) { + const CurledLine &line = prev_layer_curled_lines.get_line(idx); + float distance_from_curled = unscaled(line_alg::distance_to(line, Point::new_scale(middle))); + float proximity = (1.0 - (distance_from_curled / dist_limit)) * (1.0 - (distance_from_curled / dist_limit)) * + (line.curled_height / (path.height() * 10.0f)); // max_curled_height_factor from SupportSpotGenerator + proximity_to_curled_lines = std::max(proximity_to_curled_lines, proximity); + } + } + } + calculated_distances[i].first = std::max(curr.distance, next.distance); + calculated_distances[i].second = proximity_to_curled_lines; + } + + ExtrusionPaths result; + ExtrusionAttributes new_attrs = path.attributes(); + new_attrs.overhang_attributes = std::optional( + {calculated_distances[0].first, calculated_distances[0].first, calculated_distances[0].second}); + result.emplace_back(new_attrs); + result.back().polyline.append(Point::new_scale(extended_points[0].position)); + size_t sequence_start_index = 0; + for (size_t i = 1; i < extended_points.size(); i++) { + result.back().polyline.append(Point::new_scale(extended_points[i].position)); + result.back().overhang_attributes_mutable()->end_distance_from_prev_layer = extended_points[i].distance; + + if (std::abs(calculated_distances[sequence_start_index].first - calculated_distances[i].first) < 0.001 * path.attributes().width && + std::abs(calculated_distances[sequence_start_index].second - calculated_distances[i].second) < 0.001) { + // do not start new path, the attributes are similar enough + // NOTE: a larger tolerance may be applied here. However, it makes the gcode preview much less smooth + // (But it has very likely zero impact on the print quality.) + } else if (i + 1 < extended_points.size()) { // do not start new path if this is last point! + // start new path, parameters differ + new_attrs.overhang_attributes->start_distance_from_prev_layer = calculated_distances[i].first; + new_attrs.overhang_attributes->end_distance_from_prev_layer = calculated_distances[i].first; + new_attrs.overhang_attributes->proximity_to_curled_lines = calculated_distances[i].second; + sequence_start_index = i; + result.emplace_back(new_attrs); + result.back().polyline.append(Point::new_scale(extended_points[i].position)); + } + } + + return result; +}; + +ExtrusionEntityCollection calculate_and_split_overhanging_extrusions(const ExtrusionEntityCollection *ecc, + const AABBTreeLines::LinesDistancer &unscaled_prev_layer, + const AABBTreeLines::LinesDistancer &prev_layer_curled_lines) +{ + ExtrusionEntityCollection result{}; + result.no_sort = ecc->no_sort; + for (const auto *e : ecc->entities) { + if (auto *col = dynamic_cast(e)) { + result.append(calculate_and_split_overhanging_extrusions(col, unscaled_prev_layer, prev_layer_curled_lines)); + } else if (auto *loop = dynamic_cast(e)) { + ExtrusionLoop new_loop = *loop; + new_loop.paths.clear(); + for (const ExtrusionPath &p : loop->paths) { + auto paths = calculate_and_split_overhanging_extrusions(p, unscaled_prev_layer, prev_layer_curled_lines); + new_loop.paths.insert(new_loop.paths.end(), paths.begin(), paths.end()); + } + result.append(new_loop); + } else if (auto *mp = dynamic_cast(e)) { + ExtrusionMultiPath new_mp = *mp; + new_mp.paths.clear(); + for (const ExtrusionPath &p : mp->paths) { + auto paths = calculate_and_split_overhanging_extrusions(p, unscaled_prev_layer, prev_layer_curled_lines); + new_mp.paths.insert(new_mp.paths.end(), paths.begin(), paths.end()); + } + result.append(new_mp); + } else if (auto *op = dynamic_cast(e)) { + auto paths = calculate_and_split_overhanging_extrusions(*op, unscaled_prev_layer, prev_layer_curled_lines); + for (const ExtrusionPath &p : paths) { + result.append(ExtrusionPathOriented(p.polyline, p.attributes())); + } + } else if (auto *p = dynamic_cast(e)) { + auto paths = calculate_and_split_overhanging_extrusions(*p, unscaled_prev_layer, prev_layer_curled_lines); + result.append(paths); + } else { + throw Slic3r::InvalidArgument("Unknown extrusion entity type"); + } + } + return result; +}; + + +std::pair calculate_overhang_speed(const ExtrusionAttributes &attributes, + const FullPrintConfig &config, + size_t extruder_id, + float external_perim_reference_speed, + float default_speed) +{ + assert(attributes.overhang_attributes.has_value()); + std::vector> overhangs_with_speeds = { + {100, ConfigOptionFloatOrPercent{default_speed, false}}}; + if (config.enable_dynamic_overhang_speeds) { + overhangs_with_speeds = {{0, config.overhang_speed_0}, + {25, config.overhang_speed_1}, + {50, config.overhang_speed_2}, + {75, config.overhang_speed_3}, + {100, ConfigOptionFloatOrPercent{default_speed, false}}}; + } + + std::vector> overhang_with_fan_speeds = {{100, ConfigOptionInts{0}}}; + if (config.enable_dynamic_fan_speeds.get_at(extruder_id)) { + overhang_with_fan_speeds = {{0, config.overhang_fan_speed_0}, + {25, config.overhang_fan_speed_1}, + {50, config.overhang_fan_speed_2}, + {75, config.overhang_fan_speed_3}, + {100, ConfigOptionInts{0}}}; + } + + float speed_base = external_perim_reference_speed > 0 ? external_perim_reference_speed : default_speed; + std::map speed_sections; + for (size_t i = 0; i < overhangs_with_speeds.size(); i++) { + float distance = attributes.width * (1.0 - (overhangs_with_speeds[i].first / 100.0)); + float speed = overhangs_with_speeds[i].second.percent ? (speed_base * overhangs_with_speeds[i].second.value / 100.0) : + overhangs_with_speeds[i].second.value; + if (speed < EPSILON) + speed = speed_base; + speed_sections[distance] = speed; + } + + std::map fan_speed_sections; + for (size_t i = 0; i < overhang_with_fan_speeds.size(); i++) { + float distance = attributes.width * (1.0 - (overhang_with_fan_speeds[i].first / 100.0)); + float fan_speed = overhang_with_fan_speeds[i].second.get_at(extruder_id); + fan_speed_sections[distance] = fan_speed; + } + + auto interpolate_speed = [](const std::map &values, float distance) { + auto upper_dist = values.lower_bound(distance); + if (upper_dist == values.end()) { + return values.rbegin()->second; + } + if (upper_dist == values.begin()) { + return upper_dist->second; + } + + auto lower_dist = std::prev(upper_dist); + float t = (distance - lower_dist->first) / (upper_dist->first - lower_dist->first); + return (1.0f - t) * lower_dist->second + t * upper_dist->second; + }; + + float extrusion_speed = std::min(interpolate_speed(speed_sections, attributes.overhang_attributes->start_distance_from_prev_layer), + interpolate_speed(speed_sections, attributes.overhang_attributes->end_distance_from_prev_layer)); + float curled_base_speed = interpolate_speed(speed_sections, + attributes.width * attributes.overhang_attributes->proximity_to_curled_lines); + float final_speed = std::min(curled_base_speed, extrusion_speed); + float fan_speed = std::min(interpolate_speed(fan_speed_sections, attributes.overhang_attributes->start_distance_from_prev_layer), + interpolate_speed(fan_speed_sections, attributes.overhang_attributes->end_distance_from_prev_layer)); + + if (!config.enable_dynamic_overhang_speeds) { + final_speed = -1; + } + if (!config.enable_dynamic_fan_speeds.get_at(extruder_id)) { + fan_speed = -1; + } + + return {final_speed, fan_speed}; +} + +}} // namespace Slic3r::ExtrusionProcessor \ No newline at end of file diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 5314e9a..0a33316 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -14,19 +14,23 @@ #include "../Flow.hpp" #include "../Config.hpp" #include "../Line.hpp" +#include "../Exception.hpp" +#include "../PrintConfig.hpp" #include +#include #include #include #include #include #include +#include #include #include #include #include -namespace Slic3r { +namespace Slic3r { namespace ExtrusionProcessor { struct ExtendedPoint { @@ -41,6 +45,33 @@ std::vector estimate_points_properties(const POINTS float flow_width, float max_line_length = -1.0f) { + bool looped = input_points.front() == input_points.back(); + std::function get_prev_index = [](size_t idx, size_t count) { + if (idx > 0) { + return idx - 1; + } else + return idx; + }; + if (looped) { + get_prev_index = [](size_t idx, size_t count) { + if (idx == 0) + idx = count; + return --idx; + }; + }; + std::function get_next_index = [](size_t idx, size_t size) { + if (idx + 1 < size) { + return idx + 1; + } else + return idx; + }; + if (looped) { + get_next_index = [](size_t idx, size_t count) { + if (++idx == count) + idx = 0; + return idx; + }; + }; using P = typename POINTS::value_type; using AABBScalar = typename AABBTreeLines::LinesDistancer::Scalar; @@ -54,19 +85,22 @@ std::vector estimate_points_properties(const POINTS { ExtendedPoint start_point{maybe_unscale(input_points.front())}; - auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(start_point.position.cast()); + auto [distance, nearest_line, + x] = unscaled_prev_layer.template distance_from_lines_extra(start_point.position.cast()); start_point.distance = distance + boundary_offset; points.push_back(start_point); } for (size_t i = 1; i < input_points.size(); i++) { ExtendedPoint next_point{maybe_unscale(input_points[i])}; - auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(next_point.position.cast()); + auto [distance, nearest_line, + x] = unscaled_prev_layer.template distance_from_lines_extra(next_point.position.cast()); next_point.distance = distance + boundary_offset; if (ADD_INTERSECTIONS && ((points.back().distance > boundary_offset + EPSILON) != (next_point.distance > boundary_offset + EPSILON))) { const ExtendedPoint &prev_point = points.back(); - auto intersections = unscaled_prev_layer.template intersections_with_line(L{prev_point.position.cast(), next_point.position.cast()}); + auto intersections = unscaled_prev_layer.template intersections_with_line( + L{prev_point.position.cast(), next_point.position.cast()}); for (const auto &intersection : intersections) { ExtendedPoint p{}; p.position = intersection.first.template cast(); @@ -85,18 +119,19 @@ std::vector estimate_points_properties(const POINTS const ExtendedPoint &curr = points[point_idx]; const ExtendedPoint &next = points[point_idx + 1]; - if ((curr.distance > 0 && curr.distance < boundary_offset + 2.0f) || - (next.distance > 0 && next.distance < boundary_offset + 2.0f)) { + if ((curr.distance > -boundary_offset && curr.distance < boundary_offset + 2.0f) || + (next.distance > -boundary_offset && next.distance < boundary_offset + 2.0f)) { double line_len = (next.position - curr.position).norm(); if (line_len > 4.0f) { - double a0 = std::clamp((curr.distance + 2 * boundary_offset) / line_len, 0.0, 1.0); - double a1 = std::clamp(1.0f - (next.distance + 2 * boundary_offset) / line_len, 0.0, 1.0); + double a0 = std::clamp((curr.distance + 3 * boundary_offset) / line_len, 0.0, 1.0); + double a1 = std::clamp(1.0f - (next.distance + 3 * boundary_offset) / line_len, 0.0, 1.0); double t0 = std::min(a0, a1); double t1 = std::max(a0, a1); if (t0 < 1.0) { auto p0 = curr.position + t0 * (next.position - curr.position); - auto [p0_dist, p0_near_l, p0_x] = unscaled_prev_layer.template distance_from_lines_extra(p0.cast()); + auto [p0_dist, p0_near_l, + p0_x] = unscaled_prev_layer.template distance_from_lines_extra(p0.cast()); ExtendedPoint new_p{}; new_p.position = p0; new_p.distance = float(p0_dist + boundary_offset); @@ -104,7 +139,8 @@ std::vector estimate_points_properties(const POINTS } if (t1 > 0.0) { auto p1 = curr.position + t1 * (next.position - curr.position); - auto [p1_dist, p1_near_l, p1_x] = unscaled_prev_layer.template distance_from_lines_extra(p1.cast()); + auto [p1_dist, p1_near_l, + p1_x] = unscaled_prev_layer.template distance_from_lines_extra(p1.cast()); ExtendedPoint new_p{}; new_p.position = p1; new_p.distance = float(p1_dist + boundary_offset); @@ -114,7 +150,7 @@ std::vector estimate_points_properties(const POINTS } new_points.push_back(next); } - points = new_points; + points = std::move(new_points); } if (max_line_length > 0) { @@ -140,216 +176,92 @@ std::vector estimate_points_properties(const POINTS } new_points.push_back(points.back()); } - points = new_points; + points = std::move(new_points); } - std::vector angles_for_curvature(points.size()); + float accumulated_distance = 0; std::vector distances_for_curvature(points.size()); for (size_t point_idx = 0; point_idx < points.size(); ++point_idx) { - ExtendedPoint &a = points[point_idx]; - size_t prev = prev_idx_modulo(point_idx, points.size()); - size_t next = next_idx_modulo(point_idx, points.size()); + const ExtendedPoint &a = points[point_idx]; + const ExtendedPoint &b = points[get_prev_index(point_idx, points.size())]; - int iter_limit = points.size(); - while ((a.position - points[prev].position).squaredNorm() < 1 && iter_limit > 0) { - prev = prev_idx_modulo(prev, points.size()); - iter_limit--; - } - - while ((a.position - points[next].position).squaredNorm() < 1 && iter_limit > 0) { - next = next_idx_modulo(next, points.size()); - iter_limit--; - } - - distances_for_curvature[point_idx] = (points[prev].position - a.position).norm(); - float alfa = angle(a.position - points[prev].position, points[next].position - a.position); - angles_for_curvature[point_idx] = alfa; + distances_for_curvature[point_idx] = (b.position - a.position).norm(); + accumulated_distance += distances_for_curvature[point_idx]; } - if (std::accumulate(distances_for_curvature.begin(), distances_for_curvature.end(), 0) > EPSILON) + if (accumulated_distance > EPSILON) for (float window_size : {3.0f, 9.0f, 16.0f}) { - size_t tail_point = 0; - float tail_window_acc = 0; - float tail_angle_acc = 0; + for (int point_idx = 0; point_idx < int(points.size()); ++point_idx) { + ExtendedPoint ¤t = points[point_idx]; - size_t head_point = 0; - float head_window_acc = 0; - float head_angle_acc = 0; + Vec2d back_position = current.position; +{ + size_t back_point_index = point_idx; + float dist_backwards = 0; + while (dist_backwards < window_size * 0.5 && back_point_index != get_prev_index(back_point_index, points.size())) { + float line_dist = distances_for_curvature[get_prev_index(back_point_index, points.size())]; + if (dist_backwards + line_dist > window_size * 0.5) { + back_position = points[back_point_index].position + + (window_size * 0.5 - dist_backwards) * + (points[get_prev_index(back_point_index, points.size())].position - + points[back_point_index].position) + .normalized(); + dist_backwards += window_size * 0.5 - dist_backwards + EPSILON; + } else { + dist_backwards += line_dist; + back_point_index = get_prev_index(back_point_index, points.size()); + } - for (size_t point_idx = 0; point_idx < points.size(); ++point_idx) { - if (point_idx == 0) { - while (tail_window_acc < window_size * 0.5) { - tail_window_acc += distances_for_curvature[tail_point]; - tail_angle_acc += angles_for_curvature[tail_point]; - tail_point = prev_idx_modulo(tail_point, points.size()); + } + + } + + Vec2d front_position = current.position; + { + size_t front_point_index = point_idx; + float dist_forwards = 0; + while (dist_forwards < window_size * 0.5 && front_point_index != get_next_index(front_point_index, points.size())) { + float line_dist = distances_for_curvature[front_point_index]; + if (dist_forwards + line_dist > window_size * 0.5) { + front_position = points[front_point_index].position + + (window_size * 0.5 - dist_forwards) * + (points[get_next_index(front_point_index, points.size())].position - + points[front_point_index].position) + .normalized(); + dist_forwards += window_size * 0.5 - dist_forwards + EPSILON; + } else { + dist_forwards += line_dist; + front_point_index = get_next_index(front_point_index, points.size()); + } + } + } + + float new_curvature = angle(current.position - back_position, front_position - current.position) / window_size; + if (abs(current.curvature) < abs(new_curvature)) { + current.curvature = new_curvature; } } - while (tail_window_acc - distances_for_curvature[next_idx_modulo(tail_point, points.size())] > window_size * 0.5) { - tail_point = next_idx_modulo(tail_point, points.size()); - tail_window_acc -= distances_for_curvature[tail_point]; - tail_angle_acc -= angles_for_curvature[tail_point]; - } - - while (head_window_acc < window_size * 0.5) { - head_point = next_idx_modulo(head_point, points.size()); - head_window_acc += distances_for_curvature[head_point]; - head_angle_acc += angles_for_curvature[head_point]; - } - - float curvature = (tail_angle_acc + head_angle_acc) / window_size; - if (std::abs(curvature) > std::abs(points[point_idx].curvature)) { - points[point_idx].curvature = curvature; - } - - tail_window_acc += distances_for_curvature[point_idx]; - tail_angle_acc += angles_for_curvature[point_idx]; - head_window_acc -= distances_for_curvature[point_idx]; - head_angle_acc -= angles_for_curvature[point_idx]; } - } return points; -} - -struct ProcessedPoint -{ - Point p; - float speed = 1.0f; - int fan_speed = 0; -}; - -class ExtrusionQualityEstimator -{ - std::unordered_map> prev_layer_boundaries; - std::unordered_map> next_layer_boundaries; - std::unordered_map> prev_curled_extrusions; - std::unordered_map> next_curled_extrusions; - const PrintObject *current_object; - -public: - void set_current_object(const PrintObject *object) { current_object = object; } - - void prepare_for_new_layer(const Layer *layer) - { - if (layer == nullptr) - return; - const PrintObject *object = layer->object(); - prev_layer_boundaries[object] = next_layer_boundaries[object]; - next_layer_boundaries[object] = AABBTreeLines::LinesDistancer{to_unscaled_linesf(layer->lslices)}; - prev_curled_extrusions[object] = next_curled_extrusions[object]; - next_curled_extrusions[object] = AABBTreeLines::LinesDistancer{layer->curled_lines}; - } - - std::vector estimate_speed_from_extrusion_quality( - const ExtrusionPath &path, - const std::vector> overhangs_w_speeds, - const std::vector> overhangs_w_fan_speeds, - size_t extruder_id, - float ext_perimeter_speed, - float original_speed) - { - float speed_base = ext_perimeter_speed > 0 ? ext_perimeter_speed : original_speed; - std::map speed_sections; - for (size_t i = 0; i < overhangs_w_speeds.size(); i++) { - float distance = path.width * (1.0 - (overhangs_w_speeds[i].first / 100.0)); - float speed = overhangs_w_speeds[i].second.percent ? (speed_base * overhangs_w_speeds[i].second.value / 100.0) : - overhangs_w_speeds[i].second.value; - if (speed < EPSILON) speed = speed_base; - speed_sections[distance] = speed; - } - - std::map fan_speed_sections; - for (size_t i = 0; i < overhangs_w_fan_speeds.size(); i++) { - float distance = path.width * (1.0 - (overhangs_w_fan_speeds[i].first / 100.0)); - float fan_speed = overhangs_w_fan_speeds[i].second.get_at(extruder_id); - fan_speed_sections[distance] = fan_speed; - } - - std::vector extended_points = - estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width); - - std::vector processed_points; - processed_points.reserve(extended_points.size()); - for (size_t i = 0; i < extended_points.size(); i++) { - const ExtendedPoint &curr = extended_points[i]; - const ExtendedPoint &next = extended_points[i + 1 < extended_points.size() ? i + 1 : i]; - - // The following code artifically increases the distance to provide slowdown for extrusions that are over curled lines - float artificial_distance_to_curled_lines = 0.0; - const double dist_limit = 10.0 * path.width; - { - Vec2d middle = 0.5 * (curr.position + next.position); - auto line_indices = prev_curled_extrusions[current_object].all_lines_in_radius(Point::new_scale(middle), scale_(dist_limit)); - if (!line_indices.empty()) { - double len = (next.position - curr.position).norm(); - // For long lines, there is a problem with the additional slowdown. If by accident, there is small curled line near the middle of this long line - // The whole segment gets slower unnecesarily. For these long lines, we do additional check whether it is worth slowing down. - // NOTE that this is still quite rough approximation, e.g. we are still checking lines only near the middle point - // TODO maybe split the lines into smaller segments before running this alg? but can be demanding, and GCode will be huge - if (len > 8) { - Vec2d dir = Vec2d(next.position - curr.position) / len; - Vec2d right = Vec2d(-dir.y(), dir.x()); - - Polygon box_of_influence = { - scaled(Vec2d(curr.position + right * dist_limit)), - scaled(Vec2d(next.position + right * dist_limit)), - scaled(Vec2d(next.position - right * dist_limit)), - scaled(Vec2d(curr.position - right * dist_limit)), - }; - - double projected_lengths_sum = 0; - for (size_t idx : line_indices) { - const CurledLine &line = prev_curled_extrusions[current_object].get_line(idx); - Lines inside = intersection_ln({{line.a, line.b}}, {box_of_influence}); - if (inside.empty()) - continue; - double projected_length = abs(dir.dot(unscaled(Vec2d((inside.back().b - inside.back().a).cast())))); - projected_lengths_sum += projected_length; - } - if (projected_lengths_sum < 0.4 * len) { - line_indices.clear(); - } - } - - for (size_t idx : line_indices) { - const CurledLine &line = prev_curled_extrusions[current_object].get_line(idx); - float distance_from_curled = unscaled(line_alg::distance_to(line, Point::new_scale(middle))); - float dist = path.width * (1.0 - (distance_from_curled / dist_limit)) * - (1.0 - (distance_from_curled / dist_limit)) * - (line.curled_height / (path.height * 10.0f)); // max_curled_height_factor from SupportSpotGenerator - artificial_distance_to_curled_lines = std::max(artificial_distance_to_curled_lines, dist); - } - } - } - - auto interpolate_speed = [](const std::map &values, float distance) { - auto upper_dist = values.lower_bound(distance); - if (upper_dist == values.end()) { - return values.rbegin()->second; - } - if (upper_dist == values.begin()) { - return upper_dist->second; } - auto lower_dist = std::prev(upper_dist); - float t = (distance - lower_dist->first) / (upper_dist->first - lower_dist->first); - return (1.0f - t) * lower_dist->second + t * upper_dist->second; - }; +ExtrusionPaths calculate_and_split_overhanging_extrusions(const ExtrusionPath &path, + const AABBTreeLines::LinesDistancer &unscaled_prev_layer, + const AABBTreeLines::LinesDistancer &prev_layer_curled_lines); - float extrusion_speed = std::min(interpolate_speed(speed_sections, curr.distance), - interpolate_speed(speed_sections, next.distance)); - float curled_base_speed = interpolate_speed(speed_sections, artificial_distance_to_curled_lines); - float final_speed = std::min(curled_base_speed, extrusion_speed); - float fan_speed = std::min(interpolate_speed(fan_speed_sections, curr.distance), - interpolate_speed(fan_speed_sections, next.distance)); +ExtrusionEntityCollection calculate_and_split_overhanging_extrusions( + const ExtrusionEntityCollection *ecc, + const AABBTreeLines::LinesDistancer &unscaled_prev_layer, + const AABBTreeLines::LinesDistancer &prev_layer_curled_lines); - processed_points.push_back({scaled(curr.position), final_speed, int(fan_speed)}); - } - return processed_points; - } -}; +std::pair calculate_overhang_speed(const ExtrusionAttributes &attributes, + const FullPrintConfig &config, + size_t extruder_id, + float external_perim_reference_speed, + float default_speed); -} // namespace Slic3r +}} // namespace Slic3r::ExtrusionProcessor #endif // slic3r_ExtrusionProcessor_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index b7aa9cd..55018ca 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4,8 +4,9 @@ #include "libslic3r/LocalesUtils.hpp" #include "libslic3r/format.hpp" #include "libslic3r/I18N.hpp" -#include "libslic3r/GCodeWriter.hpp" +#include "libslic3r/GCode/GCodeWriter.hpp" #include "libslic3r/I18N.hpp" +#include "libslic3r/Geometry/ArcWelder.hpp" #include "GCodeProcessor.hpp" #include @@ -43,7 +44,6 @@ static const Slic3r::Vec3f DEFAULT_EXTRUDER_OFFSET = Slic3r::Vec3f::Zero(); // taken from QIDITechnology.ini - [printer:Original QIDI i3 MK2.5 MMU2] static const std::vector DEFAULT_EXTRUDER_COLORS = { "#FF8000", "#DB5182", "#3EC0FF", "#FF4F4F", "#FBEB7D" }; -static const std::string INTERNAL_G2G3_TAG = "!#!#! internal only - from G2/G3 expansion !#!#!"; namespace Slic3r { @@ -65,6 +65,18 @@ const std::vector GCodeProcessor::Reserved_Tags = { const float GCodeProcessor::Wipe_Width = 0.05f; const float GCodeProcessor::Wipe_Height = 0.05f; +bgcode::binarize::BinarizerConfig GCodeProcessor::s_binarizer_config{ + { + bgcode::core::ECompressionType::None, // file metadata + bgcode::core::ECompressionType::None, // printer metadata + bgcode::core::ECompressionType::Deflate, // print metadata + bgcode::core::ECompressionType::Deflate, // slicer metadata + bgcode::core::ECompressionType::Heatshrink_12_4, // gcode + }, + bgcode::core::EGCodeEncodingType::MeatPackComments, + bgcode::core::EMetadataEncodingType::INI, + bgcode::core::EChecksumType::CRC32 +}; #if ENABLE_GCODE_VIEWER_DATA_CHECKING const std::string GCodeProcessor::Mm3_Per_Mm_Tag = "MM3_PER_MM:"; #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING @@ -328,7 +340,7 @@ void GCodeProcessor::TimeMachine::calculate_time(size_t keep_last_n_blocks, floa } } layers_time[block.layer_id - 1] += block_time; - g1_times_cache.push_back({ block.g1_line_id, time }); + g1_times_cache.push_back({ block.g1_line_id, block.remaining_internal_g1_lines, time }); // update times for remaining time to printer stop placeholders auto it_stop_time = std::lower_bound(stop_times.begin(), stop_times.end(), block.g1_line_id, [](const StopTime& t, unsigned int value) { return t.g1_line_id < value; }); @@ -441,6 +453,7 @@ void GCodeProcessorResult::reset() { moves = std::vector(); bed_shape = Pointfs(); max_print_height = 0.0f; + z_offset = 0.0f; settings_ids.reset(); extruders_count = 0; backtrace_enabled = false; @@ -456,10 +469,12 @@ void GCodeProcessorResult::reset() { #else void GCodeProcessorResult::reset() { + is_binary_file = false; moves.clear(); lines_ends.clear(); bed_shape = Pointfs(); max_print_height = 0.0f; + z_offset = 0.0f; settings_ids.reset(); extruders_count = 0; backtrace_enabled = false; @@ -553,6 +568,8 @@ GCodeProcessor::GCodeProcessor() void GCodeProcessor::apply_config(const PrintConfig& config) { m_parser.apply_config(config); + m_binarizer.set_enabled(config.gcode_binary); + m_result.is_binary_file = config.gcode_binary; m_producer = EProducer::QIDISlicer; m_flavor = config.gcode_flavor; @@ -575,8 +592,12 @@ void GCodeProcessor::apply_config(const PrintConfig& config) for (size_t i = 0; i < extruders_count; ++ i) { m_extruder_offsets[i] = to_3d(config.extruder_offset.get_at(i).cast().eval(), 0.f); m_extruder_colors[i] = static_cast(i); - m_extruder_temps_config[i] = static_cast(config.temperature.get_at(i)); m_extruder_temps_first_layer_config[i] = static_cast(config.first_layer_temperature.get_at(i)); + m_extruder_temps_config[i] = static_cast(config.temperature.get_at(i)); + if (m_extruder_temps_config[i] == 0) { + // This means the value should be ignored and first layer temp should be used. + m_extruder_temps_config[i] = m_extruder_temps_first_layer_config[i]; + } m_result.filament_diameters[i] = static_cast(config.filament_diameter.get_at(i)); m_result.filament_densities[i] = static_cast(config.filament_density.get_at(i)); m_result.filament_cost[i] = static_cast(config.filament_cost.get_at(i)); @@ -1023,6 +1044,23 @@ static inline const char* remove_eols(const char *begin, const char *end) { // Load a G-code into a stand-alone G-code viewer. // throws CanceledException through print->throw_if_canceled() (sent by the caller as callback). void GCodeProcessor::process_file(const std::string& filename, std::function cancel_callback) +{ + FILE* file = boost::nowide::fopen(filename.c_str(), "rb"); + if (file == nullptr) + throw Slic3r::RuntimeError(format("Error opening file %1%", filename)); + + using namespace bgcode::core; + std::vector cs_buffer(65536); + const bool is_binary = is_valid_binary_gcode(*file, true, cs_buffer.data(), cs_buffer.size()) == EResult::Success; + fclose(file); + + if (is_binary) + process_binary_file(filename, cancel_callback); + else + process_ascii_file(filename, cancel_callback); +} + +void GCodeProcessor::process_ascii_file(const std::string& filename, std::function cancel_callback) { CNumericLocalesSetter locales_setter; @@ -1073,6 +1111,7 @@ void GCodeProcessor::process_file(const std::string& filename, std::functionfinalize(false); } +static void update_lines_ends_and_out_file_pos(const std::string& out_string, std::vector& lines_ends, size_t* out_file_pos) +{ + for (size_t i = 0; i < out_string.size(); ++i) { + if (out_string[i] == '\n') + lines_ends.emplace_back((out_file_pos != nullptr) ? *out_file_pos + i + 1 : i + 1); + } + if (out_file_pos != nullptr) + *out_file_pos += out_string.size(); +} + +void GCodeProcessor::process_binary_file(const std::string& filename, std::function cancel_callback) +{ +#if ENABLE_GCODE_VIEWER_STATISTICS + m_start_time = std::chrono::high_resolution_clock::now(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + FilePtr file{ boost::nowide::fopen(filename.c_str(), "rb") }; + if (file.f == nullptr) + throw Slic3r::RuntimeError(format("Error opening file %1%", filename)); + + fseek(file.f, 0, SEEK_END); + const long file_size = ftell(file.f); + rewind(file.f); + + // read file header + using namespace bgcode::core; + using namespace bgcode::binarize; + FileHeader file_header; + EResult res = read_header(*file.f, file_header, nullptr); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("File %1% does not contain a valid binary gcode\nError: %2%", filename, + std::string(translate_result(res)))); + + // read file metadata block, if present + BlockHeader block_header; + std::vector cs_buffer(65536); + res = read_next_block_header(*file.f, file_header, block_header, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + if ((EBlockType)block_header.type != EBlockType::FileMetadata && + (EBlockType)block_header.type != EBlockType::PrinterMetadata) + throw Slic3r::RuntimeError(format("Unable to find file metadata block in file %1%", filename)); + if ((EBlockType)block_header.type == EBlockType::FileMetadata) { + FileMetadataBlock file_metadata_block; + res = file_metadata_block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + auto producer_it = std::find_if(file_metadata_block.raw_data.begin(), file_metadata_block.raw_data.end(), + [](const std::pair& item) { return item.first == "Producer"; }); + if (producer_it != file_metadata_block.raw_data.end() && boost::starts_with(producer_it->second, std::string(SLIC3R_APP_NAME))) + m_producer = EProducer::QIDISlicer; + else + m_producer = EProducer::Unknown; + res = read_next_block_header(*file.f, file_header, block_header, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + } + else { + m_producer = EProducer::Unknown; + } + + // read printer metadata block + if ((EBlockType)block_header.type != EBlockType::PrinterMetadata) + throw Slic3r::RuntimeError(format("Unable to find printer metadata block in file %1%", filename)); + PrinterMetadataBlock printer_metadata_block; + res = printer_metadata_block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + + // read thumbnail blocks + res = read_next_block_header(*file.f, file_header, block_header, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + + while ((EBlockType)block_header.type == EBlockType::Thumbnail) { + ThumbnailBlock thumbnail_block; + res = thumbnail_block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + res = read_next_block_header(*file.f, file_header, block_header, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + } + + // read print metadata block + if ((EBlockType)block_header.type != EBlockType::PrintMetadata) + throw Slic3r::RuntimeError(format("Unable to find print metadata block in file %1%", filename)); + PrintMetadataBlock print_metadata_block; + res = print_metadata_block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + + // read slicer metadata block + res = read_next_block_header(*file.f, file_header, block_header, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + if ((EBlockType)block_header.type != EBlockType::SlicerMetadata) + throw Slic3r::RuntimeError(format("Unable to find slicer metadata block in file %1%", filename)); + SlicerMetadataBlock slicer_metadata_block; + res = slicer_metadata_block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + DynamicPrintConfig config; + config.apply(FullPrintConfig::defaults()); + std::string str; + for (const auto& [key, value] : slicer_metadata_block.raw_data) { + str += key + " = " + value + "\n"; + } + // Silently substitute unknown values by new ones for loading configurations from PrusaSlicer's own G-code. + // Showing substitution log or errors may make sense, but we are not really reading many values from the G-code config, + // thus a probability of incorrect substitution is low and the G-code viewer is a consumer-only anyways. + config.load_from_ini_string(str, ForwardCompatibilitySubstitutionRule::EnableSilent); + apply_config(config); + + m_result.filename = filename; + m_result.is_binary_file = true; + m_result.id = ++s_result_id; + initialize_result_moves(); + + // read gcodes block + res = read_next_block_header(*file.f, file_header, block_header, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + if ((EBlockType)block_header.type != EBlockType::GCode) + throw Slic3r::RuntimeError(format("Unable to find gcode block in file %1%", filename)); + while ((EBlockType)block_header.type == EBlockType::GCode) { + GCodeBlock block; + res = block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + + std::vector& lines_ends = m_result.lines_ends.emplace_back(std::vector()); + update_lines_ends_and_out_file_pos(block.raw_data, lines_ends, nullptr); + + m_parser.parse_buffer(block.raw_data, [this](GCodeReader& reader, const GCodeReader::GCodeLine& line) { + this->process_gcode_line(line, true); + }); + + if (ftell(file.f) == file_size) + break; + + res = read_next_block_header(*file.f, file_header, block_header, cs_buffer.data(), cs_buffer.size()); + if (res != EResult::Success) + throw Slic3r::RuntimeError(format("Error reading file %1%: %2%", filename, std::string(translate_result(res)))); + } + + // Don't post-process the G-code to update time stamps. + this->finalize(false); +} void GCodeProcessor::initialize(const std::string& filename) { assert(is_decimal_separator_point()); @@ -1113,6 +1301,7 @@ void GCodeProcessor::process_buffer(const std::string &buffer) void GCodeProcessor::finalize(bool perform_post_process) { + m_result.z_offset = m_z_offset; // update width/height of wipe moves for (GCodeProcessorResult::MoveVertex& move : m_result.moves) { if (move.type == EMoveType::Wipe) { @@ -1132,7 +1321,7 @@ void GCodeProcessor::finalize(bool perform_post_process) m_used_filaments.process_caches(this); - update_estimated_times_stats(); + update_estimated_statistics(); #if ENABLE_GCODE_VIEWER_DATA_CHECKING std::cout << "\n"; @@ -2363,13 +2552,11 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) if (line.has_e()) g1_axes[E] = (double)line.e(); std::optional g1_feedrate = std::nullopt; if (line.has_f()) g1_feedrate = (double)line.f(); - std::optional g1_cmt = std::nullopt; - if (!line.comment().empty()) g1_cmt = line.comment(); - - process_G1(g1_axes, g1_feedrate, g1_cmt); + process_G1(g1_axes, g1_feedrate); } -void GCodeProcessor::process_G1(const std::array, 4>& axes, std::optional feedrate, std::optional cmt) +void GCodeProcessor::process_G1(const std::array, 4>& axes, const std::optional& feedrate, + G1DiscretizationOrigin origin, const std::optional& remaining_internal_g1_lines) { const float filament_diameter = (static_cast(m_extruder_id) < m_result.filament_diameters.size()) ? m_result.filament_diameters[m_extruder_id] : m_result.filament_diameters.back(); const float filament_radius = 0.5f * filament_diameter; @@ -2449,10 +2636,17 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING if (m_forced_height > 0.0f) + // use height coming from the gcode tags m_height = m_forced_height; - else if (m_layer_id == 0) + else if (m_layer_id == 0) { // first layer + if (m_end_position[Z] > 0.0f) + // use the current (clamped) z, if greater than zero + m_height = std::min(m_end_position[Z], 2.0f); + else + // use the first layer height m_height = m_first_layer_height + m_z_offset; - else if (!cmt.has_value() || *cmt != INTERNAL_G2G3_TAG) { + } + else if (origin == G1DiscretizationOrigin::G1) { if (m_end_position[Z] > m_extruded_last_z + EPSILON && delta_pos[Z] == 0.0) m_height = m_end_position[Z] - m_extruded_last_z; } @@ -2460,10 +2654,7 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes if (m_height == 0.0f) m_height = DEFAULT_TOOLPATH_HEIGHT; - if (m_end_position[Z] == 0.0f || (m_extrusion_role == GCodeExtrusionRole::Custom && m_layer_id == 0)) - m_end_position[Z] = m_height; - - if (!cmt.has_value() || *cmt != INTERNAL_G2G3_TAG) + if (origin == G1DiscretizationOrigin::G1) m_extruded_last_z = m_end_position[Z]; m_options_z_corrector.update(m_height); @@ -2472,6 +2663,7 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING if (m_forced_width > 0.0f) + // use width coming from the gcode tags m_width = m_forced_width; else if (m_extrusion_role == GCodeExtrusionRole::ExternalPerimeter) // cross section: rectangle @@ -2525,6 +2717,7 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes block.role = m_extrusion_role; block.distance = distance; block.g1_line_id = m_g1_line_id; + block.remaining_internal_g1_lines = remaining_internal_g1_lines.has_value() ? *remaining_internal_g1_lines : 0; block.layer_id = std::max(1, m_layer_id); // calculates block cruise feedrate @@ -2693,18 +2886,60 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes } // store move - store_move_vertex(type, cmt.has_value() && *cmt == INTERNAL_G2G3_TAG); + store_move_vertex(type, origin == G1DiscretizationOrigin::G2G3); } void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool clockwise) { - if (!line.has('I') || !line.has('J')) + enum class EFitting { None, IJ, R }; + std::string_view axis_pos_I; + std::string_view axis_pos_J; + EFitting fitting = EFitting::None; + if (line.has('R')) { + fitting = EFitting::R; + } else { + axis_pos_I = line.axis_pos('I'); + axis_pos_J = line.axis_pos('J'); + if (! axis_pos_I.empty() || ! axis_pos_J.empty()) + fitting = EFitting::IJ; + } + + if (fitting == EFitting::None) return; + const float filament_diameter = (static_cast(m_extruder_id) < m_result.filament_diameters.size()) ? m_result.filament_diameters[m_extruder_id] : m_result.filament_diameters.back(); + const float filament_radius = 0.5f * filament_diameter; + const float area_filament_cross_section = static_cast(M_PI) * sqr(filament_radius); + + AxisCoords end_position = m_start_position; + for (unsigned char a = X; a <= E; ++a) { + end_position[a] = extract_absolute_position_on_axis((Axis)a, line, double(area_filament_cross_section)); + } // relative center Vec3f rel_center = Vec3f::Zero(); - if (!line.has_value('I', rel_center.x()) || !line.has_value('J', rel_center.y())) +#ifndef NDEBUG + double radius = 0.0; +#endif // NDEBUG + if (fitting == EFitting::R) { + float r; + if (!line.has_value('R', r) || r == 0.0f) + return; +#ifndef NDEBUG + radius = (double)std::abs(r); +#endif // NDEBUG + const Vec2f start_pos((float)m_start_position[X], (float)m_start_position[Y]); + const Vec2f end_pos((float)end_position[X], (float)end_position[Y]); + const Vec2f c = Geometry::ArcWelder::arc_center(start_pos, end_pos, r, !clockwise); + rel_center.x() = c.x() - m_start_position[X]; + rel_center.y() = c.y() - m_start_position[Y]; + } + else { + assert(fitting == EFitting::IJ); + if (! axis_pos_I.empty() && ! line.has_value(axis_pos_I, rel_center.x())) + return; + if (! axis_pos_J.empty() && ! line.has_value(axis_pos_J, rel_center.y())) return; + } // scale center, if needed if (m_units == EUnits::Inches) @@ -2740,14 +2975,6 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc // arc center arc.center = arc.start + rel_center.cast(); - const float filament_diameter = (static_cast(m_extruder_id) < m_result.filament_diameters.size()) ? m_result.filament_diameters[m_extruder_id] : m_result.filament_diameters.back(); - const float filament_radius = 0.5f * filament_diameter; - const float area_filament_cross_section = static_cast(M_PI) * sqr(filament_radius); - - AxisCoords end_position = m_start_position; - for (unsigned char a = X; a <= E; ++a) { - end_position[a] = extract_absolute_position_on_axis((Axis)a, line, double(area_filament_cross_section)); - } // arc end endpoint arc.end = Vec3d(end_position[X], end_position[Y], end_position[Z]); @@ -2757,6 +2984,7 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc // what to do ??? } + assert(fitting != EFitting::R || std::abs(radius - arc.start_radius()) < EPSILON); // updates feedrate from line std::optional feedrate; if (line.has_f()) @@ -2807,18 +3035,17 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc return ret; }; - auto internal_only_g1_line = [this](const AxisCoords& target, bool has_z, const std::optional& feedrate, const std::optional& extrusion) { + auto internal_only_g1_line = [this](const AxisCoords& target, bool has_z, const std::optional& feedrate, + const std::optional& extrusion, const std::optional& remaining_internal_g1_lines = std::nullopt) { std::array, 4> g1_axes = { target[X], target[Y], std::nullopt, std::nullopt }; std::optional g1_feedrate = std::nullopt; if (has_z) g1_axes[Z] = target[Z]; - if (feedrate.has_value()) - g1_feedrate = (double)*feedrate; if (extrusion.has_value()) g1_axes[E] = target[E]; - std::optional g1_cmt = INTERNAL_G2G3_TAG; - - process_G1(g1_axes, g1_feedrate, g1_cmt); + if (feedrate.has_value()) + g1_feedrate = (double)*feedrate; + process_G1(g1_axes, g1_feedrate, G1DiscretizationOrigin::G2G3, remaining_internal_g1_lines); }; // calculate arc segments @@ -2827,8 +3054,13 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc // https://github.com/qidi3d/QIDI-Firmware/blob/MK3/Firmware/motion_control.cpp // segments count +#if 0 static const double MM_PER_ARC_SEGMENT = 1.0; const size_t segments = std::max(std::floor(travel_length / MM_PER_ARC_SEGMENT), 1); +#else + static const double gcode_arc_tolerance = 0.0125; + const size_t segments = Geometry::ArcWelder::arc_discretization_steps(arc.start_radius(), std::abs(arc.angle), gcode_arc_tolerance); +#endif const double inv_segment = 1.0 / double(segments); const double theta_per_segment = arc.angle * inv_segment; @@ -2876,7 +3108,8 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc arc_target[E] += extruder_per_segment; m_start_position = m_end_position; // this is required because we are skipping the call to process_gcode_line() - internal_only_g1_line(adjust_target(arc_target, prev_target), z_per_segment != 0.0, (i == 1) ? feedrate : std::nullopt, extrusion); + internal_only_g1_line(adjust_target(arc_target, prev_target), z_per_segment != 0.0, (i == 1) ? feedrate : std::nullopt, + extrusion, segments - i); prev_target = arc_target; } @@ -3435,8 +3668,67 @@ void GCodeProcessor::post_process() // temporary file to contain modified gcode std::string out_path = m_result.filename + ".postprocess"; FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") }; - if (out.f == nullptr) { + if (out.f == nullptr) throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for writing.\n")); + std::vector filament_mm(m_result.extruders_count, 0.0); + std::vector filament_cm3(m_result.extruders_count, 0.0); + std::vector filament_g(m_result.extruders_count, 0.0); + std::vector filament_cost(m_result.extruders_count, 0.0); + + double filament_total_g = 0.0; + double filament_total_cost = 0.0; + + for (const auto& [id, volume] : m_result.print_statistics.volumes_per_extruder) { + filament_mm[id] = volume / (static_cast(M_PI) * sqr(0.5 * m_result.filament_diameters[id])); + filament_cm3[id] = volume * 0.001; + filament_g[id] = filament_cm3[id] * double(m_result.filament_densities[id]); + filament_cost[id] = filament_g[id] * double(m_result.filament_cost[id]) * 0.001; + filament_total_g += filament_g[id]; + filament_total_cost += filament_cost[id]; + } + + if (m_binarizer.is_enabled()) { + // update print metadata + auto stringify = [](const std::vector& values) { + std::string ret; + char buf[1024]; + for (size_t i = 0; i < values.size(); ++i) { + sprintf(buf, i < values.size() - 1 ? "%.2lf, " : "%.2lf", values[i]); + ret += buf; + } + return ret; + }; + + // update binary data + bgcode::binarize::BinaryData& binary_data = m_binarizer.get_binary_data(); + binary_data.print_metadata.raw_data.emplace_back(PrintStatistics::FilamentUsedMm, stringify(filament_mm)); + binary_data.print_metadata.raw_data.emplace_back(PrintStatistics::FilamentUsedCm3, stringify(filament_cm3)); + binary_data.print_metadata.raw_data.emplace_back(PrintStatistics::FilamentUsedG, stringify(filament_g)); + binary_data.print_metadata.raw_data.emplace_back(PrintStatistics::FilamentCost, stringify(filament_cost)); + binary_data.print_metadata.raw_data.emplace_back(PrintStatistics::TotalFilamentUsedG, stringify({ filament_total_g })); + binary_data.print_metadata.raw_data.emplace_back(PrintStatistics::TotalFilamentCost, stringify({ filament_total_cost })); + + binary_data.printer_metadata.raw_data.emplace_back(PrintStatistics::FilamentUsedMm, stringify(filament_mm)); // duplicated into print metadata + binary_data.printer_metadata.raw_data.emplace_back(PrintStatistics::FilamentUsedG, stringify(filament_g)); // duplicated into print metadata + binary_data.printer_metadata.raw_data.emplace_back(PrintStatistics::FilamentCost, stringify(filament_cost)); // duplicated into print metadata + binary_data.printer_metadata.raw_data.emplace_back(PrintStatistics::FilamentUsedCm3, stringify(filament_cm3)); // duplicated into print metadata + + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = m_time_processor.machines[i]; + PrintEstimatedStatistics::ETimeMode mode = static_cast(i); + if (mode == PrintEstimatedStatistics::ETimeMode::Normal || machine.enabled) { + char buf[128]; + sprintf(buf, "(%s mode)", (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent"); + binary_data.print_metadata.raw_data.emplace_back("estimated printing time " + std::string(buf), get_time_dhms(machine.time)); + binary_data.print_metadata.raw_data.emplace_back("estimated first layer printing time " + std::string(buf), get_time_dhms(machine.layers_time.empty() ? 0.f : machine.layers_time.front())); + + binary_data.printer_metadata.raw_data.emplace_back("estimated printing time " + std::string(buf), get_time_dhms(machine.time)); + } + } + + const bgcode::core::EResult res = m_binarizer.initialize(*out.f, s_binarizer_config); + if (res != bgcode::core::EResult::Success) + throw Slic3r::RuntimeError(format("Unable to initialize the gcode binarizer.\nError: %1%", bgcode::core::translate_result(res))); } auto time_in_minutes = [](float time_in_seconds) { @@ -3552,32 +3844,55 @@ void GCodeProcessor::post_process() // used to update m_result.moves[].gcode_id std::vector> m_gcode_lines_map; - size_t m_curr_g1_id{ 0 }; + size_t m_times_cache_id{ 0 }; size_t m_out_file_pos{ 0 }; + bgcode::binarize::Binarizer& m_binarizer; public: - ExportLines(EWriteType type, TimeMachine& machine) + ExportLines(bgcode::binarize::Binarizer& binarizer, EWriteType type, TimeMachine& machine) #ifndef NDEBUG - : m_statistics(*this), m_write_type(type), m_machine(machine) {} + : m_statistics(*this), m_binarizer(binarizer), m_write_type(type), m_machine(machine) {} #else - : m_write_type(type), m_machine(machine) {} + : m_binarizer(binarizer), m_write_type(type), m_machine(machine) {} #endif // NDEBUG - void update(size_t lines_counter, size_t g1_lines_counter) { + // return: number of internal G1 lines (from G2/G3 splitting) processed + unsigned int update(const std::string& line, size_t lines_counter, size_t g1_lines_counter) { + unsigned int ret = 0; m_gcode_lines_map.push_back({ lines_counter, 0 }); - if (g1_lines_counter == 0) - return; + if (GCodeReader::GCodeLine::cmd_is(line, "G0") || + GCodeReader::GCodeLine::cmd_is(line, "G1") || + GCodeReader::GCodeLine::cmd_is(line, "G2") || + GCodeReader::GCodeLine::cmd_is(line, "G3") || + GCodeReader::GCodeLine::cmd_is(line, "G28")) + ++g1_lines_counter; + else + return ret; - auto init_it = m_machine.g1_times_cache.begin() + m_curr_g1_id; + auto init_it = m_machine.g1_times_cache.begin() + m_times_cache_id; auto it = init_it; - while (it != m_machine.g1_times_cache.end() && it->id < g1_lines_counter + 1) { + while (it != m_machine.g1_times_cache.end() && it->id < g1_lines_counter) { ++it; - ++m_curr_g1_id; + ++m_times_cache_id; } - if ((it != m_machine.g1_times_cache.end() && it != init_it) || m_curr_g1_id == 0) + if (it->id > g1_lines_counter) + return ret; + + // search for internal G1 lines + if (GCodeReader::GCodeLine::cmd_is(line, "G2") || GCodeReader::GCodeLine::cmd_is(line, "G3")) { + while (it != m_machine.g1_times_cache.end() && it->remaining_internal_g1_lines > 0) { + ++it; + ++m_times_cache_id; + ++g1_lines_counter; + ++ret; + } + } + + if (it != m_machine.g1_times_cache.end() && it->id == g1_lines_counter) m_time = it->elapsed_time; + return ret; } // add the given gcode line to the cache @@ -3673,7 +3988,14 @@ void GCodeProcessor::post_process() } } + if (m_binarizer.is_enabled()) { + if (m_binarizer.append_gcode(out_string) != bgcode::core::EResult::Success) + throw Slic3r::RuntimeError("Error while sending gcode to the binarizer."); + } + else { write_to_file(out, out_string, result, out_path); + update_lines_ends_and_out_file_pos(out_string, result.lines_ends.front(), &m_out_file_pos); + } } // flush the current content of the cache to file @@ -3689,7 +4011,14 @@ void GCodeProcessor::post_process() m_statistics.remove_all_lines(); #endif // NDEBUG + if (m_binarizer.is_enabled()) { + if (m_binarizer.append_gcode(out_string) != bgcode::core::EResult::Success) + throw Slic3r::RuntimeError("Error while sending gcode to the binarizer."); + } + else { write_to_file(out, out_string, result, out_path); + update_lines_ends_and_out_file_pos(out_string, result.lines_ends.front(), &m_out_file_pos); + } } void synchronize_moves(GCodeProcessorResult& result) const { @@ -3708,22 +4037,19 @@ void GCodeProcessor::post_process() private: void write_to_file(FilePtr& out, const std::string& out_string, GCodeProcessorResult& result, const std::string& out_path) { if (!out_string.empty()) { + if (!m_binarizer.is_enabled()) { fwrite((const void*)out_string.c_str(), 1, out_string.length(), out.f); if (ferror(out.f)) { out.close(); boost::nowide::remove(out_path.c_str()); - throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nIs the disk full?\n")); + throw Slic3r::RuntimeError("GCode processor post process export failed.\nIs the disk full?"); } - for (size_t i = 0; i < out_string.size(); ++i) { - if (out_string[i] == '\n') - result.lines_ends.emplace_back(m_out_file_pos + i + 1); } - m_out_file_pos += out_string.size(); } } }; - ExportLines export_lines(m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize, m_time_processor.machines[0]); + ExportLines export_lines(m_binarizer, m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize, m_time_processor.machines[0]); // replace placeholder lines with the proper final value // gcode_line is in/out parameter, to reduce expensive memory allocation @@ -3786,22 +4112,6 @@ void GCodeProcessor::post_process() return processed; }; - std::vector filament_mm(m_result.extruders_count, 0.0); - std::vector filament_cm3(m_result.extruders_count, 0.0); - std::vector filament_g(m_result.extruders_count, 0.0); - std::vector filament_cost(m_result.extruders_count, 0.0); - - double filament_total_g = 0.0; - double filament_total_cost = 0.0; - - for (const auto& [id, volume] : m_result.print_statistics.volumes_per_extruder) { - filament_mm[id] = volume / (static_cast(M_PI) * sqr(0.5 * m_result.filament_diameters[id])); - filament_cm3[id] = volume * 0.001; - filament_g[id] = filament_cm3[id] * double(m_result.filament_densities[id]); - filament_cost[id] = filament_g[id] * double(m_result.filament_cost[id]) * 0.001; - filament_total_g += filament_g[id]; - filament_total_cost += filament_cost[id]; - } auto process_used_filament = [&](std::string& gcode_line) { // Prefilter for parsing speed. @@ -3823,12 +4133,12 @@ void GCodeProcessor::post_process() }; bool ret = false; - ret |= process_tag(gcode_line, "; filament used [mm] =", filament_mm); - ret |= process_tag(gcode_line, "; filament used [g] =", filament_g); - ret |= process_tag(gcode_line, "; total filament used [g] =", { filament_total_g }); - ret |= process_tag(gcode_line, "; filament used [cm3] =", filament_cm3); - ret |= process_tag(gcode_line, "; filament cost =", filament_cost); - ret |= process_tag(gcode_line, "; total filament cost =", { filament_total_cost }); + ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedMmMask, filament_mm); + ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedGMask, filament_g); + ret |= process_tag(gcode_line, PrintStatistics::TotalFilamentUsedGMask, { filament_total_g }); + ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedCm3Mask, filament_cm3); + ret |= process_tag(gcode_line, PrintStatistics::FilamentCostMask, filament_cost); + ret |= process_tag(gcode_line, PrintStatistics::TotalFilamentCostMask, { filament_total_cost }); return ret; }; @@ -3920,7 +4230,7 @@ void GCodeProcessor::post_process() } }; - // add lines XXX to exported gcode + // add lines M104 to exported gcode auto process_line_T = [this, &export_lines](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { const std::string cmd = GCodeReader::GCodeLine::extract_cmd(gcode_line); if (cmd.size() >= 2) { @@ -3967,6 +4277,7 @@ void GCodeProcessor::post_process() }; m_result.lines_ends.clear(); + m_result.lines_ends.emplace_back(std::vector()); unsigned int line_id = 0; // Backtrace data for Tx gcode lines @@ -3997,9 +4308,9 @@ void GCodeProcessor::post_process() gcode_line.insert(gcode_line.end(), it, it_end); if (eol) { ++line_id; - export_lines.update(line_id, g1_lines_counter); gcode_line += "\n"; + const unsigned int internal_g1_lines_counter = export_lines.update(gcode_line, line_id, g1_lines_counter); // replace placeholder lines bool processed = process_placeholders(gcode_line); if (processed) @@ -4007,11 +4318,24 @@ void GCodeProcessor::post_process() if (!processed) processed = process_used_filament(gcode_line); if (!processed && !is_temporary_decoration(gcode_line)) { - if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G1")) + if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G0") || GCodeReader::GCodeLine::cmd_is(gcode_line, "G1")) { + export_lines.append_line(gcode_line); // add lines M73 where needed process_line_G1(g1_lines_counter++); + gcode_line.clear(); + } + else if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G2") || GCodeReader::GCodeLine::cmd_is(gcode_line, "G3")) { + export_lines.append_line(gcode_line); + // add lines M73 where needed + process_line_G1(g1_lines_counter + internal_g1_lines_counter); + g1_lines_counter += (1 + internal_g1_lines_counter); + gcode_line.clear(); + } + else if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G28")) { + ++g1_lines_counter; + } else if (m_result.backtrace_enabled && GCodeReader::GCodeLine::cmd_starts_with(gcode_line, "T")) { - // add lines XXX where needed + // add lines M104 where needed process_line_T(gcode_line, g1_lines_counter, backtrace_T); max_backtrace_time = std::max(max_backtrace_time, backtrace_T.time); } @@ -4036,13 +4360,29 @@ void GCodeProcessor::post_process() export_lines.flush(out, m_result, out_path); + if (m_binarizer.is_enabled()) { + if (m_binarizer.finalize() != bgcode::core::EResult::Success) + throw Slic3r::RuntimeError("Error while finalizing the gcode binarizer."); + } out.close(); in.close(); + const std::string result_filename = m_result.filename; + if (m_binarizer.is_enabled()) { + // The list of lines in the binary gcode is different from the original one. + // This requires to re-process the binarized file to be able to synchronize with it all the data needed by the preview, + // as gcode window, tool position and moves slider which relies on indexing the gcode lines. + reset(); + // the following call modifies m_result.filename + process_binary_file(out_path); + // restore the proper filename + m_result.filename = result_filename; + } + else export_lines.synchronize_moves(m_result); - if (rename_file(out_path, m_result.filename)) - throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + m_result.filename + '\n' + + if (rename_file(out_path, result_filename)) + throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + result_filename + '\n' + "Is " + out_path + " locked?" + '\n'); } @@ -4243,7 +4583,7 @@ void GCodeProcessor::simulate_st_synchronize(float additional_time) } } -void GCodeProcessor::update_estimated_times_stats() +void GCodeProcessor::update_estimated_statistics() { auto update_mode = [this](PrintEstimatedStatistics::ETimeMode mode) { PrintEstimatedStatistics::Mode& data = m_result.print_statistics.modes[static_cast(mode)]; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 61100ed..a0ed388 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -7,6 +7,7 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/CustomGCode.hpp" +#include #include #include #include @@ -56,9 +57,13 @@ namespace Slic3r { time = 0.0f; travel_time = 0.0f; custom_gcode_times.clear(); + custom_gcode_times.shrink_to_fit(); moves_times.clear(); + moves_times.shrink_to_fit(); roles_times.clear(); + roles_times.shrink_to_fit(); layers_times.clear(); + layers_times.shrink_to_fit(); } }; @@ -76,6 +81,7 @@ namespace Slic3r { m.reset(); } volumes_per_color_change.clear(); + volumes_per_color_change.shrink_to_fit(); volumes_per_extruder.clear(); used_filaments_per_role.clear(); cost_per_extruder.clear(); @@ -135,12 +141,16 @@ namespace Slic3r { }; std::string filename; + bool is_binary_file; unsigned int id; std::vector moves; // Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code. - std::vector lines_ends; + // Binarized gcodes usually have several gcode blocks. Each block has its own list on ends of lines. + // Ascii gcodes have only one list on ends of lines + std::vector> lines_ends; Pointfs bed_shape; float max_print_height; + float z_offset; SettingsIds settings_ids; size_t extruders_count; bool backtrace_enabled; @@ -261,6 +271,7 @@ namespace Slic3r { EMoveType move_type{ EMoveType::Noop }; GCodeExtrusionRole role{ GCodeExtrusionRole::None }; unsigned int g1_line_id{ 0 }; + unsigned int remaining_internal_g1_lines; unsigned int layer_id{ 0 }; float distance{ 0.0f }; // mm float acceleration{ 0.0f }; // mm/s^2 @@ -301,6 +312,7 @@ namespace Slic3r { struct G1LinesCacheItem { unsigned int id; + unsigned int remaining_internal_g1_lines; float elapsed_time; }; @@ -523,8 +535,11 @@ namespace Slic3r { }; #endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + static bgcode::binarize::BinarizerConfig& get_binarizer_config() { return s_binarizer_config; } private: GCodeReader m_parser; + bgcode::binarize::Binarizer m_binarizer; + static bgcode::binarize::BinarizerConfig s_binarizer_config; EUnits m_units; EPositioningType m_global_positioning_type; @@ -622,6 +637,8 @@ namespace Slic3r { void apply_config(const PrintConfig& config); void set_print(Print* print) { m_print = print; } + bgcode::binarize::BinaryData& get_binary_data() { return m_binarizer.get_binary_data(); } + const bgcode::binarize::BinaryData& get_binary_data() const { return m_binarizer.get_binary_data(); } void enable_stealth_time_estimator(bool enabled); bool is_stealth_time_estimator_enabled() const { @@ -664,6 +681,8 @@ namespace Slic3r { void apply_config_kissslicer(const std::string& filename); void process_gcode_line(const GCodeReader::GCodeLine& line, bool producers_enabled); + void process_ascii_file(const std::string& filename, std::function cancel_callback = nullptr); + void process_binary_file(const std::string& filename, std::function cancel_callback = nullptr); // Process tags embedded into comments void process_tags(const std::string_view comment, bool producers_enabled); bool process_producers_tags(const std::string_view comment); @@ -680,8 +699,13 @@ namespace Slic3r { // Move void process_G0(const GCodeReader::GCodeLine& line); void process_G1(const GCodeReader::GCodeLine& line); + enum class G1DiscretizationOrigin { + G1, + G2G3, + }; void process_G1(const std::array, 4>& axes = { std::nullopt, std::nullopt, std::nullopt, std::nullopt }, - std::optional feedrate = std::nullopt, std::optional cmt = std::nullopt); + const std::optional& feedrate = std::nullopt, G1DiscretizationOrigin origin = G1DiscretizationOrigin::G1, + const std::optional& remaining_internal_g1_lines = std::nullopt); // Arc Move void process_G2_G3(const GCodeReader::GCodeLine& line, bool clockwise); @@ -815,7 +839,7 @@ namespace Slic3r { // Simulates firmware st_synchronize() call void simulate_st_synchronize(float additional_time = 0.0f); - void update_estimated_times_stats(); + void update_estimated_statistics(); double extract_absolute_position_on_axis(Axis axis, const GCodeReader::GCodeLine& line, double area_filament_cross_section); }; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCode/GCodeWriter.cpp similarity index 81% rename from src/libslic3r/GCodeWriter.cpp rename to src/libslic3r/GCode/GCodeWriter.cpp index 9635f3b..18f81a2 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCode/GCodeWriter.cpp @@ -1,10 +1,12 @@ #include "GCodeWriter.hpp" -#include "CustomGCode.hpp" +#include "../CustomGCode.hpp" #include #include #include #include #include +#include +#include #ifdef __APPLE__ #include @@ -13,6 +15,7 @@ #define FLAVOR_IS(val) this->config.gcode_flavor == val #define FLAVOR_IS_NOT(val) this->config.gcode_flavor != val +using namespace std::string_view_literals; namespace Slic3r { // static @@ -90,17 +93,17 @@ std::string GCodeWriter::set_temperature(unsigned int temperature, bool wait, in if (wait && (FLAVOR_IS(gcfMakerWare) || FLAVOR_IS(gcfSailfish))) return {}; - std::string code, comment; + std::string_view code, comment; if (wait && FLAVOR_IS_NOT(gcfTeacup) && FLAVOR_IS_NOT(gcfRepRapFirmware)) { - code = "M109"; - comment = "set temperature and wait for it to be reached"; + code = "M109"sv; + comment = "set temperature and wait for it to be reached"sv; } else { if (FLAVOR_IS(gcfRepRapFirmware)) { // M104 is deprecated on RepRapFirmware - code = "G10"; + code = "G10"sv; } else { - code = "M104"; + code = "M104"sv; } - comment = "set temperature"; + comment = "set temperature"sv; } std::ostringstream gcode; @@ -130,22 +133,22 @@ std::string GCodeWriter::set_temperature(unsigned int temperature, bool wait, in std::string GCodeWriter::set_bed_temperature(unsigned int temperature, bool wait) { if (temperature == m_last_bed_temperature && (! wait || m_last_bed_temperature_reached)) - return std::string(); + return {}; m_last_bed_temperature = temperature; m_last_bed_temperature_reached = wait; - std::string code, comment; + std::string_view code, comment; if (wait && FLAVOR_IS_NOT(gcfTeacup)) { if (FLAVOR_IS(gcfMakerWare) || FLAVOR_IS(gcfSailfish)) { - code = "M109"; + code = "M109"sv; } else { - code = "M190"; + code = "M190"sv; } - comment = "set bed temperature and wait for it to be reached"; + comment = "set bed temperature and wait for it to be reached"sv; } else { - code = "M140"; - comment = "set bed temperature"; + code = "M140"sv; + comment = "set bed temperature"sv; } std::ostringstream gcode; @@ -233,7 +236,7 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i auto& last_value = separate_travel ? m_last_travel_acceleration : m_last_acceleration ; if (acceleration == 0 || acceleration == last_value) - return std::string(); + return {}; last_value = acceleration; @@ -301,7 +304,7 @@ std::string GCodeWriter::toolchange(unsigned int extruder_id) return gcode.str(); } -std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker) const +std::string GCodeWriter::set_speed(double F, const std::string_view comment, const std::string_view cooling_marker) const { assert(F > 0.); assert(F < 100000.); @@ -313,74 +316,62 @@ std::string GCodeWriter::set_speed(double F, const std::string &comment, const s return w.string(); } -std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) +std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string_view comment) { - m_pos.x() = point.x(); - m_pos.y() = point.y(); + m_pos.head<2>() = point.head<2>(); GCodeG1Formatter w; w.emit_xy(point); //B36 auto speed = m_is_first_layer ? this->config.get_abs_value("first_layer_travel_speed") : this->config.travel_speed.value; - w.emit_f(speed * 60.0); - w.emit_comment(this->config.gcode_comments, comment); - return w.string(); -} - -std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment) -{ - // FIXME: This function was not being used when travel_speed_z was separated (bd6badf). - // Calculation of feedrate was not updated accordingly. If you want to use - // this function, fix it first. - std::terminate(); - - /* If target Z is lower than current Z but higher than nominal Z we - don't perform the Z move but we only move in the XY plane and - adjust the nominal Z by reducing the lift amount that will be - used for unlift. */ - if (!this->will_move_z(point.z())) { - double nominal_z = m_pos.z() - m_lifted; - m_lifted -= (point.z() - nominal_z); - // In case that retract_lift == layer_height we could end up with almost zero in_m_lifted - // and a retract could be skipped (https://github.com/qidi3d/QIDISlicer/issues/2154 - if (std::abs(m_lifted) < EPSILON) - m_lifted = 0.; - return this->travel_to_xy(to_2d(point)); - } - - /* In all the other cases, we perform an actual XYZ move and cancel - the lift. */ - m_lifted = 0; - m_pos = point; - - GCodeG1Formatter w; - w.emit_xyz(point); w.emit_f(this->config.travel_speed.value * 60.0); w.emit_comment(this->config.gcode_comments, comment); return w.string(); } -std::string GCodeWriter::travel_to_z(double z, const std::string &comment) +std::string GCodeWriter::travel_to_xy_G2G3IJ(const Vec2d &point, const Vec2d &ij, const bool ccw, const std::string_view comment) { - /* If target Z is lower than current Z but higher than nominal Z - we don't perform the move but we only adjust the nominal Z by - reducing the lift amount that will be used for unlift. */ - if (!this->will_move_z(z)) { - double nominal_z = m_pos.z() - m_lifted; - m_lifted -= (z - nominal_z); - if (std::abs(m_lifted) < EPSILON) - m_lifted = 0.; - return {}; + assert(std::abs(point.x()) < 1200.); + assert(std::abs(point.y()) < 1200.); + assert(std::abs(ij.x()) < 1200.); + assert(std::abs(ij.y()) < 1200.); + assert(std::abs(ij.x()) >= 0.001 || std::abs(ij.y()) >= 0.001); + + m_pos.head<2>() = point.head<2>(); + + GCodeG2G3Formatter w(ccw); + w.emit_xy(point); + w.emit_ij(ij); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } - /* In all the other cases, we perform an actual Z move and cancel - the lift. */ - m_lifted = 0; - return this->_travel_to_z(z, comment); +std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string_view comment) +{ + if (std::abs(point.x() - m_pos.x()) < EPSILON && std::abs(point.y() - m_pos.y()) < EPSILON) { + return this->travel_to_z(point.z(), comment); + } else if (std::abs(point.z() - m_pos.z()) < EPSILON) { + return this->travel_to_xy(point.head<2>(), comment); + } else { + m_pos = point; + + GCodeG1Formatter w; + w.emit_xyz(point); + Vec2f speed {this->config.travel_speed_z.value, this->config.travel_speed.value}; + w.emit_f(speed.norm() * 60.0); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } -std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) + } + +std::string GCodeWriter::travel_to_z(double z, const std::string_view comment) +{ + return std::abs(m_pos.z() - z) < EPSILON ? "" : this->get_travel_to_z_gcode(z, comment); +} + +std::string GCodeWriter::get_travel_to_z_gcode(double z, const std::string_view comment) { m_pos.z() = z; @@ -395,22 +386,12 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) return w.string(); } -bool GCodeWriter::will_move_z(double z) const +std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string_view comment) { - /* If target Z is lower than current Z but higher than nominal Z - we don't perform an actual Z move. */ - if (m_lifted > 0) { - double nominal_z = m_pos.z() - m_lifted; - if (z >= nominal_z && z <= m_pos.z()) - return false; - } - return true; -} + assert(dE != 0); + assert(std::abs(dE) < 1000.0); -std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment) -{ - m_pos.x() = point.x(); - m_pos.y() = point.y(); + m_pos.head<2>() = point.head<2>(); GCodeG1Formatter w; w.emit_xy(point); @@ -419,8 +400,28 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: return w.string(); } +std::string GCodeWriter::extrude_to_xy_G2G3IJ(const Vec2d &point, const Vec2d &ij, const bool ccw, double dE, const std::string_view comment) +{ + assert(std::abs(dE) < 1000.0); + assert(dE != 0); + assert(std::abs(point.x()) < 1200.); + assert(std::abs(point.y()) < 1200.); + assert(std::abs(ij.x()) < 1200.); + assert(std::abs(ij.y()) < 1200.); + assert(std::abs(ij.x()) >= 0.001 || std::abs(ij.y()) >= 0.001); + + m_pos.head<2>() = point.head<2>(); + + GCodeG2G3Formatter w(ccw); + w.emit_xy(point); + w.emit_ij(ij); + w.emit_e(m_extrusion_axis, m_extruder->extrude(dE).second); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); +} + #if 0 -std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment) +std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string_view comment) { m_pos = point; m_lifted = 0; @@ -440,7 +441,7 @@ std::string GCodeWriter::retract(bool before_wipe) assert(factor >= 0. && factor <= 1. + EPSILON); return this->_retract( factor * m_extruder->retract_length(), - factor * m_extruder->retract_restart_extra(), + m_extruder->retract_restart_extra(), "retract" ); } @@ -451,13 +452,15 @@ std::string GCodeWriter::retract_for_toolchange(bool before_wipe) assert(factor >= 0. && factor <= 1. + EPSILON); return this->_retract( factor * m_extruder->retract_length_toolchange(), - factor * m_extruder->retract_restart_extra_toolchange(), + m_extruder->retract_restart_extra_toolchange(), "retract for toolchange" ); } -std::string GCodeWriter::_retract(double length, double restart_extra, const std::string &comment) +std::string GCodeWriter::_retract(double length, double restart_extra, const std::string_view comment) { + assert(std::abs(length) < 1000.0); + assert(std::abs(restart_extra) < 1000.0); /* If firmware retraction is enabled, we use a fake value of 1 since we ignore the actual configured retract_length which might be 0, in which case the retraction logic gets skipped. */ @@ -515,47 +518,9 @@ std::string GCodeWriter::unretract() return gcode; } -/* If this method is called more than once before calling unlift(), - it will not perform subsequent lifts, even if Z was raised manually - (i.e. with travel_to_z()) and thus _lifted was reduced. */ -std::string GCodeWriter::lift() -{ - // check whether the above/below conditions are met - double target_lift = 0; - { - double above = this->config.retract_lift_above.get_at(m_extruder->id()); - double below = this->config.retract_lift_below.get_at(m_extruder->id()); - if (m_pos.z() >= above && (below == 0 || m_pos.z() <= below)) - target_lift = this->config.retract_lift.get_at(m_extruder->id()); - } - if (m_lifted == 0 && target_lift > 0) { - m_lifted = target_lift; - return this->_travel_to_z(m_pos.z() + target_lift, "lift Z"); - } - return {}; -} - -std::string GCodeWriter::unlift() -{ - std::string gcode; - if (m_lifted > 0) { - gcode += this->_travel_to_z(m_pos.z() - m_lifted, "restore layer Z"); - m_lifted = 0; - } - return gcode; -} void GCodeWriter::update_position(const Vec3d &new_pos) { - assert(this->m_lifted >= 0); - const double nominal_z = m_pos.z() - m_lifted; - m_lifted = new_pos.z() - nominal_z; - if (m_lifted < - EPSILON) - throw Slic3r::RuntimeError("Custom G-code reports negative Z-hop. Final Z position is below the print_z height."); - // In case that retract_lift == layer_height we could end up with almost zero in_m_lifted - // and a retract could be skipped (https://github.com/qidi3d/QIDISlicer/issues/2154 - if (m_lifted < EPSILON) - m_lifted = 0.; m_pos = new_pos; } diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCode/GCodeWriter.hpp similarity index 82% rename from src/libslic3r/GCodeWriter.hpp rename to src/libslic3r/GCode/GCodeWriter.hpp index 4420007..e846d0b 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCode/GCodeWriter.hpp @@ -1,13 +1,14 @@ #ifndef slic3r_GCodeWriter_hpp_ #define slic3r_GCodeWriter_hpp_ -#include "libslic3r.h" +#include "../libslic3r.h" +#include "../Extruder.hpp" +#include "../Point.hpp" +#include "../PrintConfig.hpp" +#include "CoolingBuffer.hpp" #include +#include #include -#include "Extruder.hpp" -#include "Point.hpp" -#include "PrintConfig.hpp" -#include "GCode/CoolingBuffer.hpp" namespace Slic3r { @@ -64,25 +65,25 @@ public: // printed with the same extruder. std::string toolchange_prefix() const; std::string toolchange(unsigned int extruder_id); - std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()) const; - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()); - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string()); - std::string travel_to_z(double z, const std::string &comment = std::string()); - bool will_move_z(double z) const; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string()); -// std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string()); + std::string set_speed(double F, const std::string_view comment = {}, const std::string_view cooling_marker = {}) const; + std::string travel_to_xy(const Vec2d &point, const std::string_view comment = {}); + std::string travel_to_xy_G2G3IJ(const Vec2d &point, const Vec2d &ij, const bool ccw, const std::string_view comment = {}); + std::string travel_to_xyz(const Vec3d &point, const std::string_view comment = {}); + std::string get_travel_to_z_gcode(double z, const std::string_view comment); + std::string travel_to_z(double z, const std::string_view comment = {}); + std::string extrude_to_xy(const Vec2d &point, double dE, const std::string_view comment = {}); + std::string extrude_to_xy_G2G3IJ(const Vec2d &point, const Vec2d &ij, const bool ccw, double dE, const std::string_view comment); +// std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string_view comment = {}); std::string retract(bool before_wipe = false); std::string retract_for_toolchange(bool before_wipe = false); std::string unretract(); - std::string lift(); - std::string unlift(); // Current position of the printer, in G-code coordinates. // Z coordinate of current position contains zhop. If zhop is applied (this->zhop() > 0), // then the print_z = this->get_position().z() - this->zhop(). Vec3d get_position() const { return m_pos; } - // Current Z hop value. - double get_zhop() const { return m_lifted; } + // Zhop value is obsolete. This is for backwards compability. + double get_zhop() const { return 0; } // Update position of the print head based on the final position returned by a custom G-code block. // The new position Z coordinate contains the Z-hop. // GCodeWriter expects the custom script to NOT change print_z, only Z-hop, thus the print_z is maintained @@ -144,8 +145,7 @@ private: Print }; - std::string _travel_to_z(double z, const std::string &comment); - std::string _retract(double length, double restart_extra, const std::string &comment); + std::string _retract(double length, double restart_extra, const std::string_view comment); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); }; @@ -183,6 +183,10 @@ public: static double quantize(double v, size_t ndigits) { return std::round(v * pow_10[ndigits]) * pow_10_inv[ndigits]; } static double quantize_xyzf(double v) { return quantize(v, XYZF_EXPORT_DIGITS); } static double quantize_e(double v) { return quantize(v, E_EXPORT_DIGITS); } + static Vec2d quantize(const Vec2d &pt) + { return { quantize(pt.x(), XYZF_EXPORT_DIGITS), quantize(pt.y(), XYZF_EXPORT_DIGITS) }; } + static Vec3d quantize(const Vec3d &pt) + { return { quantize(pt.x(), XYZF_EXPORT_DIGITS), quantize(pt.y(), XYZF_EXPORT_DIGITS), quantize(pt.z(), XYZF_EXPORT_DIGITS) }; } void emit_axis(const char axis, const double v, size_t digits); @@ -201,7 +205,14 @@ public: this->emit_axis('Z', z, XYZF_EXPORT_DIGITS); } - void emit_e(const std::string &axis, double v) { + void emit_ij(const Vec2d &point) { + if (point.x() != 0) + this->emit_axis('I', point.x(), XYZF_EXPORT_DIGITS); + if (point.y() != 0) + this->emit_axis('J', point.y(), XYZF_EXPORT_DIGITS); + } + + void emit_e(const std::string_view axis, double v) { if (! axis.empty()) { // not gcfNoExtrusion this->emit_axis(axis[0], v, E_EXPORT_DIGITS); @@ -212,12 +223,12 @@ public: this->emit_axis('F', speed, XYZF_EXPORT_DIGITS); } - void emit_string(const std::string &s) { - strncpy(ptr_err.ptr, s.c_str(), s.size()); + void emit_string(const std::string_view s) { + strncpy(ptr_err.ptr, s.data(), s.size()); ptr_err.ptr += s.size(); } - void emit_comment(bool allow_comments, const std::string &comment) { + void emit_comment(bool allow_comments, const std::string_view comment) { if (allow_comments && ! comment.empty()) { *ptr_err.ptr ++ = ' '; *ptr_err.ptr ++ = ';'; *ptr_err.ptr ++ = ' '; this->emit_string(comment); @@ -241,14 +252,24 @@ public: GCodeG1Formatter() { this->buf[0] = 'G'; this->buf[1] = '1'; - this->buf_end = buf + buflen; - this->ptr_err.ptr = this->buf + 2; + this->ptr_err.ptr += 2; } GCodeG1Formatter(const GCodeG1Formatter&) = delete; GCodeG1Formatter& operator=(const GCodeG1Formatter&) = delete; }; +class GCodeG2G3Formatter : public GCodeFormatter { +public: + GCodeG2G3Formatter(bool ccw) { + this->buf[0] = 'G'; + this->buf[1] = ccw ? '3' : '2'; + this->ptr_err.ptr += 2; + } + + GCodeG2G3Formatter(const GCodeG2G3Formatter&) = delete; + GCodeG2G3Formatter& operator=(const GCodeG2G3Formatter&) = delete; +}; } /* namespace Slic3r */ #endif /* slic3r_GCodeWriter_hpp_ */ diff --git a/src/libslic3r/GCode/LabelObjects.cpp b/src/libslic3r/GCode/LabelObjects.cpp new file mode 100644 index 0000000..1e5953e --- /dev/null +++ b/src/libslic3r/GCode/LabelObjects.cpp @@ -0,0 +1,191 @@ +#include "LabelObjects.hpp" + +#include "ClipperUtils.hpp" +#include "Model.hpp" +#include "Print.hpp" +#include "TriangleMeshSlicer.hpp" + + +namespace Slic3r::GCode { + + +namespace { + +Polygon instance_outline(const PrintInstance* pi) +{ + ExPolygons outline; + const ModelObject* mo = pi->model_instance->get_object(); + const ModelInstance* mi = pi->model_instance; + for (const ModelVolume *v : mo->volumes) { + Polygons vol_outline; + vol_outline = project_mesh(v->mesh().its, + mi->get_matrix() * v->get_matrix(), + [] {}); + switch (v->type()) { + case ModelVolumeType::MODEL_PART: outline = union_ex(outline, vol_outline); break; + case ModelVolumeType::NEGATIVE_VOLUME: outline = diff_ex(outline, vol_outline); break; + default:; + } + } + + // The projection may contain multiple polygons, which is not supported by Klipper. + // When that happens, calculate and use a 2d convex hull instead. + if (outline.size() == 1u) + return outline.front().contour; + else + return pi->model_instance->get_object()->convex_hull_2d(pi->model_instance->get_matrix()); +} + +}; // anonymous namespace + + +void LabelObjects::init(const Print& print) +{ + m_label_objects_style = print.config().gcode_label_objects; + m_flavor = print.config().gcode_flavor; + + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return; + + std::map> model_object_to_print_instances; + + // Iterate over all PrintObjects and their PrintInstances, collect PrintInstances which + // belong to the same ModelObject. + for (const PrintObject* po : print.objects()) + for (const PrintInstance& pi : po->instances()) + model_object_to_print_instances[pi.model_instance->get_object()].emplace_back(&pi); + + // Now go through the map, assign a unique_id to each of the PrintInstances and get the indices of the + // respective ModelObject and ModelInstance so we can use them in the tags. This will maintain + // indices even in case that some instances are rotated (those end up in different PrintObjects) + // or when some are out of bed (these ModelInstances have no corresponding PrintInstances). + int unique_id = 0; + for (const auto& [model_object, print_instances] : model_object_to_print_instances) { + const ModelObjectPtrs& model_objects = model_object->get_model()->objects; + int object_id = int(std::find(model_objects.begin(), model_objects.end(), model_object) - model_objects.begin()); + for (const PrintInstance* const pi : print_instances) { + bool object_has_more_instances = print_instances.size() > 1u; + int instance_id = int(std::find(model_object->instances.begin(), model_object->instances.end(), pi->model_instance) - model_object->instances.begin()); + + // Now compose the name of the object and define whether indexing is 0 or 1-based. + std::string name = model_object->name; + if (m_label_objects_style == LabelObjectsStyle::Octoprint) { + // use zero-based indexing for objects and instances, as we always have done + name += " id:" + std::to_string(object_id) + " copy " + std::to_string(instance_id); + } + else if (m_label_objects_style == LabelObjectsStyle::Firmware) { + // use one-based indexing for objects and instances so indices match what we see in QIDISlicer. + ++object_id; + ++instance_id; + + if (object_has_more_instances) + name += " (Instance " + std::to_string(instance_id) + ")"; + if (m_flavor == gcfKlipper) { + const std::string banned = "-. \r\n\v\t\f"; + std::replace_if(name.begin(), name.end(), [&banned](char c) { return banned.find(c) != std::string::npos; }, '_'); + } + } + + m_label_data.emplace(pi, LabelData{name, unique_id}); + ++unique_id; + } + } +} + + + +std::string LabelObjects::all_objects_header() const +{ + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return std::string(); + + std::string out; + + // Let's sort the values according to unique_id so they are in the same order in which they were added. + std::vector> label_data_sorted; + for (const auto& pi_and_label : m_label_data) + label_data_sorted.emplace_back(pi_and_label); + std::sort(label_data_sorted.begin(), label_data_sorted.end(), [](const auto& ld1, const auto& ld2) { return ld1.second.unique_id < ld2.second.unique_id; }); + + out += "\n"; + for (const auto& [print_instance, label] : label_data_sorted) { + if (m_label_objects_style == LabelObjectsStyle::Firmware && m_flavor == gcfKlipper) { + char buffer[64]; + out += "EXCLUDE_OBJECT_DEFINE NAME=" + label.name; + Polygon outline = instance_outline(print_instance); + assert(! outline.empty()); + outline.douglas_peucker(50000.f); + Point center = outline.centroid(); + std::snprintf(buffer, sizeof(buffer) - 1, " CENTER=%.3f,%.3f", unscale(center[0]), unscale(center[1])); + out += buffer + std::string(" POLYGON=["); + for (const Point& point : outline) { + std::snprintf(buffer, sizeof(buffer) - 1, "[%.3f,%.3f],", unscale(point[0]), unscale(point[1])); + out += buffer; + } + out.pop_back(); + out += "]\n"; + } else { + out += start_object(*print_instance, IncludeName::Yes); + out += stop_object(*print_instance); + } + } + out += "\n"; + return out; +} + + + +std::string LabelObjects::start_object(const PrintInstance& print_instance, IncludeName include_name) const +{ + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return std::string(); + + const LabelData& label = m_label_data.at(&print_instance); + + std::string out; + if (m_label_objects_style == LabelObjectsStyle::Octoprint) + out += std::string("; printing object ") + label.name + "\n"; + else if (m_label_objects_style == LabelObjectsStyle::Firmware) { + if (m_flavor == GCodeFlavor::gcfMarlinFirmware || m_flavor == GCodeFlavor::gcfMarlinLegacy || m_flavor == GCodeFlavor::gcfRepRapFirmware) { + out += std::string("M486 S") + std::to_string(label.unique_id); + if (include_name == IncludeName::Yes) { + out += (m_flavor == GCodeFlavor::gcfRepRapFirmware ? " A" : "\nM486 A"); + out += (m_flavor == GCodeFlavor::gcfRepRapFirmware ? (std::string("\"") + label.name + "\"") : label.name); + } + out += "\n"; + } else if (m_flavor == gcfKlipper) + out += "EXCLUDE_OBJECT_START NAME=" + label.name + "\n"; + else { + // Not supported by / implemented for the other firmware flavors. + } + } + return out; +} + + + +std::string LabelObjects::stop_object(const PrintInstance& print_instance) const +{ + if (m_label_objects_style == LabelObjectsStyle::Disabled) + return std::string(); + + const LabelData& label = m_label_data.at(&print_instance); + + std::string out; + if (m_label_objects_style == LabelObjectsStyle::Octoprint) + out += std::string("; stop printing object ") + label.name + "\n"; + else if (m_label_objects_style == LabelObjectsStyle::Firmware) { + if (m_flavor == GCodeFlavor::gcfMarlinFirmware || m_flavor == GCodeFlavor::gcfMarlinLegacy || m_flavor == GCodeFlavor::gcfRepRapFirmware) + out += std::string("M486 S-1\n"); + else if (m_flavor ==gcfKlipper) + out += "EXCLUDE_OBJECT_END NAME=" + label.name + "\n"; + else { + // Not supported by / implemented for the other firmware flavors. + } + } + return out; +} + + + +} // namespace Slic3r::GCode diff --git a/src/libslic3r/GCode/LabelObjects.hpp b/src/libslic3r/GCode/LabelObjects.hpp new file mode 100644 index 0000000..e6cf9b3 --- /dev/null +++ b/src/libslic3r/GCode/LabelObjects.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_GCode_LabelObjects_hpp_ +#define slic3r_GCode_LabelObjects_hpp_ + +#include +#include + +namespace Slic3r { + +enum GCodeFlavor : unsigned char; +enum class LabelObjectsStyle; +struct PrintInstance; +class Print; + + +namespace GCode { + + +class LabelObjects { +public: + enum class IncludeName { + No, + Yes + }; + void init(const Print& print); + std::string all_objects_header() const; + std::string start_object(const PrintInstance& print_instance, IncludeName include_name) const; + std::string stop_object(const PrintInstance& print_instance) const; + +private: + struct LabelData { + std::string name; + int unique_id; + }; + + LabelObjectsStyle m_label_objects_style; + GCodeFlavor m_flavor; + std::unordered_map m_label_data; + +}; + + +} // namespace GCode +} // namespace Slic3r + +#endif // slic3r_GCode_LabelObjects_hpp_ diff --git a/src/libslic3r/GCode/PrintExtents.cpp b/src/libslic3r/GCode/PrintExtents.cpp index 9a01dc8..9bcaa27 100644 --- a/src/libslic3r/GCode/PrintExtents.cpp +++ b/src/libslic3r/GCode/PrintExtents.cpp @@ -30,7 +30,7 @@ static inline BoundingBox extrusion_polyline_extents(const Polyline &polyline, c static inline BoundingBoxf extrusionentity_extents(const ExtrusionPath &extrusion_path) { - BoundingBox bbox = extrusion_polyline_extents(extrusion_path.polyline, coord_t(scale_(0.5 * extrusion_path.width))); + BoundingBox bbox = extrusion_polyline_extents(extrusion_path.polyline, coord_t(scale_(0.5 * extrusion_path.width()))); BoundingBoxf bboxf; if (! empty(bbox)) { bboxf.min = unscale(bbox.min); @@ -44,7 +44,7 @@ static inline BoundingBoxf extrusionentity_extents(const ExtrusionLoop &extrusio { BoundingBox bbox; for (const ExtrusionPath &extrusion_path : extrusion_loop.paths) - bbox.merge(extrusion_polyline_extents(extrusion_path.polyline, coord_t(scale_(0.5 * extrusion_path.width)))); + bbox.merge(extrusion_polyline_extents(extrusion_path.polyline, coord_t(scale_(0.5 * extrusion_path.width())))); BoundingBoxf bboxf; if (! empty(bbox)) { bboxf.min = unscale(bbox.min); @@ -58,7 +58,7 @@ static inline BoundingBoxf extrusionentity_extents(const ExtrusionMultiPath &ext { BoundingBox bbox; for (const ExtrusionPath &extrusion_path : extrusion_multi_path.paths) - bbox.merge(extrusion_polyline_extents(extrusion_path.polyline, coord_t(scale_(0.5 * extrusion_path.width)))); + bbox.merge(extrusion_polyline_extents(extrusion_path.polyline, coord_t(scale_(0.5 * extrusion_path.width())))); BoundingBoxf bboxf; if (! empty(bbox)) { bboxf.min = unscale(bbox.min); diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index c4e4296..b6c3d4b 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -1484,7 +1484,7 @@ void SeamPlacer::init(const Print &print, std::function throw_if_can } } -void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, bool external_first, +Point SeamPlacer::place_seam(const Layer *layer, const ExtrusionLoop &loop, bool external_first, const Point &last_pos) const { using namespace SeamPlacerImpl; const PrintObject *po = layer->object(); @@ -1587,7 +1587,7 @@ void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, bool extern //lastly, for internal perimeters, do the staggering if requested if (po->config().staggered_inner_seams && loop.length() > 0.0) { //fix depth, it is sometimes strongly underestimated - depth = std::max(loop.paths[projected_point.path_idx].width, depth); + depth = std::max(loop.paths[projected_point.path_idx].width(), depth); while (depth > 0.0f) { auto next_point = get_next_loop_point(projected_point); @@ -1605,13 +1605,7 @@ void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, bool extern } } - // Because the G-code export has 1um resolution, don't generate segments shorter than 1.5 microns, - // thus empty path segments will not be produced by G-code export. - if (!loop.split_at_vertex(seam_point, scaled(0.0015))) { - // The point is not in the original loop. - // Insert it. - loop.split_at(seam_point, true); - } + return seam_point; } diff --git a/src/libslic3r/GCode/SeamPlacer.hpp b/src/libslic3r/GCode/SeamPlacer.hpp index 671f6bc..5603e99 100644 --- a/src/libslic3r/GCode/SeamPlacer.hpp +++ b/src/libslic3r/GCode/SeamPlacer.hpp @@ -141,7 +141,7 @@ public: void init(const Print &print, std::function throw_if_canceled_func); - void place_seam(const Layer *layer, ExtrusionLoop &loop, bool external_first, const Point &last_pos) const; + Point place_seam(const Layer *layer, const ExtrusionLoop &loop, bool external_first, const Point &last_pos) const; private: void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info); diff --git a/src/libslic3r/GCode/SmoothPath.cpp b/src/libslic3r/GCode/SmoothPath.cpp new file mode 100644 index 0000000..f27f1f2 --- /dev/null +++ b/src/libslic3r/GCode/SmoothPath.cpp @@ -0,0 +1,269 @@ +#include "SmoothPath.hpp" + +#include "../ExtrusionEntity.hpp" +#include "../ExtrusionEntityCollection.hpp" + +namespace Slic3r::GCode { + +// Length of a smooth path. +double length(const SmoothPath &path) +{ + double l = 0; + for (const SmoothPathElement &el : path) + l += Geometry::ArcWelder::path_length(el.path); + return l; +} + +// Returns true if the smooth path is longer than a threshold. +bool longer_than(const SmoothPath &path, double length) +{ + for (const SmoothPathElement &el : path) { + for (auto it = std::next(el.path.begin()); it != el.path.end(); ++ it) { + length -= Geometry::ArcWelder::segment_length(*std::prev(it), *it); + if (length < 0) + return true; + } + } + return length < 0; +} + +std::optional sample_path_point_at_distance_from_start(const SmoothPath &path, double distance) +{ + if (distance >= 0) { + for (const SmoothPathElement &el : path) { + auto it = el.path.begin(); + auto end = el.path.end(); + Point prev_point = it->point; + for (++ it; it != end; ++ it) { + Point point = it->point; + if (it->linear()) { + // Linear segment + Vec2d v = (point - prev_point).cast(); + double lsqr = v.squaredNorm(); + if (lsqr > sqr(distance)) + return std::make_optional(prev_point + (v * (distance / sqrt(lsqr))).cast()); + distance -= sqrt(lsqr); + } else { + // Circular segment + float angle = Geometry::ArcWelder::arc_angle(prev_point.cast(), point.cast(), it->radius); + double len = std::abs(it->radius) * angle; + if (len > distance) { + // Rotate the segment end point in reverse towards the start point. + return std::make_optional(prev_point.rotated(- angle * (distance / len), + Geometry::ArcWelder::arc_center(prev_point.cast(), point.cast(), it->radius, it->ccw()).cast())); + } + distance -= len; + } + if (distance < 0) + return std::make_optional(point); + prev_point = point; + } + } + } + // Failed. + return {}; +} + +std::optional sample_path_point_at_distance_from_end(const SmoothPath &path, double distance) +{ + if (distance >= 0) { + for (const SmoothPathElement& el : path) { + auto it = el.path.begin(); + auto end = el.path.end(); + Point prev_point = it->point; + for (++it; it != end; ++it) { + Point point = it->point; + if (it->linear()) { + // Linear segment + Vec2d v = (point - prev_point).cast(); + double lsqr = v.squaredNorm(); + if (lsqr > sqr(distance)) + return std::make_optional(prev_point + (v * (distance / sqrt(lsqr))).cast()); + distance -= sqrt(lsqr); + } + else { + // Circular segment + float angle = Geometry::ArcWelder::arc_angle(prev_point.cast(), point.cast(), it->radius); + double len = std::abs(it->radius) * angle; + if (len > distance) { + // Rotate the segment end point in reverse towards the start point. + return std::make_optional(prev_point.rotated(-angle * (distance / len), + Geometry::ArcWelder::arc_center(prev_point.cast(), point.cast(), it->radius, it->ccw()).cast())); + } + distance -= len; + } + if (distance < 0) + return std::make_optional(point); + prev_point = point; + } + } + } + // Failed. + return {}; +} + +// Clip length of a smooth path, for seam hiding. +// When clipping the end of a path, don't create segments shorter than min_point_distance_threshold, +// rather discard such a degenerate segment. +double clip_end(SmoothPath &path, double distance, double min_point_distance_threshold) +{ + while (! path.empty() && distance > 0) { + Geometry::ArcWelder::Path &p = path.back().path; + distance = clip_end(p, distance); + if (p.empty()) { + path.pop_back(); + } else { + // Trailing path was trimmed and it is valid. + Geometry::ArcWelder::Path &last_path = path.back().path; + assert(last_path.size() > 1); + assert(distance == 0); + // Distance to go is zero. + // Remove the last segment if its length is shorter than min_point_distance_threshold. + const Geometry::ArcWelder::Segment &prev_segment = last_path[last_path.size() - 2]; + const Geometry::ArcWelder::Segment &last_segment = last_path.back(); + if (Geometry::ArcWelder::segment_length(prev_segment, last_segment) < min_point_distance_threshold) { + last_path.pop_back(); + if (last_path.size() < 2) + path.pop_back(); + } + return 0; + } + } + // Return distance to go after the whole smooth path was trimmed to zero. + return distance; +} + +void SmoothPathCache::interpolate_add(const ExtrusionPath &path, const InterpolationParameters ¶ms) +{ + double tolerance = params.tolerance; + if (path.role().is_sparse_infill()) + // Use 3x lower resolution than the object fine detail for sparse infill. + tolerance *= 3.; + else if (path.role().is_support()) + // Use 4x lower resolution than the object fine detail for support. + tolerance *= 4.; + else if (path.role().is_skirt()) + // Brim is currently marked as skirt. + // Use 4x lower resolution than the object fine detail for skirt & brim. + tolerance *= 4.; + m_cache[&path.polyline] = Slic3r::Geometry::ArcWelder::fit_path(path.polyline.points, tolerance, params.fit_circle_tolerance); +} + +void SmoothPathCache::interpolate_add(const ExtrusionMultiPath &multi_path, const InterpolationParameters ¶ms) +{ + for (const ExtrusionPath &path : multi_path.paths) + this->interpolate_add(path, params); +} + +void SmoothPathCache::interpolate_add(const ExtrusionLoop &loop, const InterpolationParameters ¶ms) +{ + for (const ExtrusionPath &path : loop.paths) + this->interpolate_add(path, params); +} + +void SmoothPathCache::interpolate_add(const ExtrusionEntityCollection &eec, const InterpolationParameters ¶ms) +{ + for (const ExtrusionEntity *ee : eec) { + if (ee->is_collection()) + this->interpolate_add(*static_cast(ee), params); + else if (const ExtrusionPath *path = dynamic_cast(ee); path) + this->interpolate_add(*path, params); + else if (const ExtrusionMultiPath *multi_path = dynamic_cast(ee); multi_path) + this->interpolate_add(*multi_path, params); + else if (const ExtrusionLoop *loop = dynamic_cast(ee); loop) + this->interpolate_add(*loop, params); + else + assert(false); + } +} + +const Geometry::ArcWelder::Path* SmoothPathCache::resolve(const Polyline *pl) const +{ + auto it = m_cache.find(pl); + return it == m_cache.end() ? nullptr : &it->second; +} + +const Geometry::ArcWelder::Path* SmoothPathCache::resolve(const ExtrusionPath &path) const +{ + return this->resolve(&path.polyline); +} + +Geometry::ArcWelder::Path SmoothPathCache::resolve_or_fit(const ExtrusionPath &path, bool reverse, double tolerance) const +{ + Geometry::ArcWelder::Path out; + if (const Geometry::ArcWelder::Path *cached = this->resolve(path); cached) + out = *cached; + else + out = Geometry::ArcWelder::fit_polyline(path.polyline.points, tolerance); + if (reverse) + Geometry::ArcWelder::reverse(out); + return out; +} + +SmoothPath SmoothPathCache::resolve_or_fit(const ExtrusionPaths &paths, bool reverse, double resolution) const +{ + SmoothPath out; + out.reserve(paths.size()); + if (reverse) { + for (auto it = paths.crbegin(); it != paths.crend(); ++ it) + out.push_back({ it->attributes(), this->resolve_or_fit(*it, true, resolution) }); + } else { + for (auto it = paths.cbegin(); it != paths.cend(); ++ it) + out.push_back({ it->attributes(), this->resolve_or_fit(*it, false, resolution) }); + } + return out; +} + +SmoothPath SmoothPathCache::resolve_or_fit(const ExtrusionMultiPath &multipath, bool reverse, double resolution) const +{ + return this->resolve_or_fit(multipath.paths, reverse, resolution); +} + +SmoothPath SmoothPathCache::resolve_or_fit_split_with_seam( + const ExtrusionLoop &loop, const bool reverse, const double resolution, + const Point &seam_point, const double seam_point_merge_distance_threshold) const +{ + SmoothPath out = this->resolve_or_fit(loop.paths, reverse, resolution); + assert(! out.empty()); + if (! out.empty()) { + // Find a closest point on a vector of smooth paths. + Geometry::ArcWelder::PathSegmentProjection proj; + int proj_path = -1; + for (const SmoothPathElement &el : out) + if (Geometry::ArcWelder::PathSegmentProjection this_proj = Geometry::ArcWelder::point_to_path_projection(el.path, seam_point, proj.distance2); + this_proj.valid()) { + // Found a better (closer) projection. + assert(this_proj.distance2 < proj.distance2); + assert(this_proj.segment_id >= 0 && this_proj.segment_id < el.path.size()); + proj = this_proj; + proj_path = &el - out.data(); + if (proj.distance2 == 0) + // There will be no better split point found than one with zero distance. + break; + } + assert(proj_path >= 0); + // Split the path at the closest point. + Geometry::ArcWelder::Path &path = out[proj_path].path; + std::pair split = Geometry::ArcWelder::split_at( + path, proj, seam_point_merge_distance_threshold); + if (split.second.empty()) { + std::rotate(out.begin(), out.begin() + proj_path + 1, out.end()); + assert(out.back().path == split.first); + } else { + ExtrusionAttributes attr = out[proj_path].path_attributes; + std::rotate(out.begin(), out.begin() + proj_path, out.end()); + out.front().path = std::move(split.second); + if (! split.first.empty()) { + if (out.back().path_attributes == attr) { + // Merge with the last segment. + out.back().path.insert(out.back().path.end(), std::next(split.first.begin()), split.first.end()); + } else + out.push_back({ attr, std::move(split.first) }); + } + } + } + + return out; +} + +} // namespace Slic3r::GCode diff --git a/src/libslic3r/GCode/SmoothPath.hpp b/src/libslic3r/GCode/SmoothPath.hpp new file mode 100644 index 0000000..83afd60 --- /dev/null +++ b/src/libslic3r/GCode/SmoothPath.hpp @@ -0,0 +1,89 @@ +#ifndef slic3r_GCode_SmoothPath_hpp_ +#define slic3r_GCode_SmoothPath_hpp_ + +#include + +#include "../ExtrusionEntity.hpp" +#include "../Geometry/ArcWelder.hpp" + +namespace Slic3r { + +class ExtrusionEntityCollection; + +namespace GCode { + +struct SmoothPathElement +{ + ExtrusionAttributes path_attributes; + Geometry::ArcWelder::Path path; +}; + +using SmoothPath = std::vector; + +// Length of a smooth path. +double length(const SmoothPath &path); +// Returns true if the smooth path is longer than a threshold. +bool longer_than(const SmoothPath &path, const double length); + +std::optional sample_path_point_at_distance_from_start(const SmoothPath &path, double distance); +std::optional sample_path_point_at_distance_from_end(const SmoothPath &path, double distance); + +// Clip end of a smooth path, for seam hiding. +// When clipping the end of a path, don't create segments shorter than min_point_distance_threshold, +// rather discard such a degenerate segment. +double clip_end(SmoothPath &path, double distance, double min_point_distance_threshold); + +class SmoothPathCache +{ +public: + struct InterpolationParameters { + double tolerance; + double fit_circle_tolerance; + }; + + void interpolate_add(const ExtrusionPath &ee, const InterpolationParameters ¶ms); + void interpolate_add(const ExtrusionMultiPath &ee, const InterpolationParameters ¶ms); + void interpolate_add(const ExtrusionLoop &ee, const InterpolationParameters ¶ms); + void interpolate_add(const ExtrusionEntityCollection &eec, const InterpolationParameters ¶ms); + + const Geometry::ArcWelder::Path* resolve(const Polyline *pl) const; + const Geometry::ArcWelder::Path* resolve(const ExtrusionPath &path) const; + + // Look-up a smooth representation of path in the cache. If it does not exist, produce a simplified polyline. + Geometry::ArcWelder::Path resolve_or_fit(const ExtrusionPath &path, bool reverse, double resolution) const; + + // Look-up a smooth representation of path in the cache. If it does not exist, produce a simplified polyline. + SmoothPath resolve_or_fit(const ExtrusionPaths &paths, bool reverse, double resolution) const; + SmoothPath resolve_or_fit(const ExtrusionMultiPath &path, bool reverse, double resolution) const; + + // Look-up a smooth representation of path in the cache. If it does not exist, produce a simplified polyline. + SmoothPath resolve_or_fit_split_with_seam( + const ExtrusionLoop &path, const bool reverse, const double resolution, + const Point &seam_point, const double seam_point_merge_distance_threshold) const; + +private: + ankerl::unordered_dense::map m_cache; +}; + +// Encapsulates references to global and layer local caches of smooth extrusion paths. +class SmoothPathCaches final +{ +public: + SmoothPathCaches() = delete; + SmoothPathCaches(const SmoothPathCache &global, const SmoothPathCache &layer_local) : + m_global(&global), m_layer_local(&layer_local) {} + SmoothPathCaches operator=(const SmoothPathCaches &rhs) + { m_global = rhs.m_global; m_layer_local = rhs.m_layer_local; return *this; } + + const SmoothPathCache& global() const { return *m_global; } + const SmoothPathCache& layer_local() const { return *m_layer_local; } + +private: + const SmoothPathCache *m_global; + const SmoothPathCache *m_layer_local; +}; + +} // namespace GCode +} // namespace Slic3r + +#endif // slic3r_GCode_SmoothPath_hpp_ diff --git a/src/libslic3r/GCode/SpiralVase.hpp b/src/libslic3r/GCode/SpiralVase.hpp index fb461c2..f8bfa8c 100644 --- a/src/libslic3r/GCode/SpiralVase.hpp +++ b/src/libslic3r/GCode/SpiralVase.hpp @@ -19,6 +19,9 @@ public: m_enabled = en; } + bool is_enabled() const { + return m_enabled; + } std::string process_layer(const std::string &gcode); private: diff --git a/src/libslic3r/GCode/Thumbnails.cpp b/src/libslic3r/GCode/Thumbnails.cpp index 48ecf52..ad0c1a3 100644 --- a/src/libslic3r/GCode/Thumbnails.cpp +++ b/src/libslic3r/GCode/Thumbnails.cpp @@ -1,12 +1,12 @@ #include "Thumbnails.hpp" #include "../miniz_extension.hpp" +#include "../format.hpp" #include #include #include -#include -#include -// #include "colpic3.h" +#include +#include namespace Slic3r::GCodeThumbnails { using namespace std::literals; @@ -30,76 +30,14 @@ struct CompressedQOI : CompressedImageBuffer }; -std::unique_ptr compress_thumbnail_png( - const ThumbnailData& data){ - auto out = std::make_unique(); - out->data = tdefl_write_image_to_png_file_in_memory_ex((const - void*)data.pixels.data(), data.width, data.height, 4, &out->size, - MZ_DEFAULT_LEVEL, 1); - return out; -} -//B3 -std::string compress_qidi_thumbnail_png(const ThumbnailData &data) +std::unique_ptr compress_thumbnail_png(const ThumbnailData &data) { auto out = std::make_unique(); - //BOOST_LOG_TRIVIAL(error) << data.width; - int width = int(data.width); - int height = int(data.height); - - if (data.width * data.height > 500 * 500) { - width = 500; - height = 500; - } - U16 color16[500*500]; - //U16 *color16 = new U16[data.width * data.height]; - //for (int i = 0; i < 200*200; i++) color16[i] = 522240; - unsigned char outputdata[500 * 500 * 10]; - //unsigned char *outputdata = new unsigned char[data.width * data.height * 10]; - - std::vector rgba_pixels(data.pixels.size() * 4); - size_t row_size = width * 4; - for (size_t y = 0; y > 3; - gggg = int(pixels[4 * (rr + cc) + 1]) >> 2; - bbbb = int(pixels[4 * (rr + cc) + 2]) >> 3; - aaaa = int(pixels[4 * (rr + cc) + 3]); - if (aaaa == 0) { - rrrr = 239 >> 3; - gggg = 243 >> 2; - bbbb = 247 >> 3; - } - rgb = (rrrr << 11) | (gggg << 5) | bbbb; - color16[time--] = rgb; - } - } - - int res = ColPic_EncodeStr(color16, width, height, outputdata, - height * width * 10, - 1024); - std::string temp; - - //for (unsigned char i : outputdata) { temp += i; } - for (unsigned int i = 0; i < sizeof(outputdata); ++i) { - temp +=outputdata[i]; - // unsigned char strr = outputdata[i]; - // temp += strr; - } - //out->data = tdefl_write_image_to_png_file_in_memory_ex((const void*)data.pixels.data(), data.width, data.height, 4, &out->size, MZ_DEFAULT_LEVEL, 1); - return temp; + out->data = tdefl_write_image_to_png_file_in_memory_ex((const void*)data.pixels.data(), data.width, data.height, 4, &out->size, MZ_DEFAULT_LEVEL, 1); + return out; } + std::unique_ptr compress_thumbnail_jpg(const ThumbnailData& data) { // Take vector of RGBA pixels and flip the image vertically @@ -169,14 +107,6 @@ std::unique_ptr compress_thumbnail_qoi(const ThumbnailDat return out; } -//B3 -std::string compress_qidi_thumbnail(const ThumbnailData& data, - GCodeThumbnailsFormat format) -{ - return compress_qidi_thumbnail_png(data); -} - -//B3 std::unique_ptr compress_thumbnail(const ThumbnailData &data, GCodeThumbnailsFormat format) { switch (format) { @@ -189,249 +119,85 @@ std::unique_ptr compress_thumbnail(const ThumbnailData &d return compress_thumbnail_qoi(data); } } -static void colmemmove(U8 *dec, U8 *src, int lenth) + + +std::pair make_and_check_thumbnail_list(const std::string& thumbnails_string, const std::string_view def_ext /*= "PNG"sv*/) { - if (src < dec) { - dec += lenth - 1; - src += lenth - 1; - while (lenth > 0) { - *(dec--) = *(src--); - lenth--; - } - } else { - while (lenth > 0) { - *(dec++) = *(src++); - lenth--; - } - } -} -static void colmemcpy(U8 *dec, U8 *src, int lenth) -{ - while (lenth > 0) { - *(dec++) = *(src++); - lenth--; - } -} -static void colmemset(U8 *dec, U8 val, int lenth) -{ - while (lenth > 0) { - *(dec++) = val; - lenth--; - } -} -static void ADList0(U16 val, U16HEAD *listu16, int *listqty, int maxqty) -{ - U8 A0; - U8 A1; - U8 A2; - int qty = *listqty; - if (qty >= maxqty) return; - for (int i = 0; i < qty; i++) { - if (listu16[i].colo16 == val) { - listu16[i].qty++; - return; - } - } - A0 = (U8) (val >> 11); - A1 = (U8) ((val << 5) >> 10); - A2 = (U8) ((val << 11) >> 11); - U16HEAD *a = &listu16[qty]; - a->colo16 = val; - a->A0 = A0; - a->A1 = A1; - a->A2 = A2; - a->qty = 1; - *listqty = qty + 1; -} -static int Byte8bitEncode(U16 *fromcolor16, - U16 *listu16, - int listqty, - int dotsqty, - U8 * outputdata, - int decMaxBytesize) -{ - U8 tid, sid; - int dots = 0; - int srcindex = 0; - int decindex = 0; - int lastid = 0; - int temp = 0; - while (dotsqty > 0) { - dots = 1; - for (int i = 0; i < (dotsqty - 1); i++) { - if (fromcolor16[srcindex + i] != fromcolor16[srcindex + i + 1]) - break; - dots++; - if (dots == 255) break; - } - temp = 0; - for (int i = 0; i < listqty; i++) { - if (listu16[i] == fromcolor16[srcindex]) { - temp = i; - break; + if (thumbnails_string.empty()) + return {}; + + std::istringstream is(thumbnails_string); + std::string point_str; + + ThumbnailErrors errors; + + // generate thumbnails data to process it + + GCodeThumbnailDefinitionsList thumbnails_list; + while (std::getline(is, point_str, ',')) { + Vec2d point(Vec2d::Zero()); + GCodeThumbnailsFormat format; + std::istringstream iss(point_str); + std::string coord_str; + if (std::getline(iss, coord_str, 'x') && !coord_str.empty()) { + std::istringstream(coord_str) >> point(0); + if (std::getline(iss, coord_str, '/') && !coord_str.empty()) { + std::istringstream(coord_str) >> point(1); + + if (0 < point(0) && point(0) < 1000 && 0 < point(1) && point(1) < 1000) { + std::string ext_str; + std::getline(iss, ext_str, '/'); + + if (ext_str.empty()) + ext_str = def_ext.empty() ? "PNG"sv : def_ext; + + // check validity of extention + boost::to_upper(ext_str); + if (!ConfigOptionEnum::from_string(ext_str, format)) { + format = GCodeThumbnailsFormat::PNG; + errors = enum_bitmask(errors | ThumbnailError::InvalidExt); + } + + thumbnails_list.emplace_back(std::make_pair(format, point)); + } + else + errors = enum_bitmask(errors | ThumbnailError::OutOfRange); + continue; } } - tid = (U8) (temp % 32); - sid = (U8) (temp / 32); - if (lastid != sid) { - if (decindex >= decMaxBytesize) goto IL_END; - outputdata[decindex] = 7; - outputdata[decindex] <<= 5; - outputdata[decindex] += sid; - decindex++; - lastid = sid; - } - if (dots <= 6) { - if (decindex >= decMaxBytesize) goto IL_END; - outputdata[decindex] = (U8) dots; - outputdata[decindex] <<= 5; - outputdata[decindex] += tid; - decindex++; - } else { - if (decindex >= decMaxBytesize) goto IL_END; - outputdata[decindex] = 0; - outputdata[decindex] += tid; - decindex++; - if (decindex >= decMaxBytesize) goto IL_END; - outputdata[decindex] = (U8) dots; - decindex++; - } - srcindex += dots; - dotsqty -= dots; + errors = enum_bitmask(errors | ThumbnailError::InvalidVal); } -IL_END: - return decindex; + + return std::make_pair(std::move(thumbnails_list), errors); } -static int ColPicEncode(U16 *fromcolor16, - int picw, - int pich, - U8 * outputdata, - int outputmaxtsize, - int colorsmax) + +std::pair make_and_check_thumbnail_list(const ConfigBase& config) { - U16HEAD l0; - int cha0, cha1, cha2, fid, minval; - ColPicHead3 *Head0 = null; - U16HEAD Listu16[1024]; - int ListQty = 0; - int enqty = 0; - int dotsqty = picw * pich; - if (colorsmax > 1024) colorsmax = 1024; - for (int i = 0; i < dotsqty; i++) { - int ch = (int) fromcolor16[i]; - ADList0(ch, Listu16, &ListQty, 1024); - } + // ??? Unit tests or command line slicing may not define "thumbnails" or "thumbnails_format". + // ??? If "thumbnails_format" is not defined, export to PNG. + // generate thumbnails data to process it - for (int index = 1; index < ListQty; index++) { - l0 = Listu16[index]; - for (int i = 0; i < index; i++) { - if (l0.qty >= Listu16[i].qty) { - colmemmove((U8 *) &Listu16[i + 1], (U8 *) &Listu16[i], - (index - i) * sizeof(U16HEAD)); - colmemcpy((U8 *) &Listu16[i], (U8 *) &l0, sizeof(U16HEAD)); - break; - } - } - } - while (ListQty > colorsmax) { - l0 = Listu16[ListQty - 1]; - minval = 255; - fid = -1; - for (int i = 0; i < colorsmax; i++) { - cha0 = Listu16[i].A0 - l0.A0; - if (cha0 < 0) cha0 = 0 - cha0; - cha1 = Listu16[i].A1 - l0.A1; - if (cha1 < 0) cha1 = 0 - cha1; - cha2 = Listu16[i].A2 - l0.A2; - if (cha2 < 0) cha2 = 0 - cha2; - int chall = cha0 + cha1 + cha2; - if (chall < minval) { - minval = chall; - fid = i; - } - } - for (int i = 0; i < dotsqty; i++) { - if (fromcolor16[i] == l0.colo16) - fromcolor16[i] = Listu16[fid].colo16; - } - ListQty = ListQty - 1; - } - Head0 = ((ColPicHead3 *) outputdata); - colmemset(outputdata, 0, sizeof(ColPicHead3)); - Head0->encodever = 3; - Head0->oncelistqty = 0; - Head0->mark = 0x05DDC33C; - Head0->ListDataSize = ListQty * 2; - for (int i = 0; i < ListQty; i++) { - U16 *l0 = (U16 *) &outputdata[sizeof(ColPicHead3)]; - l0[i] = Listu16[i].colo16; - } - enqty = - Byte8bitEncode(fromcolor16, (U16 *) &outputdata[sizeof(ColPicHead3)], - Head0->ListDataSize >> 1, dotsqty, - &outputdata[sizeof(ColPicHead3) + Head0->ListDataSize], - outputmaxtsize - sizeof(ColPicHead3) - - Head0->ListDataSize); - Head0->ColorDataSize = enqty; - Head0->PicW = picw; - Head0->PicH = pich; - return sizeof(ColPicHead3) + Head0->ListDataSize + Head0->ColorDataSize; + if (const auto thumbnails_value = config.option("thumbnails")) + return make_and_check_thumbnail_list(thumbnails_value->value); + + return {}; } -int ColPic_EncodeStr(U16 *fromcolor16, - int picw, - int pich, - U8 * outputdata, - int outputmaxtsize, - int colorsmax) + +std::string get_error_string(const ThumbnailErrors& errors) { - int qty = 0; - int temp = 0; - int strindex = 0; - int hexindex = 0; - U8 TempBytes[4]; - qty = ColPicEncode(fromcolor16, picw, pich, outputdata, outputmaxtsize, - colorsmax); - if (qty == 0) return 0; - temp = 3 - (qty % 3); - while (temp > 0) { - outputdata[qty] = 0; - qty++; - temp--; - } - if ((qty * 4 / 3) >= outputmaxtsize) return 0; - hexindex = qty; - strindex = (qty * 4 / 3); - while (hexindex > 0) { - hexindex -= 3; - strindex -= 4; + std::string error_str; - TempBytes[0] = (U8) (outputdata[hexindex] >> 2); - TempBytes[1] = (U8) (outputdata[hexindex] & 3); - TempBytes[1] <<= 4; - TempBytes[1] += ((U8) (outputdata[hexindex + 1] >> 4)); - TempBytes[2] = (U8) (outputdata[hexindex + 1] & 15); - TempBytes[2] <<= 2; - TempBytes[2] += ((U8) (outputdata[hexindex + 2] >> 6)); - TempBytes[3] = (U8) (outputdata[hexindex + 2] & 63); + if (errors.has(ThumbnailError::InvalidVal)) + error_str += "\n - " + format("Invalid input format. Expected vector of dimensions in the following format: \"%1%\"", "XxY/EXT, XxY/EXT, ..."); + if (errors.has(ThumbnailError::OutOfRange)) + error_str += "\n - Input value is out of range"; + if (errors.has(ThumbnailError::InvalidExt)) + error_str += "\n - Some extension in the input is invalid"; - TempBytes[0] += 48; - if (TempBytes[0] == (U8) '\\') TempBytes[0] = 126; - TempBytes[0 + 1] += 48; - if (TempBytes[0 + 1] == (U8) '\\') TempBytes[0 + 1] = 126; - TempBytes[0 + 2] += 48; - if (TempBytes[0 + 2] == (U8) '\\') TempBytes[0 + 2] = 126; - TempBytes[0 + 3] += 48; - if (TempBytes[0 + 3] == (U8) '\\') TempBytes[0 + 3] = 126; - - outputdata[strindex] = TempBytes[0]; - outputdata[strindex + 1] = TempBytes[1]; - outputdata[strindex + 2] = TempBytes[2]; - outputdata[strindex + 3] = TempBytes[3]; - } - qty = qty * 4 / 3; - outputdata[qty] = 0; - return qty; + return error_str; } + + } // namespace Slic3r::GCodeThumbnails diff --git a/src/libslic3r/GCode/Thumbnails.hpp b/src/libslic3r/GCode/Thumbnails.hpp index e1e5ad3..d6e619d 100644 --- a/src/libslic3r/GCode/Thumbnails.hpp +++ b/src/libslic3r/GCode/Thumbnails.hpp @@ -9,33 +9,18 @@ #include #include +#include #include -#include "DataType.h" +#include "../libslic3r/enum_bitmask.hpp" + +namespace Slic3r { + enum class ThumbnailError : int { InvalidVal, OutOfRange, InvalidExt }; + using ThumbnailErrors = enum_bitmask; + ENABLE_ENUM_BITMASK_OPERATORS(ThumbnailError); +} + -typedef struct -{ - U16 colo16; - U8 A0; - U8 A1; - U8 A2; - U8 res0; - U16 res1; - U32 qty; -}U16HEAD; -typedef struct -{ - U8 encodever; - U8 res0; - U16 oncelistqty; - U32 PicW; - U32 PicH; - U32 mark; - U32 ListDataSize; - U32 ColorDataSize; - U32 res1; - U32 res2; -}ColPicHead3; namespace Slic3r::GCodeThumbnails { @@ -48,81 +33,43 @@ struct CompressedImageBuffer }; std::unique_ptr compress_thumbnail(const ThumbnailData &data, GCodeThumbnailsFormat format); -//B3 -std::string compress_qidi_thumbnail( - const ThumbnailData &data, GCodeThumbnailsFormat format); -int ColPic_EncodeStr(U16* fromcolor16, int picw, int pich, U8* outputdata, int outputmaxtsize, int colorsmax); + +typedef std::vector> GCodeThumbnailDefinitionsList; + +using namespace std::literals; +std::pair make_and_check_thumbnail_list(const std::string& thumbnails_string, const std::string_view def_ext = "PNG"sv); +std::pair make_and_check_thumbnail_list(const ConfigBase &config); + +std::string get_error_string(const ThumbnailErrors& errors); + + template -inline void export_thumbnails_to_file(ThumbnailsGeneratorCallback &thumbnail_cb, const std::vector &sizes, GCodeThumbnailsFormat format, WriteToOutput output, ThrowIfCanceledCallback throw_if_canceled) +inline void export_thumbnails_to_file(ThumbnailsGeneratorCallback &thumbnail_cb, const std::vector>& thumbnails_list, WriteToOutput output, ThrowIfCanceledCallback throw_if_canceled) { // Write thumbnails using base64 encoding if (thumbnail_cb != nullptr) { + for (const auto& [format, size] : thumbnails_list) { static constexpr const size_t max_row_length = 78; - // ThumbnailsList thumbnails = thumbnail_cb(ThumbnailsParams{ sizes, true, true, true, true }); - //B3 - ThumbnailsList thumbnails = thumbnail_cb(ThumbnailsParams{ sizes, true, false, false, true }); - int count = 0; + ThumbnailsList thumbnails = thumbnail_cb(ThumbnailsParams{ {size}, true, true, true, true }); for (const ThumbnailData& data : thumbnails) if (data.is_valid()) { - //B3 - switch (format) { - case GCodeThumbnailsFormat::QIDI: - { - auto compressed = compress_qidi_thumbnail(data, - format); - if (count == 0) { - output( - (boost::format("\n\n;gimage:%s\n\n") % compressed) - .str() - .c_str()); - count++; - break; - } else { - output( - (boost::format("\n\n;simage:%s\n\n") % compressed) - .str() - .c_str()); - count++; - break; - } - } - case GCodeThumbnailsFormat::JPG: - default: { auto compressed = compress_thumbnail(data, format); if (compressed->data && compressed->size) { std::string encoded; - encoded.resize( - boost::beast::detail::base64::encoded_size( - compressed->size)); - encoded.resize(boost::beast::detail::base64::encode( - (void *) encoded.data(), - (const void *) compressed->data, - compressed->size)); - output((boost::format("\n;\n; %s begin %dx%d %d\n") % - compressed->tag() % data.width % data.height % - encoded.size()) - .str() - .c_str()); + encoded.resize(boost::beast::detail::base64::encoded_size(compressed->size)); + encoded.resize(boost::beast::detail::base64::encode((void*)encoded.data(), (const void*)compressed->data, compressed->size)); + + output((boost::format("\n;\n; %s begin %dx%d %d\n") % compressed->tag() % data.width % data.height % encoded.size()).str().c_str()); while (encoded.size() > max_row_length) { - output((boost::format("; %s\n") % - encoded.substr(0, max_row_length)) - .str() - .c_str()); + output((boost::format("; %s\n") % encoded.substr(0, max_row_length)).str().c_str()); encoded = encoded.substr(max_row_length); } if (encoded.size() > 0) - output((boost::format("; %s\n") % encoded) - .str() - .c_str()); + output((boost::format("; %s\n") % encoded).str().c_str()); - output((boost::format("; %s end\n;\n") % - compressed->tag()) - .str() - .c_str()); - } - } + output((boost::format("; %s end\n;\n") % compressed->tag()).str().c_str()); } @@ -130,6 +77,41 @@ inline void export_thumbnails_to_file(ThumbnailsGeneratorCallback &thumbnail_cb, } } } +} + +template +inline void generate_binary_thumbnails(ThumbnailsGeneratorCallback& thumbnail_cb, std::vector& out_thumbnails, + const std::vector> &thumbnails_list, ThrowIfCanceledCallback throw_if_canceled) +{ + using namespace bgcode::core; + using namespace bgcode::binarize; + out_thumbnails.clear(); + assert(thumbnail_cb != nullptr); + if (thumbnail_cb != nullptr) { + for (const auto& [format, size] : thumbnails_list) { + ThumbnailsList thumbnails = thumbnail_cb(ThumbnailsParams{ {size}, true, true, true, true }); + for (const ThumbnailData &data : thumbnails) + if (data.is_valid()) { + auto compressed = compress_thumbnail(data, format); + if (compressed->data != nullptr && compressed->size > 0) { + ThumbnailBlock& block = out_thumbnails.emplace_back(ThumbnailBlock()); + block.params.width = (uint16_t)data.width; + block.params.height = (uint16_t)data.height; + switch (format) { + case GCodeThumbnailsFormat::PNG: { block.params.format = (uint16_t)EThumbnailFormat::PNG; break; } + case GCodeThumbnailsFormat::JPG: { block.params.format = (uint16_t)EThumbnailFormat::JPG; break; } + case GCodeThumbnailsFormat::QOI: { block.params.format = (uint16_t)EThumbnailFormat::QOI; break; } + } + block.data.resize(compressed->size); + memcpy(block.data.data(), compressed->data, compressed->size); + } + } + } + } +} + + + } // namespace Slic3r::GCodeThumbnails diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index caf461b..f13e8bf 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -474,6 +474,8 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ bool ToolOrdering::insert_wipe_tower_extruder() { + if (!m_print_config_ptr->wipe_tower) + return false; // In case that wipe_tower_extruder is set to non-zero, we must make sure that the extruder will be in the list. bool changed = false; if (m_print_config_ptr->wipe_tower_extruder != 0) { @@ -831,4 +833,18 @@ void WipingExtrusions::ensure_perimeters_infills_order(const Print& print, const } } +int ToolOrdering::toolchanges_count() const +{ + std::vector tools_in_order; + for (const LayerTools& lt : m_layer_tools) + tools_in_order.insert(tools_in_order.end(), lt.extruders.begin(), lt.extruders.end()); + assert(std::find(tools_in_order.begin(), tools_in_order.end(), (unsigned int)(-1)) == tools_in_order.end()); + for (size_t i=1; i 1 && tools_in_order.back() == tools_in_order[tools_in_order.size()-2]) + tools_in_order.pop_back(); + return std::max(0, int(tools_in_order.size())-1); // 5 tools = 4 toolchanges +} } // namespace Slic3r diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f4685eb..4cc62b6 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -161,6 +161,7 @@ public: bool empty() const { return m_layer_tools.empty(); } std::vector& layer_tools() { return m_layer_tools; } bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().wipe_tower_partitions > 0; } + int toolchanges_count() const; private: void initialize_layers(std::vector &zs); diff --git a/src/libslic3r/GCode/Wipe.cpp b/src/libslic3r/GCode/Wipe.cpp new file mode 100644 index 0000000..fd7cacd --- /dev/null +++ b/src/libslic3r/GCode/Wipe.cpp @@ -0,0 +1,257 @@ +#include "Wipe.hpp" +#include "../GCode.hpp" + +#include + +#include + +using namespace std::string_view_literals; + +namespace Slic3r::GCode { + +void Wipe::init(const PrintConfig &config, const std::vector &extruders) +{ + this->reset_path(); + + // Calculate maximum wipe length to accumulate by the wipe cache. + // Paths longer than wipe_xy should never be needed for the wipe move. + double wipe_xy = 0; + const bool multimaterial = extruders.size() > 1; + for (auto id : extruders) + if (config.wipe.get_at(id)) { + // Wipe length to extrusion ratio. + const double xy_to_e = this->calc_xy_to_e_ratio(config, id); + wipe_xy = std::max(wipe_xy, xy_to_e * config.retract_length.get_at(id)); + if (multimaterial) + wipe_xy = std::max(wipe_xy, xy_to_e * config.retract_length_toolchange.get_at(id)); + } + + if (wipe_xy == 0) + this->disable(); + else + this->enable(wipe_xy); +} + +void Wipe::set_path(SmoothPath &&path, bool reversed) +{ + this->reset_path(); + + if (this->enabled() && ! path.empty()) { + if (reversed) { + m_path = std::move(path.back().path); + Geometry::ArcWelder::reverse(m_path); + int64_t len = Geometry::ArcWelder::estimate_path_length(m_path); + for (auto it = std::next(path.rbegin()); len < m_wipe_len_max && it != path.rend(); ++ it) { + if (it->path_attributes.role.is_bridge()) + break; // Do not perform a wipe on bridges. + assert(it->path.size() >= 2); + assert(m_path.back().point == it->path.back().point); + if (m_path.back().point != it->path.back().point) + // ExtrusionMultiPath is interrupted in some place. This should not really happen. + break; + len += Geometry::ArcWelder::estimate_path_length(it->path); + m_path.insert(m_path.end(), it->path.rbegin() + 1, it->path.rend()); + } + } else { + m_path = std::move(path.front().path); + int64_t len = Geometry::ArcWelder::estimate_path_length(m_path); + for (auto it = std::next(path.begin()); len < m_wipe_len_max && it != path.end(); ++ it) { + if (it->path_attributes.role.is_bridge()) + break; // Do not perform a wipe on bridges. + assert(it->path.size() >= 2); + assert(m_path.back().point == it->path.front().point); + if (m_path.back().point != it->path.front().point) + // ExtrusionMultiPath is interrupted in some place. This should not really happen. + break; + len += Geometry::ArcWelder::estimate_path_length(it->path); + m_path.insert(m_path.end(), it->path.begin() + 1, it->path.end()); + } + } + } + + assert(m_path.empty() || m_path.size() > 1); +} + +std::string Wipe::wipe(GCodeGenerator &gcodegen, bool toolchange) +{ + std::string gcode; + const Extruder &extruder = *gcodegen.writer().extruder(); + static constexpr const std::string_view wipe_retract_comment = "wipe and retract"sv; + + // Remaining quantized retraction length. + if (double retract_length = extruder.retract_to_go(toolchange ? extruder.retract_length_toolchange() : extruder.retract_length()); + retract_length > 0 && this->has_path()) { + // Delayed emitting of a wipe start tag. + bool wiped = false; + const double wipe_speed = this->calc_wipe_speed(gcodegen.writer().config); + auto start_wipe = [&wiped, &gcode, &gcodegen, wipe_speed](){ + if (! wiped) { + wiped = true; + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start) + "\n"; + gcode += gcodegen.writer().set_speed(wipe_speed * 60, {}, gcodegen.enable_cooling_markers() ? ";_WIPE"sv : ""sv); + } + }; + const double xy_to_e = this->calc_xy_to_e_ratio(gcodegen.writer().config, extruder.id()); + auto wipe_linear = [&gcode, &gcodegen, &retract_length, xy_to_e](const Vec2d &prev_quantized, Vec2d &p) { + Vec2d p_quantized = GCodeFormatter::quantize(p); + if (p_quantized == prev_quantized) { + p = p_quantized; + return false; + } + double segment_length = (p_quantized - prev_quantized).norm(); + // Quantize E axis as it is to be extruded as a whole segment. + double dE = GCodeFormatter::quantize_e(xy_to_e * segment_length); + bool done = false; + if (dE > retract_length - EPSILON) { + if (dE > retract_length + EPSILON) + // Shorten the segment. + p = GCodeFormatter::quantize(Vec2d(prev_quantized + (p - prev_quantized) * (retract_length / dE))); + else + p = p_quantized; + dE = retract_length; + done = true; + } else + p = p_quantized; + gcode += gcodegen.writer().extrude_to_xy(p, -dE, wipe_retract_comment); + retract_length -= dE; + return done; + }; + auto wipe_arc = [&gcode, &gcodegen, &retract_length, xy_to_e, &wipe_linear]( + const Vec2d &prev_quantized, Vec2d &p, double radius_in, const bool ccw) { + Vec2d p_quantized = GCodeFormatter::quantize(p); + if (p_quantized == prev_quantized) { + p = p_quantized; + return false; + } + // Use the exact radius for calculating the IJ values, no quantization. + double radius = radius_in; + if (radius == 0) + // Degenerated arc after quantization. Process it as if it was a line segment. + return wipe_linear(prev_quantized, p); + Vec2d center = Geometry::ArcWelder::arc_center(prev_quantized.cast(), p_quantized.cast(), double(radius), ccw); + float angle = Geometry::ArcWelder::arc_angle(prev_quantized.cast(), p_quantized.cast(), double(radius)); + assert(angle > 0); + double segment_length = angle * std::abs(radius); + double dE = GCodeFormatter::quantize_e(xy_to_e * segment_length); + bool done = false; + if (dE > retract_length - EPSILON) { + if (dE > retract_length + EPSILON) { + // Shorten the segment. Recalculate the arc from the unquantized end coordinate. + center = Geometry::ArcWelder::arc_center(prev_quantized.cast(), p.cast(), double(radius), ccw); + angle = Geometry::ArcWelder::arc_angle(prev_quantized.cast(), p.cast(), double(radius)); + segment_length = angle * std::abs(radius); + dE = xy_to_e * segment_length; + p = GCodeFormatter::quantize( + Vec2d(center + Eigen::Rotation2D((ccw ? angle : -angle) * (retract_length / dE)) * (prev_quantized - center))); + } else + p = p_quantized; + dE = retract_length; + done = true; + } else + p = p_quantized; + assert(dE > 0); + { + // Calculate quantized IJ circle center offset. + Vec2d ij = GCodeFormatter::quantize(Vec2d(center - prev_quantized)); + if (ij == Vec2d::Zero()) + // Degenerated arc after quantization. Process it as if it was a line segment. + return wipe_linear(prev_quantized, p); + // The arc is valid. + gcode += gcodegen.writer().extrude_to_xy_G2G3IJ( + p, ij, ccw, -dE, wipe_retract_comment); + } + retract_length -= dE; + return done; + }; + // Start with the current position, which may be different from the wipe path start in case of loop clipping. + Vec2d prev = gcodegen.point_to_gcode_quantized(gcodegen.last_pos()); + auto it = this->path().begin(); + Vec2d p = gcodegen.point_to_gcode(it->point + m_offset); + ++ it; + bool done = false; + if (p != prev) { + start_wipe(); + done = wipe_linear(prev, p); + } + if (! done) { + prev = p; + auto end = this->path().end(); + for (; it != end && ! done; ++ it) { + p = gcodegen.point_to_gcode(it->point + m_offset); + if (p != prev) { + start_wipe(); + if (it->linear() ? + wipe_linear(prev, p) : + wipe_arc(prev, p, unscaled(it->radius), it->ccw())) + break; + prev = p; + } + } + } + if (wiped) { + // add tag for processor + assert(p == GCodeFormatter::quantize(p)); + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End) + "\n"; + gcodegen.set_last_pos(gcodegen.gcode_to_point(p)); + } + } + + // Prevent wiping again on the same path. + this->reset_path(); + return gcode; +} + +// Make a little move inwards before leaving loop after path was extruded, +// thus the current extruder position is at the end of a path and the path +// may not be closed in case the loop was clipped to hide a seam. +std::optional wipe_hide_seam(const SmoothPath &path, bool is_hole, double wipe_length) +{ + assert(! path.empty()); + assert(path.front().path.size() >= 2); + assert(path.back().path.size() >= 2); + + // Heuristics for estimating whether there is a chance that the wipe move will fit inside a small perimeter + // or that the wipe move direction could be calculated with reasonable accuracy. + if (longer_than(path, 2.5 * wipe_length)) { + // The print head will be moved away from path end inside the island. + Point p_current = path.back().path.back().point; + Point p_next = path.front().path.front().point; + Point p_prev; + { + // Is the seam hiding gap large enough already? + double l = wipe_length - (p_next - p_current).cast().norm(); + if (l > 0) { + // Not yet. + std::optional n = sample_path_point_at_distance_from_start(path, l); + assert(n); + if (! n) + // Wipe move cannot be calculated, the loop is not long enough. This should not happen due to the longer_than() test above. + return {}; + } + if (std::optional p = sample_path_point_at_distance_from_end(path, wipe_length); p) + p_prev = *p; + else + // Wipe move cannot be calculated, the loop is not long enough. This should not happen due to the longer_than() test above. + return {}; + } + // Detect angle between last and first segment. + // The side depends on the original winding order of the polygon (left for contours, right for holes). + double angle_inside = angle(p_next - p_current, p_prev - p_current); + assert(angle_inside >= -M_PI && angle_inside <= M_PI); + // 3rd of this angle will be taken, thus make the angle monotonic before interpolation. + if (is_hole) { + if (angle_inside > 0) + angle_inside -= 2.0 * M_PI; + } else { + if (angle_inside < 0) + angle_inside += 2.0 * M_PI; + } + // Rotate the forward segment inside by 1/3 of the wedge angle. + auto v_rotated = Eigen::Rotation2D(angle_inside) * (p_next - p_current).cast().normalized(); + return std::make_optional(p_current + (v_rotated * wipe_length).cast()); + } + + return {}; +} + +} // namespace Slic3r::GCode diff --git a/src/libslic3r/GCode/Wipe.hpp b/src/libslic3r/GCode/Wipe.hpp new file mode 100644 index 0000000..f02a77f --- /dev/null +++ b/src/libslic3r/GCode/Wipe.hpp @@ -0,0 +1,73 @@ +#ifndef slic3r_GCode_Wipe_hpp_ +#define slic3r_GCode_Wipe_hpp_ + +#include "SmoothPath.hpp" + +#include "../Geometry/ArcWelder.hpp" +#include "../Point.hpp" +#include "../PrintConfig.hpp" + +#include +#include + +namespace Slic3r { + +class GCodeGenerator; + +namespace GCode { + +class Wipe { +public: + using Path = Slic3r::Geometry::ArcWelder::Path; + + Wipe() = default; + + void init(const PrintConfig &config, const std::vector &extruders); + void enable(double wipe_len_max) { m_enabled = true; m_wipe_len_max = wipe_len_max; } + void disable() { m_enabled = false; } + bool enabled() const { return m_enabled; } + + const Path& path() const { return m_path; } + bool has_path() const { assert(m_path.empty() || m_path.size() > 1); return ! m_path.empty(); } + void reset_path() { m_path.clear(); m_offset = Point::Zero(); } + void set_path(const Path &path) { + assert(path.empty() || path.size() > 1); + this->reset_path(); + if (this->enabled() && path.size() > 1) + m_path = path; + } + void set_path(Path &&path) { + assert(path.empty() || path.size() > 1); + this->reset_path(); + if (this->enabled() && path.size() > 1) + m_path = std::move(path); + } + void set_path(SmoothPath &&path, bool reversed); + void offset_path(const Point &v) { m_offset += v; } + + std::string wipe(GCodeGenerator &gcodegen, bool toolchange); + + // Reduce feedrate a bit; travel speed is often too high to move on existing material. + // Too fast = ripping of existing material; too slow = short wipe path, thus more blob. + static double calc_wipe_speed(const GCodeConfig &config) { return config.travel_speed.value * 0.8; } + // Reduce retraction length a bit to avoid effective retraction speed to be greater than the configured one + // due to rounding (TODO: test and/or better math for this). + static double calc_xy_to_e_ratio(const GCodeConfig &config, unsigned int extruder_id) + { return 0.95 * floor(config.retract_speed.get_at(extruder_id) + 0.5) / calc_wipe_speed(config); } + +private: + bool m_enabled{ false }; + // Maximum length of a path to accumulate. Only wipes shorter than this threshold will be requested. + double m_wipe_len_max{ 0. }; + Path m_path; + // Offset from m_path to the current PrintObject active. + Point m_offset{ Point::Zero() }; +}; + +// Make a little move inwards before leaving loop. +std::optional wipe_hide_seam(const SmoothPath &path, bool is_hole, double wipe_length); + +} // namespace GCode +} // namespace Slic3r + +#endif // slic3r_GCode_Wipe_hpp_ diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index c64556b..dcad30b 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -530,6 +530,7 @@ WipeTower::WipeTower(const PrintConfig& config, const PrintRegionConfig& default m_no_sparse_layers(config.wipe_tower_no_sparse_layers), m_gcode_flavor(config.gcode_flavor), m_travel_speed(config.travel_speed), + m_travel_speed_z(config.travel_speed_z), m_infill_speed(default_region_config.infill_speed), m_perimeter_speed(default_region_config.perimeter_speed), m_current_tool(initial_tool), @@ -786,12 +787,13 @@ WipeTower::ToolChangeResult WipeTower::tool_change(size_t tool) .set_initial_tool(m_current_tool) .set_y_shift(m_y_shift + (tool!=(unsigned int)(-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth(): 0.f)) .append(";--------------------\n" - "; CP TOOLCHANGE START\n") - .comment_with_value(" toolchange #", m_num_tool_changes + 1); // the number is zero-based + "; CP TOOLCHANGE START\n"); - if (tool != (unsigned)(-1)) + if (tool != (unsigned)(-1)) { + writer.comment_with_value(" toolchange #", m_num_tool_changes + 1); // the number is zero-based writer.append(std::string("; material : " + (m_current_tool < m_filpar.size() ? m_filpar[m_current_tool].material : "(NONE)") + " -> " + m_filpar[tool].material + "\n").c_str()) .append(";--------------------\n"); + } writer.speed_override_backup(); writer.speed_override(100); @@ -1001,7 +1003,7 @@ void WipeTower::toolchange_Change( // This is where we want to place the custom gcodes. We will use placeholders for this. // These will be substituted by the actual gcodes when the gcode is generated. //writer.append("[end_filament_gcode]\n"); - writer.append("[toolchange_gcode]\n"); + writer.append("[toolchange_gcode_from_wipe_tower_generator]\n"); // Travel to where we assume we are. Custom toolchange or some special T code handling (parking extruder etc) // gcode could have left the extruder somewhere, we cannot just start extruding. We should also inform the @@ -1010,7 +1012,8 @@ void WipeTower::toolchange_Change( writer.feedrate(m_travel_speed * 60.f) // see https://github.com/qidi3d/QIDISlicer/issues/5483 .append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(current_pos.x()) + " Y" + Slic3r::float_to_string_decimal_point(current_pos.y()) - + never_skip_tag() + "\n"); + + never_skip_tag() + "\n" + ); writer.append("[deretraction_from_wipe_tower_generator]"); // The toolchange Tn command will be inserted later, only in case that the user does @@ -1479,6 +1482,10 @@ void WipeTower::save_on_last_wipe() // Which toolchange will finish_layer extrusions be subtracted from? int idx = first_toolchange_to_nonsoluble(m_layer_info->tool_changes); + if (idx == -1) { + // In this case, finish_layer will be called at the very beginning. + finish_layer().total_extrusion_length_in_plane(); + } for (int i=0; itool_changes.size()); ++i) { auto& toolchange = m_layer_info->tool_changes[i]; tool_change(toolchange.new_tool); @@ -1490,9 +1497,9 @@ void WipeTower::save_on_last_wipe() m_perimeter_width, m_layer_info->height) - toolchange.first_wipe_line - length_to_save; length_to_wipe = std::max(length_to_wipe,0.f); - float depth_to_wipe = m_perimeter_width * (std::floor(length_to_wipe/width) + ( length_to_wipe > 0.f ? 1.f : 0.f ) ) * m_extra_spacing; + float depth_to_wipe = m_perimeter_width * (std::floor(length_to_wipe/width) + ( length_to_wipe > 0.f ? 1.f : 0.f ) ); - toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe; + toolchange.required_depth = (toolchange.ramming_depth + depth_to_wipe) * m_extra_spacing; } } } @@ -1558,9 +1565,9 @@ void WipeTower::generate(std::vector> & m_old_temperature = -1; // reset last temperature written in the gcode - std::vector layer_result; for (const WipeTower::WipeTowerInfo& layer : m_plan) { + std::vector layer_result; set_layer(layer.z, layer.height, 0, false/*layer.z == m_plan.front().z*/, layer.z == m_plan.back().z); m_internal_rotation += 180.f; diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index 1e23698..d28ee38 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -275,6 +275,7 @@ private: size_t m_max_color_changes = 0; // Maximum number of color changes per layer. int m_old_temperature = -1; // To keep track of what was the last temp that we set (so we don't issue the command when not neccessary) float m_travel_speed = 0.f; + float m_travel_speed_z = 0.f; float m_infill_speed = 0.f; float m_perimeter_speed = 0.f; float m_first_layer_speed = 0.f; diff --git a/src/libslic3r/GCode/WipeTowerIntegration.cpp b/src/libslic3r/GCode/WipeTowerIntegration.cpp new file mode 100644 index 0000000..4b51d27 --- /dev/null +++ b/src/libslic3r/GCode/WipeTowerIntegration.cpp @@ -0,0 +1,257 @@ +#include "WipeTowerIntegration.hpp" + +#include "../GCode.hpp" +#include "../libslic3r.h" + +#include "boost/algorithm/string/replace.hpp" + +namespace Slic3r::GCode { + +static inline Point wipe_tower_point_to_object_point(GCodeGenerator &gcodegen, const Vec2f& wipe_tower_pt) +{ + return Point(scale_(wipe_tower_pt.x() - gcodegen.origin()(0)), scale_(wipe_tower_pt.y() - gcodegen.origin()(1))); +} + +std::string WipeTowerIntegration::append_tcr(GCodeGenerator &gcodegen, const WipeTower::ToolChangeResult& tcr, int new_extruder_id, double z) const +{ + if (new_extruder_id != -1 && new_extruder_id != tcr.new_tool) + throw Slic3r::InvalidArgument("Error: WipeTowerIntegration::append_tcr was asked to do a toolchange it didn't expect."); + + std::string gcode; + + // Toolchangeresult.gcode assumes the wipe tower corner is at the origin (except for priming lines) + // We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position + float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); + + auto transform_wt_pt = [&alpha, this](const Vec2f& pt) -> Vec2f { + Vec2f out = Eigen::Rotation2Df(alpha) * pt; + out += m_wipe_tower_pos; + return out; + }; + + Vec2f start_pos = tcr.start_pos; + Vec2f end_pos = tcr.end_pos; + if (! tcr.priming) { + start_pos = transform_wt_pt(start_pos); + end_pos = transform_wt_pt(end_pos); + } + + Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : m_wipe_tower_pos; + float wipe_tower_rotation = tcr.priming ? 0.f : alpha; + + std::string tcr_rotated_gcode = post_process_wipe_tower_moves(tcr, wipe_tower_offset, wipe_tower_rotation); + + double current_z = gcodegen.writer().get_position().z(); + gcode += gcodegen.writer().travel_to_z(current_z); + + if (z == -1.) // in case no specific z was provided, print at current_z pos + z = current_z; + + const bool needs_toolchange = gcodegen.writer().need_toolchange(new_extruder_id); + const bool will_go_down = ! is_approx(z, current_z); + const bool is_ramming = (gcodegen.config().single_extruder_multi_material) + || (! gcodegen.config().single_extruder_multi_material && gcodegen.config().filament_multitool_ramming.get_at(tcr.initial_tool)); + const bool should_travel_to_tower = ! tcr.priming + && (tcr.force_travel // wipe tower says so + || ! needs_toolchange // this is just finishing the tower with no toolchange + || is_ramming + || will_go_down); // don't dig into the print + if (should_travel_to_tower) { + gcode += gcodegen.retract_and_wipe(); + gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); + gcode += gcodegen.travel_to( + wipe_tower_point_to_object_point(gcodegen, start_pos), + ExtrusionRole::Mixed, + "Travel to a Wipe Tower"); + gcode += gcodegen.unretract(); + } else { + // When this is multiextruder printer without any ramming, we can just change + // the tool without travelling to the tower. + } + + if (will_go_down) { + gcode += gcodegen.writer().retract(); + gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer."); + gcode += gcodegen.writer().unretract(); + } + + std::string toolchange_gcode_str; + std::string deretraction_str; + if (tcr.priming || (new_extruder_id >= 0 && needs_toolchange)) { + if (is_ramming) + gcodegen.m_wipe.reset_path(); // We don't want wiping on the ramming lines. + toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z); // TODO: toolchange_z vs print_z + if (gcodegen.config().wipe_tower) + deretraction_str += gcodegen.writer().get_travel_to_z_gcode(z, "restore layer Z"); + deretraction_str += gcodegen.unretract(); + + } + assert(toolchange_gcode_str.empty() || toolchange_gcode_str.back() == '\n'); + assert(deretraction_str.empty() || deretraction_str.back() == '\n'); + + // Insert the toolchange and deretraction gcode into the generated gcode. + boost::replace_first(tcr_rotated_gcode, "[toolchange_gcode_from_wipe_tower_generator]", toolchange_gcode_str); + boost::replace_first(tcr_rotated_gcode, "[deretraction_from_wipe_tower_generator]", deretraction_str); + std::string tcr_gcode; + unescape_string_cstyle(tcr_rotated_gcode, tcr_gcode); + gcode += tcr_gcode; + + // A phony move to the end position at the wipe tower. + gcodegen.writer().travel_to_xy(end_pos.cast()); + gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, end_pos)); + if (!is_approx(z, current_z)) { + gcode += gcodegen.writer().retract(); + gcode += gcodegen.writer().travel_to_z(current_z, "Travel back up to the topmost object layer."); + gcode += gcodegen.writer().unretract(); + } + + else { + // Prepare a future wipe. + // Convert to a smooth path. + Geometry::ArcWelder::Path path; + path.reserve(tcr.wipe_path.size()); + std::transform(tcr.wipe_path.begin(), tcr.wipe_path.end(), std::back_inserter(path), + [&gcodegen, &transform_wt_pt](const Vec2f &wipe_pt) { + return Geometry::ArcWelder::Segment{ wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(wipe_pt)) }; + }); + // Pass to the wipe cache. + gcodegen.m_wipe.set_path(std::move(path)); + } + + // Let the planner know we are traveling between objects. + gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); + return gcode; +} + +// This function postprocesses gcode_original, rotates and moves all G1 extrusions and returns resulting gcode +// Starting position has to be supplied explicitely (otherwise it would fail in case first G1 command only contained one coordinate) +std::string WipeTowerIntegration::post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const +{ + Vec2f extruder_offset = m_extruder_offsets[tcr.initial_tool].cast(); + + std::istringstream gcode_str(tcr.gcode); + std::string gcode_out; + std::string line; + Vec2f pos = tcr.start_pos; + Vec2f transformed_pos = Eigen::Rotation2Df(angle) * pos + translation; + Vec2f old_pos(-1000.1f, -1000.1f); + + while (gcode_str) { + std::getline(gcode_str, line); // we read the gcode line by line + + // All G1 commands should be translated and rotated. X and Y coords are + // only pushed to the output when they differ from last time. + // WT generator can override this by appending the never_skip_tag + if (boost::starts_with(line, "G1 ")) { + bool never_skip = false; + auto it = line.find(WipeTower::never_skip_tag()); + if (it != std::string::npos) { + // remove the tag and remember we saw it + never_skip = true; + line.erase(it, it + WipeTower::never_skip_tag().size()); + } + std::ostringstream line_out; + std::istringstream line_str(line); + line_str >> std::noskipws; // don't skip whitespace + char ch = 0; + line_str >> ch >> ch; // read the "G1" + while (line_str >> ch) { + if (ch == 'X' || ch == 'Y') + line_str >> (ch == 'X' ? pos.x() : pos.y()); + else + line_out << ch; + } + + line = line_out.str(); + boost::trim(line); // Remove leading and trailing spaces. + + transformed_pos = Eigen::Rotation2Df(angle) * pos + translation; + + if (transformed_pos != old_pos || never_skip || ! line.empty()) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(3) << "G1"; + if (transformed_pos.x() != old_pos.x() || never_skip) + oss << " X" << transformed_pos.x() - extruder_offset.x(); + if (transformed_pos.y() != old_pos.y() || never_skip) + oss << " Y" << transformed_pos.y() - extruder_offset.y(); + if (! line.empty()) + oss << " "; + line = oss.str() + line; + old_pos = transformed_pos; + } + } + + gcode_out += line + "\n"; + + // If this was a toolchange command, we should change current extruder offset + if (line == "[toolchange_gcode_from_wipe_tower_generator]") { + extruder_offset = m_extruder_offsets[tcr.new_tool].cast(); + + // If the extruder offset changed, add an extra move so everything is continuous + if (extruder_offset != m_extruder_offsets[tcr.initial_tool].cast()) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(3) + << "G1 X" << transformed_pos.x() - extruder_offset.x() + << " Y" << transformed_pos.y() - extruder_offset.y() + << "\n"; + gcode_out += oss.str(); + } + } + } + return gcode_out; +} + + +std::string WipeTowerIntegration::prime(GCodeGenerator &gcodegen) +{ + std::string gcode; + for (const WipeTower::ToolChangeResult& tcr : m_priming) { + if (! tcr.extrusions.empty()) + gcode += append_tcr(gcodegen, tcr, tcr.new_tool); + } + return gcode; +} + +std::string WipeTowerIntegration::tool_change(GCodeGenerator &gcodegen, int extruder_id, bool finish_layer) +{ + std::string gcode; + assert(m_layer_idx >= 0); + if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) { + if (m_layer_idx < (int)m_tool_changes.size()) { + if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size())) + throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer."); + + // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, + // resulting in a wipe tower with sparse layers. + double wipe_tower_z = -1; + bool ignore_sparse = false; + if (gcodegen.config().wipe_tower_no_sparse_layers.value) { + wipe_tower_z = m_last_wipe_tower_print_z; + ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool && m_layer_idx != 0); + if (m_tool_change_idx == 0 && !ignore_sparse) + wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height; + } + + if (!ignore_sparse) { + gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z); + m_last_wipe_tower_print_z = wipe_tower_z; + } + } + } + return gcode; +} + +// Print is finished. Now it remains to unload the filament safely with ramming over the wipe tower. +std::string WipeTowerIntegration::finalize(GCodeGenerator &gcodegen) +{ + std::string gcode; + if (std::abs(gcodegen.writer().get_position().z() - m_final_purge.print_z) > EPSILON) + gcode += gcodegen.generate_travel_gcode( + {{gcodegen.last_pos().x(), gcodegen.last_pos().y(), scaled(m_final_purge.print_z)}}, + "move to safe place for purging" + ); + gcode += append_tcr(gcodegen, m_final_purge, -1); + return gcode; +} + +} // namespace Slic3r::GCode diff --git a/src/libslic3r/GCode/WipeTowerIntegration.hpp b/src/libslic3r/GCode/WipeTowerIntegration.hpp new file mode 100644 index 0000000..52b2762 --- /dev/null +++ b/src/libslic3r/GCode/WipeTowerIntegration.hpp @@ -0,0 +1,65 @@ +#ifndef slic3r_GCode_WipeTowerIntegration_hpp_ +#define slic3r_GCode_WipeTowerIntegration_hpp_ + +#include "WipeTower.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +class GCodeGenerator; + +namespace GCode { + +class WipeTowerIntegration { +public: + WipeTowerIntegration( + const PrintConfig &print_config, + const std::vector &priming, + const std::vector> &tool_changes, + const WipeTower::ToolChangeResult &final_purge) : + m_left(/*float(print_config.wipe_tower_x.value)*/ 0.f), + m_right(float(/*print_config.wipe_tower_x.value +*/ print_config.wipe_tower_width.value)), + m_wipe_tower_pos(float(print_config.wipe_tower_x.value), float(print_config.wipe_tower_y.value)), + m_wipe_tower_rotation(float(print_config.wipe_tower_rotation_angle)), + m_extruder_offsets(print_config.extruder_offset.values), + m_priming(priming), + m_tool_changes(tool_changes), + m_final_purge(final_purge), + m_layer_idx(-1), + m_tool_change_idx(0) + {} + + std::string prime(GCodeGenerator &gcodegen); + void next_layer() { ++ m_layer_idx; m_tool_change_idx = 0; } + std::string tool_change(GCodeGenerator &gcodegen, int extruder_id, bool finish_layer); + std::string finalize(GCodeGenerator &gcodegen); + std::vector used_filament_length() const; + +private: + WipeTowerIntegration& operator=(const WipeTowerIntegration&); + std::string append_tcr(GCodeGenerator &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; + + // Postprocesses gcode: rotates and moves G1 extrusions and returns result + std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const; + + // Left / right edges of the wipe tower, for the planning of wipe moves. + const float m_left; + const float m_right; + const Vec2f m_wipe_tower_pos; + const float m_wipe_tower_rotation; + const std::vector m_extruder_offsets; + + // Reference to cached values at the Printer class. + const std::vector &m_priming; + const std::vector> &m_tool_changes; + const WipeTower::ToolChangeResult &m_final_purge; + // Current layer index. + int m_layer_idx; + int m_tool_change_idx; + double m_last_wipe_tower_print_z = 0.f; +}; + +} // namespace GCode +} // namespace Slic3r + +#endif // slic3r_GCode_WipeTowerIntegration_hpp_ diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index cb03667..5215f41 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -195,10 +195,11 @@ bool GCodeReader::parse_file(const std::string &file, callback_t callback) return this->parse_file_internal(file, callback, [](size_t){}); } -bool GCodeReader::parse_file(const std::string &file, callback_t callback, std::vector &lines_ends) +bool GCodeReader::parse_file(const std::string& file, callback_t callback, std::vector>& lines_ends) { lines_ends.clear(); - return this->parse_file_internal(file, callback, [&lines_ends](size_t file_pos){ lines_ends.emplace_back(file_pos); }); + lines_ends.push_back(std::vector()); + return this->parse_file_internal(file, callback, [&lines_ends](size_t file_pos) { lines_ends.front().emplace_back(file_pos); }); } bool GCodeReader::parse_file_raw(const std::string &filename, raw_line_callback_t line_callback) @@ -232,33 +233,41 @@ const char* GCodeReader::axis_pos(const char *raw_str, char axis) bool GCodeReader::GCodeLine::has(char axis) const { - const char *c = axis_pos(m_raw.c_str(), axis); - return c != nullptr; + return GCodeReader::axis_pos(this->raw().c_str(), axis); } -bool GCodeReader::GCodeLine::has_value(char axis, float &value) const +std::string_view GCodeReader::GCodeLine::axis_pos(char axis) const { - assert(is_decimal_separator_point()); - const char *c = axis_pos(m_raw.c_str(), axis); - if (c == nullptr) - return false; + const std::string &s = this->raw(); + const char *c = GCodeReader::axis_pos(this->raw().c_str(), axis); + return c ? std::string_view{ c, s.size() - (c - s.data()) } : std::string_view(); +} + +bool GCodeReader::GCodeLine::has_value(std::string_view axis_pos, float &value) +{ + if (const char *c = axis_pos.data(); c) { // Try to parse the numeric value. double v = 0.; - const char* end = m_raw.c_str() + m_raw.size(); + const char *end = axis_pos.data() + axis_pos.size(); auto [pend, ec] = fast_float::from_chars(++c, end, v); if (pend != c && is_end_of_word(*pend)) { // The axis value has been parsed correctly. value = float(v); return true; } + } return false; } -bool GCodeReader::GCodeLine::has_value(char axis, int &value) const +bool GCodeReader::GCodeLine::has_value(char axis, float &value) const { - const char *c = axis_pos(m_raw.c_str(), axis); - if (c == nullptr) - return false; + assert(is_decimal_separator_point()); + return this->has_value(this->axis_pos(axis), value); +} + +bool GCodeReader::GCodeLine::has_value(std::string_view axis_pos, int &value) +{ + if (const char *c = axis_pos.data(); c) { // Try to parse the numeric value. char *pend = nullptr; long v = strtol(++ c, &pend, 10); @@ -267,9 +276,14 @@ bool GCodeReader::GCodeLine::has_value(char axis, int &value) const value = int(v); return true; } + } return false; } +bool GCodeReader::GCodeLine::has_value(char axis, int &value) const +{ + return this->has_value(this->axis_pos(axis), value); +} void GCodeReader::GCodeLine::set(const GCodeReader &reader, const Axis axis, const float new_value, const int decimal_digits) { std::ostringstream ss; diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index 58f55fd..feb8723 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -26,11 +26,16 @@ public: const std::string_view comment() const { size_t pos = m_raw.find(';'); return (pos == std::string::npos) ? std::string_view() : std::string_view(m_raw).substr(pos + 1); } + // Return position in this->raw() string starting with the "axis" character. + std::string_view axis_pos(char axis) const; bool has(Axis axis) const { return (m_mask & (1 << int(axis))) != 0; } float value(Axis axis) const { return m_axis[axis]; } bool has(char axis) const; bool has_value(char axis, float &value) const; bool has_value(char axis, int &value) const; + // Parse value of an axis from raw string starting at axis_pos. + static bool has_value(std::string_view axis_pos, float &value); + static bool has_value(std::string_view axis_pos, int &value); float new_X(const GCodeReader &reader) const { return this->has(X) ? this->x() : reader.x(); } float new_Y(const GCodeReader &reader) const { return this->has(Y) ? this->y() : reader.y(); } float new_Z(const GCodeReader &reader) const { return this->has(Z) ? this->z() : reader.z(); } @@ -131,7 +136,7 @@ public: bool parse_file(const std::string &file, callback_t callback); // Collect positions of line ends in the binary G-code to be used by the G-code viewer when memory mapping and displaying section of G-code // as an overlay in the 3D scene. - bool parse_file(const std::string &file, callback_t callback, std::vector &lines_ends); + bool parse_file(const std::string& file, callback_t callback, std::vector>& lines_ends); // Just read the G-code file line by line, calls callback (const char *begin, const char *end). Returns false if reading the file failed. bool parse_file_raw(const std::string &file, raw_line_callback_t callback); diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 9d04bb7..8e29b0a 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -301,6 +301,13 @@ template T angle_to_0_2PI(T angle) return angle; } +template void to_range_pi_pi(T &angle){ + if (angle > T(PI) || angle <= -T(PI)) { + int count = static_cast(std::round(angle / (2 * PI))); + angle -= static_cast(count * 2 * PI); + assert(angle <= T(PI) && angle > -T(PI)); + } +} void simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval); diff --git a/src/libslic3r/Geometry/ArcWelder.cpp b/src/libslic3r/Geometry/ArcWelder.cpp new file mode 100644 index 0000000..4b02a31 --- /dev/null +++ b/src/libslic3r/Geometry/ArcWelder.cpp @@ -0,0 +1,933 @@ +// The following code for merging circles into arches originates from https://github.com/FormerLurker/ArcWelderLib + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Arc Welder: Anti-Stutter Library +// +// Compresses many G0/G1 commands into G2/G3(arc) commands where possible, ensuring the tool paths stay within the specified resolution. +// This reduces file size and the number of gcodes per second. +// +// Uses the 'Gcode Processor Library' for gcode parsing, position processing, logging, and other various functionality. +// +// Copyright(C) 2021 - Brad Hochgesang +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// This program is free software : you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the +// GNU Affero General Public License for more details. +// +// +// You can contact the author at the following email address: +// FormerLurker@pm.me +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "ArcWelder.hpp" +#include "Circle.hpp" + +#include "../MultiPoint.hpp" +#include "../Polygon.hpp" + +#include +#include +#include +#include + +namespace Slic3r { namespace Geometry { namespace ArcWelder { + +Points arc_discretize(const Point &p1, const Point &p2, const double radius, const bool ccw, const double deviation) +{ + Vec2d center = arc_center(p1.cast(), p2.cast(), radius, ccw); + double angle = arc_angle(p1.cast(), p2.cast(), radius); + assert(angle > 0); + + double r = std::abs(radius); + size_t num_steps = arc_discretization_steps(r, angle, deviation); + double angle_step = angle / num_steps; + + Points out; + out.reserve(num_steps + 1); + out.emplace_back(p1); + if (! ccw) + angle_step *= -1.; + for (size_t i = 1; i < num_steps; ++ i) + out.emplace_back(p1.rotated(angle_step * i, center.cast())); + out.emplace_back(p2); + return out; +} + +struct Circle +{ + Point center; + double radius; +}; + +// Interpolate three points with a circle. +// Returns false if the three points are collinear or if the radius is bigger than maximum allowed radius. +static std::optional try_create_circle(const Point &p1, const Point &p2, const Point &p3, const double max_radius) +{ + if (auto center = Slic3r::Geometry::try_circle_center(p1.cast(), p2.cast(), p3.cast(), SCALED_EPSILON); center) { + Point c = center->cast(); + if (double r = sqrt(double((c - p1).cast().squaredNorm())); r <= max_radius) + return std::make_optional({ c, float(r) }); + } + return {}; +} + +// Returns a closest point on the segment. +// Returns false if the closest point is not inside the segment, but at its boundary. +static bool foot_pt_on_segment(const Point &p1, const Point &p2, const Point &pt, Point &out) +{ + Vec2i64 v21 = (p2 - p1).cast(); + int64_t l2 = v21.squaredNorm(); + if (l2 > int64_t(SCALED_EPSILON)) { + if (int64_t t = (pt - p1).cast().dot(v21); + t >= int64_t(SCALED_EPSILON) && t < l2 - int64_t(SCALED_EPSILON)) { + out = p1 + ((double(t) / double(l2)) * v21.cast()).cast(); + return true; + } + } + // The segment is short or the closest point is an end point. + return false; +} + +static inline bool circle_approximation_sufficient(const Circle &circle, const Points::const_iterator begin, const Points::const_iterator end, const double tolerance) +{ + // The circle was calculated from the 1st and last point of the point sequence, thus the fitting of those points does not need to be evaluated. + assert(std::abs((*begin - circle.center).cast().norm() - circle.radius) < SCALED_EPSILON); + assert(std::abs((*std::prev(end) - circle.center).cast().norm() - circle.radius) < SCALED_EPSILON); + assert(end - begin >= 3); + + // Test the 1st point. + if (double distance_from_center = (*begin - circle.center).cast().norm(); + std::abs(distance_from_center - circle.radius) > tolerance) + return false; + + for (auto it = std::next(begin); it != end; ++ it) { + if (double distance_from_center = (*it - circle.center).cast().norm(); + std::abs(distance_from_center - circle.radius) > tolerance) + return false; + Point closest_point; + if (foot_pt_on_segment(*std::prev(it), *it, circle.center, closest_point)) { + if (double distance_from_center = (closest_point - circle.center).cast().norm(); + std::abs(distance_from_center - circle.radius) > tolerance) + return false; + } + } + return true; +} + +static inline bool get_deviation_sum_squared(const Circle &circle, const Points::const_iterator begin, const Points::const_iterator end, const double tolerance, double &total_deviation) +{ + // The circle was calculated from the 1st and last point of the point sequence, thus the fitting of those points does not need to be evaluated. + assert(std::abs((*begin - circle.center).cast().norm() - circle.radius) < SCALED_EPSILON); + assert(std::abs((*std::prev(end) - circle.center).cast().norm() - circle.radius) < SCALED_EPSILON); + assert(end - begin >= 3); + + total_deviation = 0; + + const double tolerance2 = sqr(tolerance); + for (auto it = std::next(begin); std::next(it) != end; ++ it) + if (double deviation2 = sqr((*it - circle.center).cast().norm() - circle.radius); deviation2 > tolerance2) + return false; + else + total_deviation += deviation2; + + for (auto it = begin; std::next(it) != end; ++ it) { + Point closest_point; + if (foot_pt_on_segment(*it, *std::next(it), circle.center, closest_point)) { + if (double deviation2 = sqr((closest_point - circle.center).cast().norm() - circle.radius); deviation2 > tolerance2) + return false; + else + total_deviation += deviation2; + } + } + + return true; +} + +double arc_fit_variance(const Point &start_pos, const Point &end_pos, const float radius, bool is_ccw, + const Points::const_iterator begin, const Points::const_iterator end) +{ + const Vec2d center = arc_center(start_pos.cast(), end_pos.cast(), double(radius), is_ccw); + const double r = std::abs(radius); + + // The circle was calculated from the 1st and last point of the point sequence, thus the fitting of those points does not need to be evaluated. + assert(std::abs((begin->cast() - center).norm() - r) < SCALED_EPSILON); + assert(std::abs((std::prev(end)->cast() - center).norm() - r) < SCALED_EPSILON); + assert(end - begin >= 3); + + double total_deviation = 0; + size_t cnt = 0; + for (auto it = begin; std::next(it) != end; ++ it) { + if (it != begin) { + total_deviation += sqr((it->cast() - center).norm() - r); + ++ cnt; + } + Point closest_point; + if (foot_pt_on_segment(*it, *std::next(it), center.cast(), closest_point)) { + total_deviation += sqr((closest_point.cast() - center).cast().norm() - r); + ++ cnt; + } + } + + return total_deviation / double(cnt); +} + +double arc_fit_max_deviation(const Point &start_pos, const Point &end_pos, const float radius, bool is_ccw, + const Points::const_iterator begin, const Points::const_iterator end) +{ + const Vec2d center = arc_center(start_pos.cast(), end_pos.cast(), double(radius), is_ccw); + const double r = std::abs(radius); + + // The circle was calculated from the 1st and last point of the point sequence, thus the fitting of those points does not need to be evaluated. + assert(std::abs((begin->cast() - center).norm() - r) < SCALED_EPSILON); + assert(std::abs((std::prev(end)->cast() - center).norm() - r) < SCALED_EPSILON); + assert(end - begin >= 3); + + double max_deviation = 0; + double max_signed_deviation = 0; + for (auto it = begin; std::next(it) != end; ++ it) { + if (it != begin) { + double signed_deviation = (it->cast() - center).norm() - r; + double deviation = std::abs(signed_deviation); + if (deviation > max_deviation) { + max_deviation = deviation; + max_signed_deviation = signed_deviation; + } + } + Point closest_point; + if (foot_pt_on_segment(*it, *std::next(it), center.cast(), closest_point)) { + double signed_deviation = (closest_point.cast() - center).cast().norm() - r; + double deviation = std::abs(signed_deviation); + if (deviation > max_deviation) { + max_deviation = deviation; + max_signed_deviation = signed_deviation; + } + } + } + return max_signed_deviation; +} + +static inline int sign(const int64_t i) +{ + return i > 0 ? 1 : i < 0 ? -1 : 0; +} + +static std::optional try_create_circle(const Points::const_iterator begin, const Points::const_iterator end, const double max_radius, const double tolerance) +{ + std::optional out; + size_t size = end - begin; + if (size == 3) { + // Fit the circle throuh the three input points. + out = try_create_circle(*begin, *std::next(begin), *std::prev(end), max_radius); + if (out) { + // Fit the center point and the two center points of the two edges with non-linear least squares. + std::array fpts; + Vec2d center_point = out->center.cast(); + Vec2d first_point = begin->cast(); + Vec2d mid_point = std::next(begin)->cast(); + Vec2d last_point = std::prev(end)->cast(); + fpts[0] = 0.5 * (first_point + mid_point); + fpts[1] = mid_point; + fpts[2] = 0.5 * (mid_point + last_point); + const double radius = (first_point - center_point).norm(); + if (std::abs((fpts[0] - center_point).norm() - radius) < 2. * tolerance && + std::abs((fpts[2] - center_point).norm() - radius) < 2. * tolerance) { + if (std::optional opt_center = ArcWelder::arc_fit_center_gauss_newton_ls(first_point, last_point, + center_point, fpts.begin(), fpts.end(), 3); + opt_center) { + out->center = opt_center->cast(); + out->radius = (out->radius > 0 ? 1.f : -1.f) * (*opt_center - first_point).norm(); + } + if (! circle_approximation_sufficient(*out, begin, end, tolerance)) + out.reset(); + } else + out.reset(); + } + } else { + std::optional circle; + { + // Try to fit a circle to first, middle and last point. + auto mid = begin + (end - begin) / 2; + circle = try_create_circle(*begin, *mid, *std::prev(end), max_radius); + if (// Use twice the tolerance for fitting the initial circle. + // Early exit if such approximation is grossly inaccurate, thus the tolerance could not be achieved. + circle && ! circle_approximation_sufficient(*circle, begin, end, tolerance * 2)) + circle.reset(); + } + if (! circle) { + // Find an intersection point of the polyline to be fitted with the bisector of the arc chord. + // At such a point the distance of a polyline to an arc wrt. the circle center (or circle radius) will have a largest gradient + // of all points on the polyline to be fitted. + Vec2i64 first_point = begin->cast(); + Vec2i64 last_point = std::prev(end)->cast(); + Vec2i64 v = last_point - first_point; + Vec2d vd = v.cast(); + double ld = v.squaredNorm(); + if (ld > sqr(scaled(0.0015))) { + Vec2i64 c = (first_point.cast() + last_point.cast()) / 2; + Vec2i64 prev_point = first_point; + int prev_side = sign(v.dot(prev_point - c)); + assert(prev_side != 0); + Point point_on_bisector; + #ifndef NDEBUG + point_on_bisector = { std::numeric_limits::max(), std::numeric_limits::max() }; + #endif // NDEBUG + for (auto it = std::next(begin); it != end; ++ it) { + Vec2i64 this_point = it->cast(); + int64_t d = v.dot(this_point - c); + int this_side = sign(d); + int sideness = this_side * prev_side; + if (sideness < 0) { + // Calculate the intersection point. + Vec2d p = c.cast() + vd * double(d) / ld; + point_on_bisector = p.cast(); + break; + } + if (sideness == 0) { + // this_point is on the bisector. + assert(prev_side != 0); + assert(this_side == 0); + point_on_bisector = this_point.cast(); + break; + } + prev_point = this_point; + prev_side = this_side; + } + // point_on_bisector must be set + assert(point_on_bisector.x() != std::numeric_limits::max() && point_on_bisector.y() != std::numeric_limits::max()); + circle = try_create_circle(*begin, point_on_bisector, *std::prev(end), max_radius); + if (// Use twice the tolerance for fitting the initial circle. + // Early exit if such approximation is grossly inaccurate, thus the tolerance could not be achieved. + circle && ! circle_approximation_sufficient(*circle, begin, end, tolerance * 2)) + circle.reset(); + } + } + if (circle) { + // Fit the arc between the end points by least squares. + // Optimize over all points along the path and the centers of the segments. + boost::container::small_vector fpts; + Vec2d first_point = begin->cast(); + Vec2d last_point = std::prev(end)->cast(); + Vec2d prev_point = first_point; + for (auto it = std::next(begin); it != std::prev(end); ++ it) { + Vec2d this_point = it->cast(); + fpts.emplace_back(0.5 * (prev_point + this_point)); + fpts.emplace_back(this_point); + prev_point = this_point; + } + fpts.emplace_back(0.5 * (prev_point + last_point)); + std::optional opt_center = ArcWelder::arc_fit_center_gauss_newton_ls(first_point, last_point, + circle->center.cast(), fpts.begin(), fpts.end(), 5); + if (opt_center) { + // Fitted radius must not be excessively large. If so, it is better to fit with a line segment. + if (const double r2 = (*opt_center - first_point).squaredNorm(); r2 < max_radius * max_radius) { + circle->center = opt_center->cast(); + circle->radius = (circle->radius > 0 ? 1.f : -1.f) * sqrt(r2); + if (circle_approximation_sufficient(*circle, begin, end, tolerance)) { + out = circle; + } else { + //FIXME One may consider adjusting the arc to fit the worst offender as a last effort, + // however Vojtech is not sure whether it is worth it. + } + } + } + } +/* + // From the original arc welder. + // Such a loop makes the time complexity of the arc fitting an ugly O(n^3). + else { + // Find the circle with the least deviation, if one exists. + double least_deviation = std::numeric_limits::max(); + double current_deviation; + for (auto it = std::next(begin); std::next(it) != end; ++ it) + if (std::optional circle = try_create_circle(*begin, *it, *std::prev(end), max_radius); + circle && get_deviation_sum_squared(*circle, begin, end, tolerance, current_deviation)) { + if (current_deviation < least_deviation) { + out = circle; + least_deviation = current_deviation; + } + } + } +*/ + } + return out; +} + +// ported from ArcWelderLib/ArcWelder/segmented/shape.h class "arc" +class Arc { +public: + Point start_point; + Point end_point; + Point center; + double radius; + Orientation direction { Orientation::Unknown }; +}; + +// Return orientation of a polyline with regard to the center. +// Successive points are expected to take less than a PI angle step. +Orientation arc_orientation( + const Point ¢er, + const Points::const_iterator begin, + const Points::const_iterator end) +{ + assert(end - begin >= 3); + // Assumption: Two successive points of a single segment span an angle smaller than PI. + Vec2i64 vstart = (*begin - center).cast(); + Vec2i64 vprev = vstart; + int arc_dir = 0; + for (auto it = std::next(begin); it != end; ++ it) { + Vec2i64 v = (*it - center).cast(); + int dir = sign(cross2(vprev, v)); + if (dir == 0) { + // Ignore radial segments. + } else if (arc_dir * dir < 0) { + // The path turns back and overextrudes. Such path is likely invalid, but the arc interpolation should + // rather maintain such an invalid path instead of covering it up. + // Don't replace such a path with an arc. + return {}; + } else { + // Success, either establishing the direction for the first time, or moving in the same direction as the last time. + arc_dir = dir; + vprev = v; + } + } + return arc_dir == 0 ? + // All points are radial wrt. the center, this is unexpected. + Orientation::Unknown : + // Arc is valid, either CCW or CW. + arc_dir > 0 ? Orientation::CCW : Orientation::CW; +} + +static inline std::optional try_create_arc_impl( + const Circle &circle, + const Points::const_iterator begin, + const Points::const_iterator end, + const double tolerance, + const double path_tolerance_percent) +{ + assert(end - begin >= 3); + // Assumption: Two successive points of a single segment span an angle smaller than PI. + Orientation orientation = arc_orientation(circle.center, begin, end); + if (orientation == Orientation::Unknown) + return {}; + + Vec2i64 vstart = (*begin - circle.center).cast(); + Vec2i64 vend = (*std::prev(end) - circle.center).cast(); + double angle = atan2(double(cross2(vstart, vend)), double(vstart.dot(vend))); + if (orientation == Orientation::CW) + angle *= -1.; + if (angle < 0) + angle += 2. * M_PI; + assert(angle >= 0. && angle < 2. * M_PI + EPSILON); + + // Check the length against the original length. + // This can trigger simply due to the differing path lengths + // but also could indicate that the vector calculation above + // got wrong direction + const double arc_length = circle.radius * angle; + const double approximate_length = length(begin, end); + assert(approximate_length > 0); + const double arc_length_difference_relative = (arc_length - approximate_length) / approximate_length; + + if (std::fabs(arc_length_difference_relative) >= path_tolerance_percent) { + return {}; + } else { + assert(circle_approximation_sufficient(circle, begin, end, tolerance + SCALED_EPSILON)); + return std::make_optional(Arc{ + *begin, + *std::prev(end), + circle.center, + angle > M_PI ? - circle.radius : circle.radius, + orientation + }); + } +} + +static inline std::optional try_create_arc( + const Points::const_iterator begin, + const Points::const_iterator end, + double max_radius = default_scaled_max_radius, + double tolerance = default_scaled_resolution, + double path_tolerance_percent = default_arc_length_percent_tolerance) +{ + std::optional circle = try_create_circle(begin, end, max_radius, tolerance); + if (! circle) + return {}; + return try_create_arc_impl(*circle, begin, end, tolerance, path_tolerance_percent); +} + +float arc_angle(const Vec2f &start_pos, const Vec2f &end_pos, Vec2f ¢er_pos, bool is_ccw) +{ + if ((end_pos - start_pos).squaredNorm() < sqr(1e-6)) { + // If start equals end, full circle is considered. + return float(2. * M_PI); + } else { + Vec2f v1 = start_pos - center_pos; + Vec2f v2 = end_pos - center_pos; + if (! is_ccw) + std::swap(v1, v2); + float radian = atan2(cross2(v1, v2), v1.dot(v2)); + return radian < 0 ? float(2. * M_PI) + radian : radian; + } +} + +float arc_length(const Vec2f &start_pos, const Vec2f &end_pos, Vec2f ¢er_pos, bool is_ccw) +{ + return (center_pos - start_pos).norm() * arc_angle(start_pos, end_pos, center_pos, is_ccw); +} + +// Reduces polyline in the (begin, end, begin, tolerance, [](const Segment &s) { return s.point; }); +} + +Path fit_path(const Points &src_in, double tolerance, double fit_circle_percent_tolerance) +{ + assert(tolerance >= 0); + assert(fit_circle_percent_tolerance >= 0); + double tolerance2 = Slic3r::sqr(tolerance); + + Path out; + out.reserve(src_in.size()); + if (tolerance <= 0 || src_in.size() <= 2) { + // No simplification, just convert. + std::transform(src_in.begin(), src_in.end(), std::back_inserter(out), [](const Point &p) -> Segment { return { p }; }); + } else if (double tolerance_fine = std::max(0.03 * tolerance, scaled(0.000060)); + fit_circle_percent_tolerance <= 0 || tolerance_fine > 0.5 * tolerance) { + // Convert and simplify to a polyline. + std::transform(src_in.begin(), src_in.end(), std::back_inserter(out), [](const Point &p) -> Segment { return { p }; }); + out.erase(douglas_peucker_in_place(out.begin(), out.end(), tolerance), out.end()); + } else { + // Simplify the polyline first using a fine threshold. + Points src = douglas_peucker(src_in, tolerance_fine); + // Perform simplification & fitting. + // Index of the start of a last polyline, which has not yet been decimated. + int begin_pl_idx = 0; + out.push_back({ src.front(), 0.f }); + for (auto it = std::next(src.begin()); it != src.end();) { + // Minimum 2 additional points required for circle fitting. + auto begin = std::prev(it); + auto end = std::next(it); + assert(end <= src.end()); + std::optional arc; + while (end != src.end()) { + auto next_end = std::next(end); + if (std::optional this_arc = try_create_arc( + begin, next_end, + ArcWelder::default_scaled_max_radius, + tolerance, fit_circle_percent_tolerance); + this_arc) { + // Could extend the arc by one point. + assert(this_arc->direction != Orientation::Unknown); + arc = this_arc; + end = next_end; + if (end == src.end()) + // No way to extend the arc. + goto fit_end; + // Now try to expand the arc by adding points one by one. That should be cheaper than a full arc fit test. + while (std::next(end) != src.end()) { + assert(end == next_end); + { + Vec2i64 v1 = arc->start_point.cast() - arc->center.cast(); + Vec2i64 v2 = arc->end_point.cast() - arc->center.cast(); + do { + if (std::abs((arc->center.cast() - next_end->cast()).norm() - arc->radius) >= tolerance || + inside_arc_wedge_vectors(v1, v2, + arc->radius > 0, arc->direction == Orientation::CCW, + next_end->cast() - arc->center.cast())) + // Cannot extend the current arc with this new point. + break; + } while (++ next_end != src.end()); + } + if (next_end == end) + // No additional point could be added to a current arc. + break; + // Try to fit a new arc to the extended set of points. + // last_tested_failed set to invalid value, no test failed yet. + auto last_tested_failed = src.begin(); + for (;;) { + this_arc = try_create_arc( + begin, next_end, + ArcWelder::default_scaled_max_radius, + tolerance, fit_circle_percent_tolerance); + if (this_arc) { + arc = this_arc; + end = next_end; + if (last_tested_failed == src.begin()) { + // First run of the loop, the arc was extended fully. + if (end == src.end()) + goto fit_end; + // Otherwise try to extend the arc with another sample. + break; + } + } else + last_tested_failed = next_end; + // Take half of the interval up to the failed point. + next_end = end + (last_tested_failed - end) / 2; + if (next_end == end) + // Backed to the last successfull sample. + goto fit_end; + // Otherwise try to extend the arc up to next_end in another iteration. + } + } + } else { + // The last arc was the best we could get. + break; + } + } + fit_end: +#if 1 + if (arc) { + // Check whether the arc end points are not too close with the risk of quantizing the arc ends to the same point on G-code export. + if ((arc->end_point - arc->start_point).cast().squaredNorm() < 2. * sqr(scaled(0.0011))) { + // Arc is too short. Skip it, decimate a polyline instead. + arc.reset(); + } else { + // Test whether the arc is so flat, that it could be replaced with a straight segment. + Line line(arc->start_point, arc->end_point); + bool arc_valid = false; + for (auto it2 = std::next(begin); it2 != std::prev(end); ++ it2) + if (line_alg::distance_to_squared(line, *it2) > tolerance2) { + // Polyline could not be fitted by a line segment, thus the arc is considered valid. + arc_valid = true; + break; + } + if (! arc_valid) + // Arc should be fitted by a line segment. Skip it, decimate a polyline instead. + arc.reset(); + } + } +#endif + if (arc) { + // printf("Arc radius: %lf, length: %lf\n", unscaled(arc->radius), arc_length(arc->start_point.cast(), arc->end_point.cast(), arc->radius)); + // If there is a trailing polyline, decimate it first before saving a new arc. + if (out.size() - begin_pl_idx > 2) { + // Decimating linear segmens only. + assert(std::all_of(out.begin() + begin_pl_idx + 1, out.end(), [](const Segment &seg) { return seg.linear(); })); + out.erase(douglas_peucker_in_place(out.begin() + begin_pl_idx, out.end(), tolerance), out.end()); + assert(out.back().linear()); + } +#ifndef NDEBUG + // Check for a very short linear segment, that connects two arches. Such segment should not be created. + if (out.size() - begin_pl_idx > 1) { + const Point& p1 = out[begin_pl_idx].point; + const Point& p2 = out.back().point; + assert((p2 - p1).cast().squaredNorm() > sqr(scaled(0.0011))); + } +#endif + // Save the index of an end of the new circle segment, which may become the first point of a possible future polyline. + begin_pl_idx = int(out.size()); + // This will be the next point to try to add. + it = end; + // Add the new arc. + assert(*begin == arc->start_point); + assert(*std::prev(it) == arc->end_point); + assert(out.back().point == arc->start_point); + out.push_back({ arc->end_point, float(arc->radius), arc->direction }); +#if 0 + // Verify that all the source points are at tolerance distance from the interpolated path. + { + const Segment &seg_start = *std::prev(std::prev(out.end())); + const Segment &seg_end = out.back(); + const Vec2d center = arc_center(seg_start.point.cast(), seg_end.point.cast(), double(seg_end.radius), seg_end.ccw()); + assert(seg_start.point == *begin); + assert(seg_end.point == *std::prev(end)); + assert(arc_orientation(center.cast(), begin, end) == arc->direction); + for (auto it = std::next(begin); it != end; ++ it) { + Point ptstart = *std::prev(it); + Point ptend = *it; + Point closest_point; + if (foot_pt_on_segment(ptstart, ptend, center.cast(), closest_point)) { + double distance_from_center = (closest_point.cast() - center).norm(); + assert(std::abs(distance_from_center - std::abs(seg_end.radius)) < tolerance + SCALED_EPSILON); + } + Vec2d v = (ptend - ptstart).cast(); + double len = v.norm(); + auto num_segments = std::min(10, ceil(2. * len / fit_circle_percent_tolerance)); + for (size_t i = 0; i < num_segments; ++ i) { + Point p = ptstart + (v * (double(i) / double(num_segments))).cast(); + assert(i == 0 || inside_arc_wedge(seg_start.point.cast(), seg_end.point.cast(), center, seg_end.radius > 0, seg_end.ccw(), p.cast())); + double d2 = sqr((p.cast() - center).norm() - std::abs(seg_end.radius)); + assert(d2 < sqr(tolerance + SCALED_EPSILON)); + } + } + } +#endif + } else { + // Arc is not valid, append a linear segment. + out.push_back({ *it ++ }); + } + } + if (out.size() - begin_pl_idx > 2) + // Do the final polyline decimation. + out.erase(douglas_peucker_in_place(out.begin() + begin_pl_idx, out.end(), tolerance), out.end()); + } + +#if 0 + // Verify that all the source points are at tolerance distance from the interpolated path. + for (auto it = std::next(src_in.begin()); it != src_in.end(); ++ it) { + Point start = *std::prev(it); + Point end = *it; + Vec2d v = (end - start).cast(); + double len = v.norm(); + auto num_segments = std::min(10, ceil(2. * len / fit_circle_percent_tolerance)); + for (size_t i = 0; i <= num_segments; ++ i) { + Point p = start + (v * (double(i) / double(num_segments))).cast(); + PathSegmentProjection proj = point_to_path_projection(out, p); + assert(proj.valid()); + assert(proj.distance2 < sqr(tolerance + SCALED_EPSILON)); + } + } +#endif + + return out; +} + +void reverse(Path &path) +{ + if (path.size() > 1) { + auto prev = path.begin(); + for (auto it = std::next(prev); it != path.end(); ++ it) { + prev->radius = it->radius; + prev->orientation = it->orientation == Orientation::CCW ? Orientation::CW : Orientation::CCW; + prev = it; + } + path.back().radius = 0; + std::reverse(path.begin(), path.end()); + } +} + +double clip_start(Path &path, const double len) +{ + reverse(path); + double remaining = clip_end(path, len); + reverse(path); + // Return remaining distance to go. + return remaining; +} + +double clip_end(Path &path, double distance) +{ + while (distance > 0) { + const Segment last = path.back(); + path.pop_back(); + if (path.empty()) + break; + if (last.linear()) { + // Linear segment + Vec2d v = (path.back().point - last.point).cast(); + double lsqr = v.squaredNorm(); + if (lsqr > sqr(distance)) { + path.push_back({ last.point + (v * (distance / sqrt(lsqr))).cast() }); + // Length to go is zero. + return 0; + } + distance -= sqrt(lsqr); + } else { + // Circular segment + double angle = arc_angle(path.back().point.cast(), last.point.cast(), last.radius); + double len = std::abs(last.radius) * angle; + if (len > distance) { + // Rotate the segment end point in reverse towards the start point. + if (last.ccw()) + angle *= -1.; + path.push_back({ + last.point.rotated(angle * (distance / len), + arc_center(path.back().point.cast(), last.point.cast(), double(last.radius), last.ccw()).cast()), + last.radius, last.orientation }); + // Length to go is zero. + return 0; + } + distance -= len; + } + } + + // Return remaining distance to go. + assert(distance >= 0); + return distance; +} + +PathSegmentProjection point_to_path_projection(const Path &path, const Point &point, double search_radius2) +{ + assert(path.size() != 1); + // initialized to "invalid" state. + PathSegmentProjection out; + out.distance2 = search_radius2; + if (path.size() < 2 || path.front().point == point) { + // First point is the closest point. + if (path.empty()) { + // No closest point available. + } else if (const Point p0 = path.front().point; p0 == point) { + out.segment_id = 0; + out.point = p0; + out.distance2 = 0; + } else if (double d2 = (p0 - point).cast().squaredNorm(); d2 < out.distance2) { + out.segment_id = 0; + out.point = p0; + out.distance2 = d2; + } + } else { + assert(path.size() >= 2); + // min_point_it will contain an end point of a segment with a closest projection found + // or path.cbegin() if no such closest projection closer than search_radius2 was found. + auto min_point_it = path.cbegin(); + Point prev = path.front().point; + for (auto it = std::next(path.cbegin()); it != path.cend(); ++ it) { + if (it->linear()) { + // Linear segment + Point proj; + // distance_to_squared() will possibly return the start or end point of a line segment. + if (double d2 = line_alg::distance_to_squared(Line(prev, it->point), point, &proj); d2 < out.distance2) { + out.point = proj; + out.distance2 = d2; + min_point_it = it; + } + } else { + // Circular arc + Vec2i64 center = arc_center(prev.cast(), it->point.cast(), double(it->radius), it->ccw()).cast(); + // Test whether point is inside the wedge. + Vec2i64 v1 = prev.cast() - center; + Vec2i64 v2 = it->point.cast() - center; + Vec2i64 vp = point.cast() - center; + if (inside_arc_wedge_vectors(v1, v2, it->radius > 0, it->ccw(), vp)) { + // Distance of the radii. + const auto r = double(std::abs(it->radius)); + const auto rtest = sqrt(double(vp.squaredNorm())); + if (double d2 = sqr(rtest - r); d2 < out.distance2) { + if (rtest > SCALED_EPSILON) + // Project vp to the arc. + out.point = center.cast() + (vp.cast() * (r / rtest)).cast(); + else + // Test point is very close to the center of the radius. Any point of the arc is the closest. + // Pick the start. + out.point = prev; + out.distance2 = d2; + out.center = center.cast(); + min_point_it = it; + } + } else { + // Distance to the start point. + if (double d2 = double((v1 - vp).squaredNorm()); d2 < out.distance2) { + out.point = prev; + out.distance2 = d2; + min_point_it = it; + } + } + } + prev = it->point; + } + if (! path.back().linear()) { + // Calculate distance to the end point. + if (double d2 = (path.back().point - point).cast().squaredNorm(); d2 < out.distance2) { + out.point = path.back().point; + out.distance2 = d2; + min_point_it = std::prev(path.end()); + } + } + // If a new closes point was found, it is closer than search_radius2. + assert((min_point_it == path.cbegin()) == (out.distance2 == search_radius2)); + // Output is not valid yet. + assert(! out.valid()); + if (min_point_it != path.cbegin()) { + // Make it valid by setting the segment. + out.segment_id = std::prev(min_point_it) - path.begin(); + assert(out.valid()); + } + } + + assert(! out.valid() || (out.segment_id >= 0 && out.segment_id < path.size())); + return out; +} + +std::pair split_at(const Path &path, const PathSegmentProjection &proj, const double min_segment_length) +{ + assert(proj.valid()); + assert(! proj.valid() || (proj.segment_id >= 0 && proj.segment_id < path.size())); + assert(path.size() > 1); + std::pair out; + if (! proj.valid() || proj.segment_id + 1 == path.size() || (proj.segment_id + 2 == path.size() && proj.point == path.back().point)) + out.first = path; + else if (proj.segment_id == 0 && proj.point == path.front().point) + out.second = path; + else { + // Path will likely be split to two pieces. + assert(proj.valid() && proj.segment_id >= 0 && proj.segment_id + 1 < path.size()); + const Segment &start = path[proj.segment_id]; + const Segment &end = path[proj.segment_id + 1]; + bool split_segment = true; + int split_segment_id = proj.segment_id; + if (int64_t d2 = (proj.point - start.point).cast().squaredNorm(); d2 < sqr(min_segment_length)) { + split_segment = false; + int64_t d22 = (proj.point - end.point).cast().squaredNorm(); + if (d22 < d2) + // Split at the end of the segment. + ++ split_segment_id; + } else if (int64_t d2 = (proj.point - end.point).cast().squaredNorm(); d2 < sqr(min_segment_length)) { + ++ split_segment_id; + split_segment = false; + } + if (split_segment) { + out.first.assign(path.begin(), path.begin() + split_segment_id + 2); + out.second.assign(path.begin() + split_segment_id, path.end()); + assert(out.first[out.first.size() - 2] == start); + assert(out.first.back() == end); + assert(out.second.front() == start); + assert(out.second[1] == end); + assert(out.first.size() + out.second.size() == path.size() + 2); + assert(out.first.back().radius == out.second[1].radius); + out.first.back().point = proj.point; + out.second.front().point = proj.point; + if (end.radius < 0) { + // A large arc (> PI) was split. + // At least one of the two arches that were created by splitting the original arch will become smaller. + // Make the radii of those arches that became < PI positive. + // In case of a projection onto an arc, proj.center should be filled in and valid. + auto vstart = (start.point - proj.center).cast(); + auto vend = (end.point - proj.center).cast(); + auto vproj = (proj.point - proj.center).cast(); + if ((cross2(vstart, vproj) > 0) == end.ccw()) + // Make the radius of a minor arc positive. + out.first.back().radius *= -1.f; + if ((cross2(vproj, vend) > 0) == end.ccw()) + // Make the radius of a minor arc positive. + out.second[1].radius *= -1.f; + assert(out.first.size() > 1); + assert(out.second.size() > 1); + out.second.front().radius = 0; + } + } else { + assert(split_segment_id >= 0 && split_segment_id < path.size()); + if (split_segment_id + 1 == int(path.size())) + out.first = path; + else if (split_segment_id == 0) + out.second = path; + else { + // Split at the start of proj.segment_id. + out.first.assign(path.begin(), path.begin() + split_segment_id + 1); + out.second.assign(path.begin() + split_segment_id, path.end()); + assert(out.first.size() + out.second.size() == path.size() + 1); + assert(out.first.back() == (split_segment_id == proj.segment_id ? start : end)); + assert(out.second.front() == (split_segment_id == proj.segment_id ? start : end)); + assert(out.first.size() > 1); + assert(out.second.size() > 1); + out.second.front().radius = 0; + } + } + } + + return out; +} + +std::pair split_at(const Path &path, const Point &point, const double min_segment_length) +{ + return split_at(path, point_to_path_projection(path, point), min_segment_length); +} + +} } } // namespace Slic3r::Geometry::ArcWelder diff --git a/src/libslic3r/Geometry/ArcWelder.hpp b/src/libslic3r/Geometry/ArcWelder.hpp new file mode 100644 index 0000000..dceec56 --- /dev/null +++ b/src/libslic3r/Geometry/ArcWelder.hpp @@ -0,0 +1,505 @@ +#ifndef slic3r_Geometry_ArcWelder_hpp_ +#define slic3r_Geometry_ArcWelder_hpp_ + +#include + +#include "../Point.hpp" + +namespace Slic3r { namespace Geometry { namespace ArcWelder { + +// Calculate center point (center of a circle) of an arc given two points and a radius. +// positive radius: take shorter arc +// negative radius: take longer arc +// radius must NOT be zero! +template +inline Eigen::Matrix arc_center( + const Eigen::MatrixBase &start_pos, + const Eigen::MatrixBase &end_pos, + const Float radius, + const bool is_ccw) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "arc_center(): first parameter is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "arc_center(): second parameter is not a 2D vector"); + static_assert(std::is_same::value, "arc_center(): Both vectors must be of the same type."); + static_assert(std::is_same::value, "arc_center(): Radius must be of the same type as the vectors."); + assert(radius != 0); + using Vector = Eigen::Matrix; + auto v = end_pos - start_pos; + Float q2 = v.squaredNorm(); + assert(q2 > 0); + Float t2 = sqr(radius) / q2 - Float(.25f); + // If the start_pos and end_pos are nearly antipodal, t2 may become slightly negative. + // In that case return a centroid of start_point & end_point. + Float t = t2 > 0 ? sqrt(t2) : Float(0); + auto mid = Float(0.5) * (start_pos + end_pos); + Vector vp{ -v.y() * t, v.x() * t }; + return (radius > Float(0)) == is_ccw ? (mid + vp).eval() : (mid - vp).eval(); +} + +// Calculate middle sample point (point on an arc) of an arc given two points and a radius. +// positive radius: take shorter arc +// negative radius: take longer arc +// radius must NOT be zero! +// Taking a sample at the middle of a convex arc (less than PI) is likely much more +// useful than taking a sample at the middle of a concave arc (more than PI). +template +inline Eigen::Matrix arc_middle_point( + const Eigen::MatrixBase &start_pos, + const Eigen::MatrixBase &end_pos, + const Float radius, + const bool is_ccw) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "arc_center(): first parameter is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "arc_center(): second parameter is not a 2D vector"); + static_assert(std::is_same::value, "arc_center(): Both vectors must be of the same type."); + static_assert(std::is_same::value, "arc_center(): Radius must be of the same type as the vectors."); + assert(radius != 0); + using Vector = Eigen::Matrix; + auto v = end_pos - start_pos; + Float q2 = v.squaredNorm(); + assert(q2 > 0); + Float t2 = sqr(radius) / q2 - Float(.25f); + // If the start_pos and end_pos are nearly antipodal, t2 may become slightly negative. + // In that case return a centroid of start_point & end_point. + Float t = (t2 > 0 ? sqrt(t2) : Float(0)) - radius / sqrt(q2); + auto mid = Float(0.5) * (start_pos + end_pos); + Vector vp{ -v.y() * t, v.x() * t }; + return (radius > Float(0)) == is_ccw ? (mid + vp).eval() : (mid - vp).eval(); +} + +// Calculate angle of an arc given two points and a radius. +// Returned angle is in the range <0, 2 PI) +// positive radius: take shorter arc +// negative radius: take longer arc +// radius must NOT be zero! +template +inline typename Derived::Scalar arc_angle( + const Eigen::MatrixBase &start_pos, + const Eigen::MatrixBase &end_pos, + const typename Derived::Scalar radius) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "arc_angle(): first parameter is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "arc_angle(): second parameter is not a 2D vector"); + static_assert(std::is_same::value, "arc_angle(): Both vectors must be of the same type."); + assert(radius != 0); + using Float = typename Derived::Scalar; + Float a = Float(0.5) * (end_pos - start_pos).norm() / radius; + return radius > Float(0) ? + // acute angle: + (a > Float( 1.) ? Float(M_PI) : Float(2.) * std::asin(a)) : + // obtuse angle: + (a < Float(-1.) ? Float(M_PI) : Float(2. * M_PI) + Float(2.) * std::asin(a)); +} + +// Calculate positive length of an arc given two points and a radius. +// positive radius: take shorter arc +// negative radius: take longer arc +// radius must NOT be zero! +template +inline typename Derived::Scalar arc_length( + const Eigen::MatrixBase &start_pos, + const Eigen::MatrixBase &end_pos, + const typename Derived::Scalar radius) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "arc_length(): first parameter is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "arc_length(): second parameter is not a 2D vector"); + static_assert(std::is_same::value, "arc_length(): Both vectors must be of the same type."); + assert(radius != 0); + return arc_angle(start_pos, end_pos, radius) * std::abs(radius); +} + +// Calculate positive length of an arc given two points, center and orientation. +template +inline typename Derived::Scalar arc_length( + const Eigen::MatrixBase &start_pos, + const Eigen::MatrixBase &end_pos, + const Eigen::MatrixBase ¢er_pos, + const bool ccw) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "arc_length(): first parameter is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "arc_length(): second parameter is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "arc_length(): third parameter is not a 2D vector"); + static_assert(std::is_same::value && + std::is_same::value, "arc_length(): All third points must be of the same type."); + using Float = typename Derived::Scalar; + auto vstart = start_pos - center_pos; + auto vend = end_pos - center_pos; + Float radius = vstart.norm(); + Float angle = atan2(double(cross2(vstart, vend)), double(vstart.dot(vend))); + if (! ccw) + angle *= Float(-1.); + if (angle < 0) + angle += Float(2. * M_PI); + assert(angle >= Float(0.) && angle < Float(2. * M_PI + EPSILON)); + return angle * radius; +} + +// Be careful! This version has a strong bias towards small circles with small radii +// for small angle (nearly straight) arches! +// One should rather use arc_fit_center_gauss_newton_ls(), which solves a non-linear least squares problem. +// +// Calculate center point (center of a circle) of an arc given two fixed points to interpolate +// and an additional list of points to fit by least squares. +// The circle fitting problem is non-linear, it was linearized by taking difference of squares of radii as a residual. +// Therefore the center point is required as a point to linearize at. +// Start & end point must be different and the interpolated points must not be collinear with input points. +template +inline typename Eigen::Matrix arc_fit_center_algebraic_ls( + const Eigen::MatrixBase &start_pos, + const Eigen::MatrixBase &end_pos, + const Eigen::MatrixBase ¢er_pos, + const Iterator it_begin, + const Iterator it_end) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "arc_fit_center_algebraic_ls(): start_pos is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "arc_fit_center_algebraic_ls(): end_pos is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "arc_fit_center_algebraic_ls(): third parameter is not a 2D vector"); + static_assert(std::is_same::value && + std::is_same::value, "arc_fit_center_algebraic_ls(): All third points must be of the same type."); + using Float = typename Derived::Scalar; + using Vector = Eigen::Matrix; + // Prepare a vector space to transform the fitting into: + // center_pos, dir_x, dir_y + Vector v = end_pos - start_pos; + Vector c = Float(.5) * (start_pos + end_pos); + Float lv = v.norm(); + assert(lv > 0); + Vector dir_y = v / lv; + Vector dir_x = perp(dir_y); + // Center of the new system: + // Center X at the projection of center_pos + Float offset_x = dir_x.dot(center_pos); + // Center is supposed to lie on bisector of the arc end points. + // Center Y at the mid point of v. + Float offset_y = dir_y.dot(c); + assert(std::abs(dir_y.dot(center_pos) - offset_y) < SCALED_EPSILON); + assert((dir_x * offset_x + dir_y * offset_y - center_pos).norm() < SCALED_EPSILON); + // Solve the least squares fitting in a transformed space. + Float a = Float(0.5) * lv; + Float b = c.dot(dir_x) - offset_x; + Float ab2 = sqr(a) + sqr(b); + Float num = Float(0); + Float denom = Float(0); + const Float w = it_end - it_begin; + for (Iterator it = it_begin; it != it_end; ++ it) { + Vector p = *it; + Float x_i = dir_x.dot(p) - offset_x; + Float y_i = dir_y.dot(p) - offset_y; + Float x_i2 = sqr(x_i); + Float y_i2 = sqr(y_i); + num += (x_i - b) * (x_i2 + y_i2 - ab2); + denom += b * (b - Float(2) * x_i) + sqr(x_i) + Float(0.25) * w; + } + assert(denom != 0); + Float c_x = Float(0.5) * num / denom; + // Transform the center back. + Vector out = dir_x * (c_x + offset_x) + dir_y * offset_y; + return out; +} + +// Calculate center point (center of a circle) of an arc given two fixed points to interpolate +// and an additional list of points to fit by non-linear least squares. +// The non-linear least squares problem is solved by a Gauss-Newton iterative method. +// Start & end point must be different and the interpolated points must not be collinear with input points. +// Center position is used to calculate the initial solution of the Gauss-Newton method. +// +// In case the input points are collinear or close to collinear (such as a small angle arc), +// the solution may not converge and an error is indicated. +template +inline std::optional> arc_fit_center_gauss_newton_ls( + const Eigen::MatrixBase &start_pos, + const Eigen::MatrixBase &end_pos, + const Eigen::MatrixBase ¢er_pos, + const Iterator it_begin, + const Iterator it_end, + const size_t num_iterations) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "arc_fit_center_gauss_newton_ls(): start_pos is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "arc_fit_center_gauss_newton_ls(): end_pos is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "arc_fit_center_gauss_newton_ls(): third parameter is not a 2D vector"); + static_assert(std::is_same::value && + std::is_same::value, "arc_fit_center_gauss_newton_ls(): All third points must be of the same type."); + using Float = typename Derived::Scalar; + using Vector = Eigen::Matrix; + // Prepare a vector space to transform the fitting into: + // center_pos, dir_x, dir_y + Vector v = end_pos - start_pos; + Vector c = Float(.5) * (start_pos + end_pos); + Float lv = v.norm(); + assert(lv > 0); + Vector dir_y = v / lv; + Vector dir_x = perp(dir_y); + // Center is supposed to lie on bisector of the arc end points. + // Center Y at the mid point of v. + Float offset_y = dir_y.dot(c); + // Initial value of the parameter to be optimized iteratively. + Float c_x = dir_x.dot(center_pos); + // Solve the least squares fitting in a transformed space. + Float a = Float(0.5) * lv; + Float a2 = sqr(a); + Float b = c.dot(dir_x); + for (size_t iter = 0; iter < num_iterations; ++ iter) { + Float num = Float(0); + Float denom = Float(0); + Float u = b - c_x; + // Current estimate of the circle radius. + Float r = sqrt(a2 + sqr(u)); + assert(r > 0); + for (Iterator it = it_begin; it != it_end; ++ it) { + Vector p = *it; + Float x_i = dir_x.dot(p); + Float y_i = dir_y.dot(p) - offset_y; + Float v = x_i - c_x; + Float y_i2 = sqr(y_i); + // Distance of i'th sample from the current circle center. + Float r_i = sqrt(sqr(v) + y_i2); + if (r_i >= EPSILON) { + // Square of residual is differentiable at the current c_x and current sample. + // Jacobian: diff(residual, c_x) + Float j_i = u / r - v / r_i; + num += j_i * (r_i - r); + denom += sqr(j_i); + } else { + // Sample point is on current center of the circle, + // therefore the gradient is not defined. + } + } + if (denom == 0) + // Fitting diverged, the input points are likely nearly collinear with the arch end points. + return std::optional(); + c_x -= num / denom; + } + // Transform the center back. + return std::optional(dir_x * c_x + dir_y * offset_y); +} + +// Test whether a point is inside a wedge of an arc. +template +inline bool inside_arc_wedge_vectors( + const Eigen::MatrixBase &start_vec, + const Eigen::MatrixBase &end_vec, + const bool shorter_arc, + const bool ccw, + const Eigen::MatrixBase &query_vec) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "inside_arc_wedge_vectors(): start_vec is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "inside_arc_wedge_vectors(): end_vec is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "inside_arc_wedge_vectors(): query_vec is not a 2D vector"); + static_assert(std::is_same::value && + std::is_same::value, "inside_arc_wedge_vectors(): All vectors must be of the same type."); + return shorter_arc ? + // Smaller (convex) wedge. + (ccw ? + cross2(start_vec, query_vec) > 0 && cross2(query_vec, end_vec) > 0 : + cross2(start_vec, query_vec) < 0 && cross2(query_vec, end_vec) < 0) : + // Larger (concave) wedge. + (ccw ? + cross2(end_vec, query_vec) < 0 || cross2(query_vec, start_vec) < 0 : + cross2(end_vec, query_vec) > 0 || cross2(query_vec, start_vec) > 0); +} + +template +inline bool inside_arc_wedge( + const Eigen::MatrixBase &start_pt, + const Eigen::MatrixBase &end_pt, + const Eigen::MatrixBase ¢er_pt, + const bool shorter_arc, + const bool ccw, + const Eigen::MatrixBase &query_pt) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "inside_arc_wedge(): start_pt is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "inside_arc_wedge(): end_pt is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "inside_arc_wedge(): center_pt is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived4::SizeAtCompileTime) == 2, "inside_arc_wedge(): query_pt is not a 2D vector"); + static_assert(std::is_same::value && + std::is_same::value && + std::is_same::value, "inside_arc_wedge(): All vectors must be of the same type."); + return inside_arc_wedge_vectors(start_pt - center_pt, end_pt - center_pt, shorter_arc, ccw, query_pt - center_pt); +} + +template +inline bool inside_arc_wedge( + const Eigen::MatrixBase &start_pt, + const Eigen::MatrixBase &end_pt, + const Float radius, + const bool ccw, + const Eigen::MatrixBase &query_pt) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "inside_arc_wedge(): start_pt is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "inside_arc_wedge(): end_pt is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "inside_arc_wedge(): query_pt is not a 2D vector"); + static_assert(std::is_same::value && + std::is_same::value && + std::is_same::value, "inside_arc_wedge(): All vectors + radius must be of the same type."); + return inside_arc_wedge(start_pt, end_pt, + arc_center(start_pt, end_pt, radius, ccw), + radius > 0, ccw, query_pt); +} + +// Return number of linear segments necessary to interpolate arc of a given positive radius and positive angle to satisfy +// maximum deviation of an interpolating polyline from an analytic arc. +template +size_t arc_discretization_steps(const FloatType radius, const FloatType angle, const FloatType deviation) +{ + assert(radius > 0); + assert(angle > 0); + assert(angle <= FloatType(2. * M_PI)); + assert(deviation > 0); + + FloatType d = radius - deviation; + return d < EPSILON ? + // Radius smaller than deviation. + ( // Acute angle: a single segment interpolates the arc with sufficient accuracy. + angle < M_PI || + // Obtuse angle: Test whether the furthest point (center) of an arc is closer than deviation to the center of a line segment. + radius * (FloatType(1.) + cos(M_PI - FloatType(.5) * angle)) < deviation ? + // Single segment is sufficient + 1 : + // Two segments are necessary, the middle point is at the center of the arc. + 2) : + size_t(ceil(angle / (2. * acos(d / radius)))); +} + +// Discretize arc given the radius, orientation and maximum deviation from the arc. +// Returned polygon starts with p1, ends with p2 and it is discretized to guarantee the maximum deviation. +Points arc_discretize(const Point &p1, const Point &p2, const double radius, const bool ccw, const double deviation); + +// Variance of the arc fit of points (600.); +// 0.05mm +static constexpr const double default_scaled_resolution = scaled(0.05); +// 5 percent +static constexpr const double default_arc_length_percent_tolerance = 0.05; + +enum class Orientation : unsigned char { + Unknown, + CCW, + CW, +}; + +// Returns orientation of a polyline with regard to the center. +// Successive points are expected to take less than a PI angle step. +// Returns Orientation::Unknown if the orientation with regard to the center +// is not monotonous. +Orientation arc_orientation( + const Point ¢er, + const Points::const_iterator begin, + const Points::const_iterator end); + +// Single segment of a smooth path. +struct Segment +{ + // End point of a linear or circular segment. + // Start point is provided by the preceding segment. + Point point; + // Radius of a circular segment. Positive - take the shorter arc. Negative - take the longer arc. Zero - linear segment. + float radius{ 0.f }; + // CCW or CW. Ignored for zero radius (linear segment). + Orientation orientation{ Orientation::CCW }; + + bool linear() const { return radius == 0; } + bool ccw() const { return orientation == Orientation::CCW; } + bool cw() const { return orientation == Orientation::CW; } +}; + +inline bool operator==(const Segment &lhs, const Segment &rhs) { + return lhs.point == rhs.point && lhs.radius == rhs.radius && lhs.orientation == rhs.orientation; +} + +using Segments = std::vector; +using Path = Segments; + +// Interpolate polyline path with a sequence of linear / circular segments given the interpolation tolerance. +// Only convert to polyline if zero tolerance. +// Convert to polyline and decimate polyline if zero fit_circle_percent_tolerance. +// Path fitting is inspired with the arc fitting algorithm in +// Arc Welder: Anti-Stutter Library by Brad Hochgesang FormerLurker@pm.me +// https://github.com/FormerLurker/ArcWelderLib +Path fit_path(const Points &points, double tolerance, double fit_circle_percent_tolerance); + +// Decimate polyline into a smooth path structure using Douglas-Peucker polyline decimation algorithm. +inline Path fit_polyline(const Points &points, double tolerance) { return fit_path(points, tolerance, 0.); } + +template +inline FloatType segment_length(const Segment &start, const Segment &end) +{ + return end.linear() ? + (end.point - start.point).cast().norm() : + arc_length(start.point.cast(), end.point.cast(), FloatType(end.radius)); +} + +template +inline FloatType path_length(const Path &path) +{ + FloatType len = 0; + for (size_t i = 1; i < path.size(); ++ i) + len += segment_length(path[i - 1], path[i]); + return len; +} + +// Estimate minimum path length of a segment cheaply without having to calculate center of an arc and it arc length. +// Used for caching a smooth path chunk that is certainly longer than a threshold. +inline int64_t estimate_min_segment_length(const Segment &start, const Segment &end) +{ + if (end.linear() || end.radius > 0) { + // Linear segment or convex wedge, take the larger X or Y component. + Point v = (end.point - start.point).cwiseAbs(); + return std::max(v.x(), v.y()); + } else { + // Arc with angle > PI. + // Returns estimate of PI * r + return - int64_t(3) * int64_t(end.radius); + } +} + +// Estimate minimum path length cheaply without having to calculate center of an arc and it arc length. +// Used for caching a smooth path chunk that is certainly longer than a threshold. +inline int64_t estimate_path_length(const Path &path) +{ + int64_t len = 0; + for (size_t i = 1; i < path.size(); ++ i) + len += Geometry::ArcWelder::estimate_min_segment_length(path[i - 1], path[i]); + return len; +} + +void reverse(Path &path); + +// Clip start / end of a smooth path by len. +// If path is shorter than len, remaining path length to trim will be returned. +double clip_start(Path &path, const double len); +double clip_end(Path &path, const double len); + +struct PathSegmentProjection +{ + // Start segment of a projection on the path. + size_t segment_id { std::numeric_limits::max() }; + // Projection of the point on the segment. + Point point { 0, 0 }; + // If the point lies on an arc, the arc center is cached here. + Point center { 0, 0 }; + // Square of a distance of the projection. + double distance2 { std::numeric_limits::max() }; + + bool valid() const { return this->segment_id != std::numeric_limits::max(); } +}; +// Returns closest segment and a parameter along the closest segment of a path to a point. +PathSegmentProjection point_to_path_projection(const Path &path, const Point &point, double search_radius2 = std::numeric_limits::max()); +// Split a path into two paths at a segment point. Snap to an existing point if the projection of "point is closer than min_segment_length. +std::pair split_at(const Path &path, const PathSegmentProjection &proj, const double min_segment_length); +// Split a path into two paths at a point closest to "point". Snap to an existing point if the projection of "point is closer than min_segment_length. +std::pair split_at(const Path &path, const Point &point, const double min_segment_length); + +} } } // namespace Slic3r::Geometry::ArcWelder + +#endif // slic3r_Geometry_ArcWelder_hpp_ diff --git a/src/libslic3r/Geometry/Circle.cpp b/src/libslic3r/Geometry/Circle.cpp index 6796671..b44aca1 100644 --- a/src/libslic3r/Geometry/Circle.cpp +++ b/src/libslic3r/Geometry/Circle.cpp @@ -137,4 +137,82 @@ Circled circle_ransac(const Vec2ds& input, size_t iterations, double* min_error) return circle_best; } +template +Circled circle_linear_least_squares_by_solver(const Vec2ds &input, Solver solver) +{ + Circled out; + if (input.size() < 3) { + out = Circled::make_invalid(); + } else { + Eigen::Matrix A(input.size(), 3); + Eigen::VectorXd b(input.size()); + for (size_t r = 0; r < input.size(); ++ r) { + const Vec2d &p = input[r]; + A.row(r) = Vec3d(2. * p.x(), 2. * p.y(), - 1.); + b(r) = p.squaredNorm(); + } + auto result = solver(A, b); + out.center = result.template head<2>(); + double r2 = out.center.squaredNorm() - result(2); + if (r2 <= EPSILON) + out.make_invalid(); + else + out.radius = sqrt(r2); + } + + return out; +} + +Circled circle_linear_least_squares_svd(const Vec2ds &input) +{ + return circle_linear_least_squares_by_solver(input, + [](const Eigen::Matrix &A, const Eigen::VectorXd &b) + { return A.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b).eval(); }); +} + +Circled circle_linear_least_squares_qr(const Vec2ds &input) +{ + return circle_linear_least_squares_by_solver(input, + [](const Eigen::Matrix &A, const Eigen::VectorXd &b) + { return A.colPivHouseholderQr().solve(b).eval(); }); +} + +Circled circle_linear_least_squares_normal(const Vec2ds &input) +{ + Circled out; + if (input.size() < 3) { + out = Circled::make_invalid(); + } else { + Eigen::Matrix A = Eigen::Matrix::Zero(); + Eigen::Matrix b = Eigen::Matrix::Zero(); + for (size_t i = 0; i < input.size(); ++ i) { + const Vec2d &p = input[i]; + // Calculate right hand side of a normal equation. + b += p.squaredNorm() * Vec3d(2. * p.x(), 2. * p.y(), -1.); + // Calculate normal matrix (correlation matrix). + // Diagonal: + A(0, 0) += 4. * p.x() * p.x(); + A(1, 1) += 4. * p.y() * p.y(); + A(2, 2) += 1.; + // Off diagonal elements: + const double a = 4. * p.x() * p.y(); + A(0, 1) += a; + A(1, 0) += a; + const double b = -2. * p.x(); + A(0, 2) += b; + A(2, 0) += b; + const double c = -2. * p.y(); + A(1, 2) += c; + A(2, 1) += c; + } + auto result = A.ldlt().solve(b).eval(); + out.center = result.head<2>(); + double r2 = out.center.squaredNorm() - result(2); + if (r2 <= EPSILON) + out.make_invalid(); + else + out.radius = sqrt(r2); + } + return out; +} } } // namespace Slic3r::Geometry diff --git a/src/libslic3r/Geometry/Circle.hpp b/src/libslic3r/Geometry/Circle.hpp index 653102e..855dbbe 100644 --- a/src/libslic3r/Geometry/Circle.hpp +++ b/src/libslic3r/Geometry/Circle.hpp @@ -9,10 +9,17 @@ namespace Slic3r { namespace Geometry { // https://en.wikipedia.org/wiki/Circumscribed_circle // Circumcenter coordinates, Cartesian coordinates -template -Vector circle_center(const Vector &a, const Vector &bsrc, const Vector &csrc, typename Vector::Scalar epsilon) +// In case the three points are collinear, returns their centroid. +template +Eigen::Matrix circle_center(const Derived &a, const Derived2 &bsrc, const Derived3 &csrc, typename Derived::Scalar epsilon) { - using Scalar = typename Vector::Scalar; + static_assert(Derived ::IsVectorAtCompileTime && int(Derived ::SizeAtCompileTime) == 2, "circle_center(): 1st point is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "circle_center(): 2nd point is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "circle_center(): 3rd point is not a 2D vector"); + static_assert(std::is_same::value && std::is_same::value, + "circle_center(): All three points must be of the same type."); + using Scalar = typename Derived::Scalar; + using Vector = Eigen::Matrix; Vector b = bsrc - a; Vector c = csrc - a; Scalar lb = b.squaredNorm(); @@ -30,6 +37,31 @@ Vector circle_center(const Vector &a, const Vector &bsrc, const Vector &csrc, ty } } +// https://en.wikipedia.org/wiki/Circumscribed_circle +// Circumcenter coordinates, Cartesian coordinates +// Returns no value if the three points are collinear. +template +std::optional> try_circle_center(const Derived &a, const Derived2 &bsrc, const Derived3 &csrc, typename Derived::Scalar epsilon) +{ + static_assert(Derived ::IsVectorAtCompileTime && int(Derived ::SizeAtCompileTime) == 2, "try_circle_center(): 1st point is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "try_circle_center(): 2nd point is not a 2D vector"); + static_assert(Derived3::IsVectorAtCompileTime && int(Derived3::SizeAtCompileTime) == 2, "try_circle_center(): 3rd point is not a 2D vector"); + static_assert(std::is_same::value && std::is_same::value, + "try_circle_center(): All three points must be of the same type."); + using Scalar = typename Derived::Scalar; + using Vector = Eigen::Matrix; + Vector b = bsrc - a; + Vector c = csrc - a; + Scalar lb = b.squaredNorm(); + Scalar lc = c.squaredNorm(); + if (Scalar d = b.x() * c.y() - b.y() * c.x(); std::abs(d) < epsilon) { + // The three points are collinear. + return {}; + } else { + Vector v = lc * b - lb * c; + return std::make_optional(a + Vector(- v.y(), v.x()) / (2 * d)); + } +} // 2D circle defined by its center and squared radius template struct CircleSq { @@ -65,7 +97,7 @@ struct Circle { Vector center; Scalar radius; - Circle() {} + Circle() = default; Circle(const Vector ¢er, const Scalar radius) : center(center), radius(radius) {} Circle(const Vector &a, const Vector &b) : center(Scalar(0.5) * (a + b)) { radius = (a - center).norm(); } Circle(const Vector &a, const Vector &b, const Vector &c, const Scalar epsilon) { *this = CircleSq(a, b, c, epsilon); } @@ -104,6 +136,17 @@ Circled circle_taubin_newton(const Vec2ds& input, size_t cycles = 20); // Find circle using RANSAC randomized algorithm. Circled circle_ransac(const Vec2ds& input, size_t iterations = 20, double* min_error = nullptr); +// Linear Least squares fitting. +// Be careful! The linear least squares fitting is strongly biased towards small circles, +// thus the method is only recommended for circles or arches with large arc angle. +// Also it is strongly recommended to center the input at an expected circle (or arc) center +// to minimize the small circle bias! + // Linear Least squares fitting with SVD. Most accurate, but slowest. + Circled circle_linear_least_squares_svd(const Vec2ds &input); + // Linear Least squares fitting with QR decomposition. Medium accuracy, medium speed. + Circled circle_linear_least_squares_qr(const Vec2ds &input); + // Linear Least squares fitting solving normal equations. Low accuracy, high speed. + Circled circle_linear_least_squares_normal(const Vec2ds &input); // Randomized algorithm by Emo Welzl, working with squared radii for efficiency. The returned circle radius is inflated by epsilon. template CircleSq smallest_enclosing_circle2_welzl(const Points &points, const typename Vector::Scalar epsilon) diff --git a/src/libslic3r/IntersectionPoints.cpp b/src/libslic3r/IntersectionPoints.cpp index 78d1923..307d51b 100644 --- a/src/libslic3r/IntersectionPoints.cpp +++ b/src/libslic3r/IntersectionPoints.cpp @@ -1,171 +1,50 @@ #include "IntersectionPoints.hpp" -//#define USE_CGAL_SWEEP_LINE -#ifdef USE_CGAL_SWEEP_LINE +#include -#include -#include -#include -#include -#include +//NOTE: using CGAL SweepLines is slower !!! (example in git history) -using NT = CGAL::Quotient; -using Kernel = CGAL::Cartesian; -using P2 = Kernel::Point_2; -using Traits_2 = CGAL::Arr_segment_traits_2; -using Segment = Traits_2::Curve_2; -using Segments = std::vector; - -namespace priv { - -P2 convert(const Slic3r::Point &p) { return P2(p.x(), p.y()); } -Slic3r::Vec2d convert(const P2 &p) +namespace { +using namespace Slic3r; +IntersectionsLines compute_intersections(const Lines &lines) { - return Slic3r::Vec2d(CGAL::to_double(p.x()), CGAL::to_double(p.y())); -} + if (lines.size() < 3) + return {}; -Slic3r::Pointfs compute_intersections(const Segments &segments) -{ - std::vector intersections; - // Compute all intersection points. - CGAL::compute_intersection_points(segments.begin(), segments.end(), - std::back_inserter(intersections)); - if (intersections.empty()) return {}; - Slic3r::Pointfs pts; - pts.reserve(intersections.size()); - for (const P2 &p : intersections) pts.push_back(convert(p)); - return pts; -} - -void add_polygon(const Slic3r::Polygon &polygon, Segments &segments) -{ - if (polygon.points.size() < 2) return; - P2 prev_point = priv::convert(polygon.last_point()); - for (const Slic3r::Point &p : polygon.points) { - P2 act_point = priv::convert(p); - if (prev_point == act_point) continue; - segments.emplace_back(prev_point, act_point); - prev_point = act_point; - } -} -Slic3r::Pointfs Slic3r::intersection_points(const Lines &lines) -{ - return priv::compute_intersections2(lines); - Segments segments; - segments.reserve(lines.size()); - for (Line l : lines) - segments.emplace_back(priv::convert(l.a), priv::convert(l.b)); - return priv::compute_intersections(segments); -} - -Slic3r::Pointfs Slic3r::intersection_points(const Polygon &polygon) -{ - Segments segments; - segments.reserve(polygon.points.size()); - priv::add_polygon(polygon, segments); - return priv::compute_intersections(segments); -} - -Slic3r::Pointfs Slic3r::intersection_points(const Polygons &polygons) -{ - Segments segments; - segments.reserve(count_points(polygons)); - for (const Polygon &polygon : polygons) - priv::add_polygon(polygon, segments); - return priv::compute_intersections(segments); -} - -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygon &expolygon) -{ - Segments segments; - segments.reserve(count_points(expolygon)); - priv::add_polygon(expolygon.contour, segments); - for (const Polygon &hole : expolygon.holes) - priv::add_polygon(hole, segments); - return priv::compute_intersections(segments); -} - -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygons &expolygons) -{ - Segments segments; - segments.reserve(count_points(expolygons)); - for (const ExPolygon &expolygon : expolygons) { - priv::add_polygon(expolygon.contour, segments); - for (const Polygon &hole : expolygon.holes) - priv::add_polygon(hole, segments); - } - return priv::compute_intersections(segments); -} - - -} // namespace priv - -#else // USE_CGAL_SWEEP_LINE - -// use bounding boxes -#include - -namespace priv { -//FIXME O(n^2) complexity! -Slic3r::Pointfs compute_intersections(const Slic3r::Lines &lines) -{ - using namespace Slic3r; - // IMPROVE0: BoundingBoxes of Polygons - // IMPROVE1: Polygon's neighbor lines can't intersect - // e.g. use indices to Point to find same points - // IMPROVE2: Use BentleyOttmann algorithm - // https://doc.cgal.org/latest/Surface_sweep_2/index.html -- CGAL implementation is significantly slower - // https://stackoverflow.com/questions/4407493/is-there-a-robust-c-implementation-of-the-bentley-ottmann-algorithm - Pointfs pts; - Point i; - for (size_t li = 0; li < lines.size(); ++li) { + auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + IntersectionsLines result; + for (uint32_t li = 0; li < lines.size()-1; ++li) { const Line &l = lines[li]; - const Point &a = l.a; - const Point &b = l.b; - Point min(std::min(a.x(), b.x()), std::min(a.y(), b.y())); - Point max(std::max(a.x(), b.x()), std::max(a.y(), b.y())); - BoundingBox bb(min, max); - for (size_t li_ = li + 1; li_ < lines.size(); ++li_) { - const Line &l_ = lines[li_]; - const Point &a_ = l_.a; - const Point &b_ = l_.b; - if (a == b_ || b == a_ || a == a_ || b == b_) continue; - Point min_(std::min(a_.x(), b_.x()), std::min(a_.y(), b_.y())); - Point max_(std::max(a_.x(), b_.x()), std::max(a_.y(), b_.y())); - BoundingBox bb_(min_, max_); - // intersect of BB compare min max - if (bb.overlap(bb_) && - l.intersection(l_, &i)) - pts.push_back(i.cast()); - } - } - return pts; -} -} // namespace priv + auto intersections = AABBTreeLines::get_intersections_with_line(lines, tree, l); + for (const auto &[p, node_index] : intersections) { + if (node_index - 1 <= li) + continue; + if (const Line &l_ = lines[node_index]; + l_.a == l.a || + l_.a == l.b || + l_.b == l.a || + l_.b == l.b ) + // it is duplicit point not intersection + continue; -Slic3r::Pointfs Slic3r::intersection_points(const Lines &lines) -{ - return priv::compute_intersections(lines); + // NOTE: fix AABBTree to compute intersection with double preccission!! + Vec2d intersection_point = p.cast(); + + result.push_back(IntersectionLines{li, static_cast(node_index), intersection_point}); } -Slic3r::Pointfs Slic3r::intersection_points(const Polygon &polygon) -{ - return priv::compute_intersections(to_lines(polygon)); } -Slic3r::Pointfs Slic3r::intersection_points(const Polygons &polygons) -{ - return priv::compute_intersections(to_lines(polygons)); + return result; +} +} // namespace + +namespace Slic3r { +IntersectionsLines get_intersections(const Lines &lines) { return compute_intersections(lines); } +IntersectionsLines get_intersections(const Polygon &polygon) { return compute_intersections(to_lines(polygon)); } +IntersectionsLines get_intersections(const Polygons &polygons) { return compute_intersections(to_lines(polygons)); } +IntersectionsLines get_intersections(const ExPolygon &expolygon) { return compute_intersections(to_lines(expolygon)); } +IntersectionsLines get_intersections(const ExPolygons &expolygons) { return compute_intersections(to_lines(expolygons)); } } -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygon &expolygon) -{ - return priv::compute_intersections(to_lines(expolygon)); -} -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygons &expolygons) -{ - return priv::compute_intersections(to_lines(expolygons)); -} - -#endif // USE_CGAL_SWEEP_LINE diff --git a/src/libslic3r/IntersectionPoints.hpp b/src/libslic3r/IntersectionPoints.hpp index 7603765..ae0299a 100644 --- a/src/libslic3r/IntersectionPoints.hpp +++ b/src/libslic3r/IntersectionPoints.hpp @@ -5,13 +5,18 @@ namespace Slic3r { +struct IntersectionLines { + uint32_t line_index1; + uint32_t line_index2; + Vec2d intersection; +}; +using IntersectionsLines = std::vector; // collect all intersecting points -//FIXME O(n^2) complexity! -Pointfs intersection_points(const Lines &lines); -Pointfs intersection_points(const Polygon &polygon); -Pointfs intersection_points(const Polygons &polygons); -Pointfs intersection_points(const ExPolygon &expolygon); -Pointfs intersection_points(const ExPolygons &expolygons); +IntersectionsLines get_intersections(const Lines &lines); +IntersectionsLines get_intersections(const Polygon &polygon); +IntersectionsLines get_intersections(const Polygons &polygons); +IntersectionsLines get_intersections(const ExPolygon &expolygon); +IntersectionsLines get_intersections(const ExPolygons &expolygons); } // namespace Slic3r #endif // slic3r_IntersectionPoints_hpp_ \ No newline at end of file diff --git a/src/libslic3r/Measure.cpp b/src/libslic3r/Measure.cpp index 9602b14..cda424e 100644 --- a/src/libslic3r/Measure.cpp +++ b/src/libslic3r/Measure.cpp @@ -185,7 +185,7 @@ void MeasuringImpl::update_planes() int neighbor_idx = face_neighbors[facets[face_id]][edge_id]; if (neighbor_idx == -1) goto PLANE_FAILURE; - if (visited[face_id][edge_id] || (int)face_to_plane[neighbor_idx] == plane_id) { + if (visited[face_id][edge_id] || face_to_plane[neighbor_idx] == plane_id) { visited[face_id][edge_id] = true; continue; } @@ -219,7 +219,7 @@ void MeasuringImpl::update_planes() // Remember all halfedges we saw to break out of such infinite loops. boost::container::small_vector he_seen; - while ( (int)face_to_plane[sm.face(he)] == plane_id && he != he_orig) { + while ( face_to_plane[sm.face(he)] == plane_id && he != he_orig) { he_seen.emplace_back(he); he = sm.next_around_target(he); if (he.is_invalid() || std::find(he_seen.begin(), he_seen.end(), he) != he_seen.end()) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 2f03341..a9af66e 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -14,6 +14,7 @@ #include "Format/STL.hpp" #include "Format/3mf.hpp" #include "Format/STEP.hpp" +#include "Format/SVG.hpp" #include @@ -27,7 +28,7 @@ #include "SVG.hpp" #include -#include "GCodeWriter.hpp" +#include "GCode/GCodeWriter.hpp" namespace Slic3r { @@ -124,8 +125,10 @@ Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* c else if (boost::algorithm::iends_with(input_file, ".3mf") || boost::algorithm::iends_with(input_file, ".zip")) //FIXME options & LoadAttribute::CheckVersion ? result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, false); + else if (boost::algorithm::iends_with(input_file, ".svg")) + result = load_svg(input_file, model); else - throw Slic3r::RuntimeError("Unknown file format. Input file must have .stl, .obj, .amf(.xml), .qidi or .step/.stp extension."); + throw Slic3r::RuntimeError("Unknown file format. Input file must have .stl, .obj, .step/.stp, .svg, .amf(.xml) or extension .3mf(.zip)."); if (! result) throw Slic3r::RuntimeError("Loading of a model file failed."); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 2df612f..4b6b2a7 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -14,6 +14,7 @@ #include "CustomGCode.hpp" #include "enum_bitmask.hpp" #include "TextConfiguration.hpp" +#include "EmbossShape.hpp" #include #include @@ -281,25 +282,26 @@ struct CutConnector float height; float radius_tolerance;// [0.f : 1.f] float height_tolerance;// [0.f : 1.f] + float z_angle {0.f}; CutConnectorAttributes attribs; CutConnector() - : pos(Vec3d::Zero()), rotation_m(Transform3d::Identity()), radius(5.f), height(10.f), radius_tolerance(0.f), height_tolerance(0.1f) + : pos(Vec3d::Zero()), rotation_m(Transform3d::Identity()), radius(5.f), height(10.f), radius_tolerance(0.f), height_tolerance(0.1f), z_angle(0.f) {} - CutConnector(Vec3d p, Transform3d rot, float r, float h, float rt, float ht, CutConnectorAttributes attributes) - : pos(p), rotation_m(rot), radius(r), height(h), radius_tolerance(rt), height_tolerance(ht), attribs(attributes) + CutConnector(Vec3d p, Transform3d rot, float r, float h, float rt, float ht, float za, CutConnectorAttributes attributes) + : pos(p), rotation_m(rot), radius(r), height(h), radius_tolerance(rt), height_tolerance(ht), z_angle(za), attribs(attributes) {} CutConnector(const CutConnector& rhs) : - CutConnector(rhs.pos, rhs.rotation_m, rhs.radius, rhs.height, rhs.radius_tolerance, rhs.height_tolerance, rhs.attribs) {} + CutConnector(rhs.pos, rhs.rotation_m, rhs.radius, rhs.height, rhs.radius_tolerance, rhs.height_tolerance, rhs.z_angle, rhs.attribs) {} bool operator==(const CutConnector& other) const; bool operator!=(const CutConnector& other) const { return !(other == (*this)); } template inline void serialize(Archive& ar) { - ar(pos, rotation_m, radius, height, radius_tolerance, height_tolerance, attribs); + ar(pos, rotation_m, radius, height, radius_tolerance, height_tolerance, z_angle, attribs); } }; @@ -816,6 +818,9 @@ public: // Contain information how to re-create volume std::optional text_configuration; + // Is set only when volume is Embossed Shape + // Contain 2d information about embossed shape to be editabled + std::optional emboss_shape; // A parent object owning this modifier volume. ModelObject* get_object() const { return this->object; } ModelVolumeType type() const { return m_type; } @@ -827,6 +832,7 @@ public: bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } bool is_text() const { return text_configuration.has_value(); } + bool is_svg() const { return emboss_shape.has_value() && !text_configuration.has_value(); } bool is_the_only_one_part() const; // behave like an object t_model_material_id material_id() const { return m_material_id; } void reset_extra_facets(); @@ -985,8 +991,7 @@ private: name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), - cut_info(other.cut_info), - text_configuration(other.text_configuration) + cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -1007,8 +1012,7 @@ private: // Providing a new mesh, therefore this volume will get a new unique ID assigned. ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation), - cut_info(other.cut_info), - text_configuration(other.text_configuration) + cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -1056,6 +1060,7 @@ private: cereal::load_by_value(ar, mmu_segmentation_facets); cereal::load_by_value(ar, config); cereal::load(ar, text_configuration); + cereal::load(ar, emboss_shape); assert(m_mesh); if (has_convex_hull) { cereal::load_optional(ar, m_convex_hull); @@ -1073,6 +1078,7 @@ private: cereal::save_by_value(ar, mmu_segmentation_facets); cereal::save_by_value(ar, config); cereal::save(ar, text_configuration); + cereal::save(ar, emboss_shape); if (has_convex_hull) cereal::save_optional(ar, m_convex_hull); } diff --git a/src/libslic3r/MultiPoint.cpp b/src/libslic3r/MultiPoint.cpp index fb4727a..b01ef13 100644 --- a/src/libslic3r/MultiPoint.cpp +++ b/src/libslic3r/MultiPoint.cpp @@ -103,99 +103,6 @@ bool MultiPoint::remove_duplicate_points() return false; } -Points MultiPoint::douglas_peucker(const Points &pts, const double tolerance) -{ - Points result_pts; - auto tolerance_sq = int64_t(sqr(tolerance)); - if (! pts.empty()) { - const Point *anchor = &pts.front(); - size_t anchor_idx = 0; - const Point *floater = &pts.back(); - size_t floater_idx = pts.size() - 1; - result_pts.reserve(pts.size()); - result_pts.emplace_back(*anchor); - if (anchor_idx != floater_idx) { - assert(pts.size() > 1); - std::vector dpStack; - dpStack.reserve(pts.size()); - dpStack.emplace_back(floater_idx); - for (;;) { - int64_t max_dist_sq = 0; - size_t furthest_idx = anchor_idx; - // find point furthest from line seg created by (anchor, floater) and note it - { - const Point a = *anchor; - const Point f = *floater; - const Vec2i64 v = (f - a).cast(); - if (const int64_t l2 = v.squaredNorm(); l2 == 0) { - for (size_t i = anchor_idx + 1; i < floater_idx; ++ i) - if (int64_t dist_sq = (pts[i] - a).cast().squaredNorm(); dist_sq > max_dist_sq) { - max_dist_sq = dist_sq; - furthest_idx = i; - } - } else { - const double dl2 = double(l2); - const Vec2d dv = v.cast(); - for (size_t i = anchor_idx + 1; i < floater_idx; ++ i) { - const Point p = pts[i]; - const Vec2i64 va = (p - a).template cast(); - const int64_t t = va.dot(v); - int64_t dist_sq; - if (t <= 0) { - dist_sq = va.squaredNorm(); - } else if (t >= l2) { - dist_sq = (p - f).cast().squaredNorm(); - } else { - const Vec2i64 w = ((double(t) / dl2) * dv).cast(); - dist_sq = (w - va).squaredNorm(); - } - if (dist_sq > max_dist_sq) { - max_dist_sq = dist_sq; - furthest_idx = i; - } - } - } - } - // remove point if less than tolerance - if (max_dist_sq <= tolerance_sq) { - result_pts.emplace_back(*floater); - anchor_idx = floater_idx; - anchor = floater; - assert(dpStack.back() == floater_idx); - dpStack.pop_back(); - if (dpStack.empty()) - break; - floater_idx = dpStack.back(); - } else { - floater_idx = furthest_idx; - dpStack.emplace_back(floater_idx); - } - floater = &pts[floater_idx]; - } - } - assert(result_pts.front() == pts.front()); - assert(result_pts.back() == pts.back()); - -#if 0 - { - static int iRun = 0; - BoundingBox bbox(pts); - BoundingBox bbox2(result_pts); - bbox.merge(bbox2); - SVG svg(debug_out_path("douglas_peucker_%d.svg", iRun ++).c_str(), bbox); - if (pts.front() == pts.back()) - svg.draw(Polygon(pts), "black"); - else - svg.draw(Polyline(pts), "black"); - if (result_pts.front() == result_pts.back()) - svg.draw(Polygon(result_pts), "green", scale_(0.1)); - else - svg.draw(Polyline(result_pts), "green", scale_(0.1)); - } -#endif - } - return result_pts; -} // Visivalingam simplification algorithm https://github.com/slic3r/Slic3r/pull/3825 // thanks to @fuchstraumer @@ -219,7 +126,7 @@ struct vis_node{ // other node if it's area is less than the other node's area bool operator<(const vis_node& other) { return (this->area < other.area); } }; -Points MultiPoint::visivalingam(const Points& pts, const double& tolerance) +Points MultiPoint::visivalingam(const Points &pts, const double tolerance) { // Make sure there's enough points in "pts" to bother with simplification. assert(pts.size() >= 2); diff --git a/src/libslic3r/MultiPoint.hpp b/src/libslic3r/MultiPoint.hpp index 637b059..edad80e 100644 --- a/src/libslic3r/MultiPoint.hpp +++ b/src/libslic3r/MultiPoint.hpp @@ -12,6 +12,122 @@ namespace Slic3r { class BoundingBox; class BoundingBox3; +// Reduces polyline in the +inline OutputIterator douglas_peucker(InputIterator begin, InputIterator end, OutputIterator out, const double tolerance, PointGetter point_getter) +{ + using InputIteratorCategory = typename std::iterator_traits::iterator_category; + static_assert(std::is_base_of_v); + using Vector = Eigen::Matrix; + if (begin != end) { + // Supporting in-place reduction and the data type may be generic, thus we are always making a copy of the point value before there is a chance + // to override input by moving the data to the output. + auto a = point_getter(*begin); + *out ++ = std::move(*begin); + if (auto next = std::next(begin); next == end) { + // Single point input only. + } else if (std::next(next) == end) { + // Two points input. + *out ++ = std::move(*next); + } else { + const auto tolerance_sq = SquareLengthType(sqr(tolerance)); + InputIterator anchor = begin; + InputIterator floater = std::prev(end); + std::vector dpStack; + if constexpr (std::is_base_of_v) + dpStack.reserve(end - begin); + dpStack.emplace_back(floater); + auto f = point_getter(*floater); + for (;;) { + assert(anchor != floater); + bool take_floater = false; + InputIterator furthest = anchor; + if (std::next(anchor) == floater) { + // Two point segment. Accept the floater. + take_floater = true; + } else { + SquareLengthType max_dist_sq = 0; + // Find point furthest from line seg created by (anchor, floater) and note it. + const Vector v = (f - a).template cast(); + if (const SquareLengthType l2 = v.squaredNorm(); l2 == 0) { + // Zero length segment, find the furthest point between anchor and floater. + for (auto it = std::next(anchor); it != floater; ++ it) + if (SquareLengthType dist_sq = (point_getter(*it) - a).template cast().squaredNorm(); + dist_sq > max_dist_sq) { + max_dist_sq = dist_sq; + furthest = it; + } + } else { + // Find Find the furthest point from the line . + const double dl2 = double(l2); + const Vec2d dv = v.template cast(); + for (auto it = std::next(anchor); it != floater; ++ it) { + const auto p = point_getter(*it); + const Vector va = (p - a).template cast(); + const SquareLengthType t = va.dot(v); + SquareLengthType dist_sq; + if (t <= 0) { + dist_sq = va.squaredNorm(); + } else if (t >= l2) { + dist_sq = (p - f).template cast().squaredNorm(); + } else if (double dt = double(t) / dl2; dt <= 0) { + dist_sq = va.squaredNorm(); + } else if (dt >= 1.) { + dist_sq = (p - f).template cast().squaredNorm(); + } else { + const Vector w = (dt * dv).cast(); + dist_sq = (w - va).squaredNorm(); + } + if (dist_sq > max_dist_sq) { + max_dist_sq = dist_sq; + furthest = it; + } + } + } + // remove point if less than tolerance + take_floater = max_dist_sq <= tolerance_sq; + } + if (take_floater) { + // The points between anchor and floater are close to the line. + // Drop the points between them. + a = f; + *out ++ = std::move(*floater); + anchor = floater; + assert(dpStack.back() == floater); + dpStack.pop_back(); + if (dpStack.empty()) + break; + floater = dpStack.back(); + f = point_getter(*floater); + } else { + // The furthest point is too far from the segment . + // Divide recursively. + floater = furthest; + f = point_getter(*floater); + dpStack.emplace_back(floater); + } + } + } + } + return out; +} + +// Reduces polyline in the +inline OutputIterator douglas_peucker(Points::const_iterator begin, Points::const_iterator end, OutputIterator out, const double tolerance) +{ + return douglas_peucker(begin, end, out, tolerance, [](const Point &p) { return p; }); +} + +inline Points douglas_peucker(const Points &src, const double tolerance) +{ + Points out; + out.reserve(src.size()); + douglas_peucker(src.begin(), src.end(), std::back_inserter(out), tolerance); + return out; +} class MultiPoint { public: @@ -81,8 +197,8 @@ public: } } - static Points douglas_peucker(const Points &points, const double tolerance); - static Points visivalingam(const Points& pts, const double& tolerance); + static Points douglas_peucker(const Points &src, const double tolerance) { return Slic3r::douglas_peucker(src, tolerance); } + static Points visivalingam(const Points &src, const double tolerance); inline auto begin() { return points.begin(); } inline auto begin() const { return points.begin(); } @@ -119,16 +235,19 @@ extern BoundingBox get_extents(const MultiPoint &mp); extern BoundingBox get_extents_rotated(const Points &points, double angle); extern BoundingBox get_extents_rotated(const MultiPoint &mp, double angle); -inline double length(const Points &pts) { +inline double length(const Points::const_iterator begin, const Points::const_iterator end) { double total = 0; - if (! pts.empty()) { - auto it = pts.begin(); - for (auto it_prev = it ++; it != pts.end(); ++ it, ++ it_prev) + if (begin != end) { + auto it = begin; + for (auto it_prev = it ++; it != end; ++ it, ++ it_prev) total += (*it - *it_prev).cast().norm(); } return total; } +inline double length(const Points &pts) { + return length(pts.begin(), pts.end()); +} inline double area(const Points &polygon) { double area = 0.; for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i ++) diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp index 2844fc5..e9576f6 100644 --- a/src/libslic3r/NSVGUtils.cpp +++ b/src/libslic3r/NSVGUtils.cpp @@ -1,85 +1,539 @@ #include "NSVGUtils.hpp" +#include +#include // to_chars + +#include +#include #include "ClipperUtils.hpp" +#include "Emboss.hpp" // heal for shape -using namespace Slic3r; +namespace { +using namespace Slic3r; // Polygon +// see function nsvg__lineTo(NSVGparser* p, float x, float y) +bool is_line(const float *p, float precision = 1e-4f); +// convert curve in path to lines +struct LinesPath{ + Polygons polygons; + Polylines polylines; }; +LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m); +HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); +HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); +} // namespace -// inspired by nanosvgrast.h function nsvgRasterize -> nsvg__flattenShape -> nsvg__flattenCubicBez -// https://github.com/memononen/nanosvg/blob/f0a3e1034dd22e2e87e5db22401e44998383124e/src/nanosvgrast.h#L335 -void NSVGUtils::flatten_cubic_bez(Polygon &polygon, - float tessTol, - Vec2f p1, - Vec2f p2, - Vec2f p3, - Vec2f p4, - int level) +namespace Slic3r { + +ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m) { - Vec2f p12 = (p1 + p2) * 0.5f; - Vec2f p23 = (p2 + p3) * 0.5f; - Vec2f p34 = (p3 + p4) * 0.5f; - Vec2f p123 = (p12 + p23) * 0.5f; + ExPolygonsWithIds result; + size_t shape_id = 0; + for (NSVGshape *shape_ptr = image.shapes; shape_ptr != NULL; shape_ptr = shape_ptr->next, ++shape_id) { + const NSVGshape &shape = *shape_ptr; + if (!(shape.flags & NSVG_FLAGS_VISIBLE)) + continue; - Vec2f pd = p4 - p1; - Vec2f pd2 = p2 - p4; - float d2 = std::abs(pd2.x() * pd.y() - pd2.y() * pd.x()); - Vec2f pd3 = p3 - p4; - float d3 = std::abs(pd3.x() * pd.y() - pd3.y() * pd.x()); + bool is_fill_used = shape.fill.type != NSVG_PAINT_NONE; + bool is_stroke_used = + shape.stroke.type != NSVG_PAINT_NONE && + shape.strokeWidth > 1e-5f; + + if (!is_fill_used && !is_stroke_used) + continue; + + const LinesPath lines_path = linearize_path(shape.paths, param); + + if (is_fill_used) { + unsigned unique_id = static_cast(2 * shape_id); + HealedExPolygons expoly = fill_to_expolygons(lines_path, shape, param); + result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); + } + if (is_stroke_used) { + unsigned unique_id = static_cast(2 * shape_id + 1); + HealedExPolygons expoly = stroke_to_expolygons(lines_path, shape, param); + result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); + } + } + + // SVG is used as centered + // Do not disturb user by settings of pivot position + center(result); + return result; +} + +Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m) +{ + Polygons result; + for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) + continue; + if (shape->fill.type == NSVG_PAINT_NONE) + continue; + const LinesPath lines_path = linearize_path(shape->paths, param); + polygons_append(result, lines_path.polygons); + // close polyline to create polygon + polygons_append(result, to_polygons(lines_path.polylines)); + } + return result; +} + +void bounds(const NSVGimage &image, Vec2f& min, Vec2f &max) +{ + for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) + for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { + if (min.x() > path->bounds[0]) + min.x() = path->bounds[0]; + if (min.y() > path->bounds[1]) + min.y() = path->bounds[1]; + if (max.x() < path->bounds[2]) + max.x() = path->bounds[2]; + if (max.y() < path->bounds[3]) + max.y() = path->bounds[3]; + } +} + +NSVGimage_ptr nsvgParseFromFile(const std::string &filename, const char *units, float dpi) +{ + NSVGimage *image = ::nsvgParseFromFile(filename.c_str(), units, dpi); + return {image, &nsvgDelete}; +} + +std::unique_ptr read_from_disk(const std::string &path) +{ + boost::nowide::ifstream fs{path}; + if (!fs.is_open()) + return nullptr; + std::stringstream ss; + ss << fs.rdbuf(); + return std::make_unique(ss.str()); +} + +NSVGimage_ptr nsvgParse(const std::string& file_data, const char *units, float dpi){ + // NOTE: nsvg parser consume data from input(char *) + size_t size = file_data.size(); + // file data could be big, so it is allocated on heap + std::unique_ptr data_copy(new char[size+1]); + memcpy(data_copy.get(), file_data.c_str(), size); + data_copy[size] = '\0'; // data for nsvg must be null terminated + NSVGimage *image = ::nsvgParse(data_copy.get(), units, dpi); + return {image, &nsvgDelete}; +} + +NSVGimage *init_image(EmbossShape::SvgFile &svg_file){ + // is already initialized? + if (svg_file.image.get() != nullptr) + return svg_file.image.get(); + + if (svg_file.file_data == nullptr) { + // chech if path is known + if (svg_file.path.empty()) + return nullptr; + svg_file.file_data = read_from_disk(svg_file.path); + if (svg_file.file_data == nullptr) + return nullptr; + } + + // init svg image + svg_file.image = nsvgParse(*svg_file.file_data); + if (svg_file.image.get() == NULL) + return nullptr; + + return svg_file.image.get(); +} + +size_t get_shapes_count(const NSVGimage &image) +{ + size_t count = 0; + for (NSVGshape * s = image.shapes; s != NULL; s = s->next) + ++count; + return count; +} + +//void save(const NSVGimage &image, std::ostream &data) +//{ +// data << ""; +// +// // tl .. top left +// Vec2f tl(std::numeric_limits::max(), std::numeric_limits::max()); +// // br .. bottom right +// Vec2f br(std::numeric_limits::min(), std::numeric_limits::min()); +// bounds(image, tl, br); +// +// tl.x() = std::floor(tl.x()); +// tl.y() = std::floor(tl.y()); +// +// br.x() = std::ceil(br.x()); +// br.y() = std::ceil(br.y()); +// Vec2f s = br - tl; +// Point size = s.cast(); +// +// data << "\n"; +// data << "\n"; +// +// std::array buffer; +// auto write_point = [&tl, &buffer](std::string &d, const float *p) { +// float x = p[0] - tl.x(); +// float y = p[1] - tl.y(); +// auto to_string = [&buffer](float f) -> std::string { +// auto [ptr, ec] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), f); +// if (ec != std::errc{}) +// return "0"; +// return std::string(buffer.data(), ptr); +// }; +// d += to_string(x) + "," + to_string(y) + " "; +// }; +// +// for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { +// enum struct Type { move, line, curve, close }; // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d +// Type type = Type::move; +// std::string d = "M "; // move on start point +// for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { +// if (path->npts <= 1) +// continue; +// +// if (type == Type::close) { +// type = Type::move; +// // NOTE: After close must be a space +// d += " M "; // move on start point +// } +// write_point(d, path->pts); +// size_t path_size = static_cast(path->npts - 1); +// +// if (path->closed) { +// // Do not use last point in path it is duplicit +// if (path->npts <= 4) +// continue; +// path_size = static_cast(path->npts - 4); +// } +// +// for (size_t i = 0; i < path_size; i += 3) { +// const float *p = &path->pts[i * 2]; +// if (!::is_line(p)) { +// if (type != Type::curve) { +// type = Type::curve; +// d += "C "; // start sequence of triplets defining curves +// } +// write_point(d, &p[2]); +// write_point(d, &p[4]); +// } else { +// +// if (type != Type::line) { +// type = Type::line; +// d += "L "; // start sequence of line points +// } +// } +// write_point(d, &p[6]); +// } +// if (path->closed) { +// type = Type::close; +// d += "Z"; // start sequence of line points +// } +// } +// if (type != Type::close) { +// //type = Type::close; +// d += "Z"; // closed path +// } +// data << "\n"; +// } +// data << "\n"; +//} +// +//bool save(const NSVGimage &image, const std::string &svg_file_path) +//{ +// std::ofstream file{svg_file_path}; +// if (!file.is_open()) +// return false; +// save(image, file); +// return true; +//} +} // namespace Slic3r + +namespace { +using namespace Slic3r; // Polygon + Vec2f + +Point::coord_type to_coor(float val, double scale) { return static_cast(std::round(val * scale)); } + +bool need_flattening(float tessTol, const Vec2f &p1, const Vec2f &p2, const Vec2f &p3, const Vec2f &p4) { + // f .. first + // s .. second + auto det = [](const Vec2f &f, const Vec2f &s) { + return std::fabs(f.x() * s.y() - f.y() * s.x()); + }; + + Vec2f pd = (p4 - p1); + Vec2f pd2 = (p2 - p4); + float d2 = det(pd2, pd); + Vec2f pd3 = (p3 - p4); + float d3 = det(pd3, pd); float d23 = d2 + d3; - if ((d23 * d23) < tessTol * (pd.x() * pd.x() + pd.y() * pd.y())) { - polygon.points.emplace_back(p4.x(), p4.y()); + return (d23 * d23) >= tessTol * pd.squaredNorm(); +} + +// see function nsvg__lineTo(NSVGparser* p, float x, float y) +bool is_line(const float *p, float precision){ + //Vec2f p1(p[0], p[1]); + //Vec2f p2(p[2], p[3]); + //Vec2f p3(p[4], p[5]); + //Vec2f p4(p[6], p[7]); + float dx_3 = (p[6] - p[0]) / 3.f; + float dy_3 = (p[7] - p[1]) / 3.f; + + return + is_approx(p[2], p[0] + dx_3, precision) && + is_approx(p[4], p[6] - dx_3, precision) && + is_approx(p[3], p[1] + dy_3, precision) && + is_approx(p[5], p[7] - dy_3, precision); +} + +/// +/// Convert cubic curve to lines +/// Inspired by nanosvgrast.h function nsvgRasterize -> nsvg__flattenShape -> nsvg__flattenCubicBez +/// https://github.com/memononen/nanosvg/blob/f0a3e1034dd22e2e87e5db22401e44998383124e/src/nanosvgrast.h#L335 +/// +/// Result points +/// Tesselation tolerance +/// Curve point +/// Curve point +/// Curve point +/// Curve point +/// Actual depth of recursion +void flatten_cubic_bez(Points &points, float tessTol, const Vec2f& p1, const Vec2f& p2, const Vec2f& p3, const Vec2f& p4, int level) +{ + if (!need_flattening(tessTol, p1, p2, p3, p4)) { + Point::coord_type x = static_cast(std::round(p4.x())); + Point::coord_type y = static_cast(std::round(p4.y())); + points.emplace_back(x, y); return; } --level; - if (level == 0) return; + if (level == 0) + return; + + Vec2f p12 = (p1 + p2) * 0.5f; + Vec2f p23 = (p2 + p3) * 0.5f; + Vec2f p34 = (p3 + p4) * 0.5f; + Vec2f p123 = (p12 + p23) * 0.5f; Vec2f p234 = (p23 + p34) * 0.5f; Vec2f p1234 = (p123 + p234) * 0.5f; - flatten_cubic_bez(polygon, tessTol, p1, p12, p123, p1234, level); - flatten_cubic_bez(polygon, tessTol, p1234, p234, p34, p4, level); + flatten_cubic_bez(points, tessTol, p1, p12, p123, p1234, level); + flatten_cubic_bez(points, tessTol, p1234, p234, p34, p4, level); } -Polygons NSVGUtils::to_polygons(NSVGimage *image, float tessTol, int max_level) +LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m) { - Polygons polygons; - for (NSVGshape *shape = image->shapes; shape != NULL; - shape = shape->next) { - if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; - Slic3r::Polygon polygon; - if (shape->fill.type != NSVG_PAINT_NONE) { - for (NSVGpath *path = shape->paths; path != NULL; - path = path->next) { + LinesPath result; + Polygons &polygons = result.polygons; + Polylines &polylines = result.polylines; + + // multiple use of allocated memmory for points between paths + Points points; + for (NSVGpath *path = first_path; path != NULL; path = path->next) { // Flatten path - polygon.points.emplace_back(path->pts[0], path->pts[1]); - size_t path_size = (path->npts > 1) ? - static_cast(path->npts - 1) : 0; + Point::coord_type x = to_coor(path->pts[0], param.scale); + Point::coord_type y = to_coor(path->pts[1], param.scale); + points.emplace_back(x, y); + size_t path_size = (path->npts > 1) ? static_cast(path->npts - 1) : 0; for (size_t i = 0; i < path_size; i += 3) { - float *p = &path->pts[i * 2]; - Vec2f p1(p[0], p[1]), p2(p[2], p[3]), p3(p[4], p[5]), - p4(p[6], p[7]); - flatten_cubic_bez(polygon, tessTol, p1, p2, p3, p4, - max_level); + const float *p = &path->pts[i * 2]; + if (is_line(p)) { + // point p4 + Point::coord_type xx = to_coor(p[6], param.scale); + Point::coord_type yy = to_coor(p[7], param.scale); + points.emplace_back(xx, yy); + continue; + } + Vec2f p1(p[0], p[1]); + Vec2f p2(p[2], p[3]); + Vec2f p3(p[4], p[5]); + Vec2f p4(p[6], p[7]); + flatten_cubic_bez(points, param.tesselation_tolerance, + p1 * param.scale, p2 * param.scale, p3 * param.scale, p4 * param.scale, + param.max_level); + } + assert(!points.empty()); + if (points.empty()) + continue; + + if (param.is_y_negative) + for (Point &p : points) + p.y() = -p.y(); + + if (path->closed) { + polygons.emplace_back(points); + } else { + polylines.emplace_back(points); + } + // prepare for new path - recycle alocated memory + points.clear(); + } + remove_same_neighbor(polygons); + remove_same_neighbor(polylines); + return result; +} + +HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) +{ + Polygons fill = lines_path.polygons; // copy + + // close polyline to create polygon + polygons_append(fill, to_polygons(lines_path.polylines)); + if (fill.empty()) + return {}; + + // if (shape->fillRule == NSVGfillRule::NSVG_FILLRULE_NONZERO) + bool is_non_zero = true; + if (shape.fillRule == NSVGfillRule::NSVG_FILLRULE_EVENODD) + is_non_zero = false; + + return Emboss::heal_polygons(fill, is_non_zero, param.max_heal_iteration); +} + +struct DashesParam{ + // first dash length + float dash_length = 1.f; // scaled + + // is current dash .. true + // is current space .. false + bool is_line = true; + + // current index to array + unsigned char dash_index = 0; + static constexpr size_t max_dash_array_size = 8; // limitation of nanosvg strokeDashArray + std::array dash_array; // scaled + unsigned char dash_count = 0; // count of values in array + + explicit DashesParam(const NSVGshape &shape, double scale) : + dash_count(shape.strokeDashCount) + { + assert(dash_count > 0); + assert(dash_count <= max_dash_array_size); // limitation of nanosvg strokeDashArray + for (size_t i = 0; i < dash_count; ++i) + dash_array[i] = static_cast(shape.strokeDashArray[i] * scale); + + // Figure out dash offset. + float all_dash_length = 0; + for (unsigned char j = 0; j < dash_count; ++j) + all_dash_length += dash_array[j]; + + if (dash_count%2 == 1) // (shape.strokeDashCount & 1) + all_dash_length *= 2.0f; + + // Find location inside pattern + float dash_offset = fmodf(static_cast(shape.strokeDashOffset * scale), all_dash_length); + if (dash_offset < 0.0f) + dash_offset += all_dash_length; + + while (dash_offset > dash_array[dash_index]) { + dash_offset -= dash_array[dash_index]; + dash_index = (dash_index + 1) % shape.strokeDashCount; + is_line = !is_line; + } + + dash_length = dash_array[dash_index] - dash_offset; } - if (path->closed && !polygon.empty()) { - polygons.push_back(polygon); - polygon = Slic3r::Polygon(); +}; + +Polylines to_dashes(const Polyline &polyline, const DashesParam& param) +{ + Polylines dashes; + Polyline dash; // cache for one dash in dashed line + Point prev_point; + + bool is_line = param.is_line; + unsigned char dash_index = param.dash_index; + float dash_length = param.dash_length; // current rest of dash distance + for (const Point &point : polyline.points) { + if (&point == &polyline.points.front()) { + // is first point + prev_point = point; // copy + continue; } + Point diff = point - prev_point; + float line_segment_length = diff.cast().norm(); + while (dash_length < line_segment_length) { + // Calculate intermediate point + float d = dash_length / line_segment_length; + Point move_point = diff * d; + Point intermediate = prev_point + move_point; + + // add Dash in stroke + if (is_line) { + if (dash.empty()) { + dashes.emplace_back(Points{prev_point, intermediate}); + } else { + dash.append(prev_point); + dash.append(intermediate); + dashes.push_back(dash); + dash.clear(); } } - if (!polygon.empty()) - polygons.push_back(polygon); + diff -= move_point; + line_segment_length -= dash_length; + prev_point = intermediate; + + // Advance dash pattern + is_line = !is_line; + dash_index = (dash_index + 1) % param.dash_count; + dash_length = param.dash_array[dash_index]; } - return polygons; + if (is_line) + dash.append(prev_point); + dash_length -= line_segment_length; + prev_point = point; // copy } -ExPolygons NSVGUtils::to_ExPolygons(NSVGimage *image, - float tessTol, - int max_level) + // add last dash + if (is_line){ + assert(!dash.empty()); + dash.append(prev_point); // prev_point == polyline.points.back() + dashes.push_back(dash); + } + return dashes; +} + +HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) { - Polygons polygons = to_polygons(image, tessTol, max_level); + // convert stroke to polygon + ClipperLib::JoinType join_type = ClipperLib::JoinType::jtSquare; + switch (static_cast(shape.strokeLineJoin)) { + case NSVGlineJoin::NSVG_JOIN_BEVEL: join_type = ClipperLib::JoinType::jtSquare; break; + case NSVGlineJoin::NSVG_JOIN_MITER: join_type = ClipperLib::JoinType::jtMiter; break; + case NSVGlineJoin::NSVG_JOIN_ROUND: join_type = ClipperLib::JoinType::jtRound; break; + } - // Fix Y axis - for (Polygon &polygon : polygons) - for (Point &p : polygon.points) p.y() *= -1; + double mitter = shape.miterLimit * param.scale; + if (join_type == ClipperLib::JoinType::jtRound) { + // mitter is used as ArcTolerance + // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm + mitter = std::pow(param.tesselation_tolerance, 1/3.); + } + float stroke_width = static_cast(shape.strokeWidth * param.scale); - return Slic3r::union_ex(polygons); -} \ No newline at end of file + ClipperLib::EndType end_type = ClipperLib::EndType::etOpenButt; + switch (static_cast(shape.strokeLineCap)) { + case NSVGlineCap::NSVG_CAP_BUTT: end_type = ClipperLib::EndType::etOpenButt; break; + case NSVGlineCap::NSVG_CAP_ROUND: end_type = ClipperLib::EndType::etOpenRound; break; + case NSVGlineCap::NSVG_CAP_SQUARE: end_type = ClipperLib::EndType::etOpenSquare; break; + } + + Polygons result; + if (shape.strokeDashCount > 0) { + DashesParam params(shape, param.scale); + Polylines dashes; + for (const Polyline &polyline : lines_path.polylines) + polylines_append(dashes, to_dashes(polyline, params)); + for (const Polygon &polygon : lines_path.polygons) + polylines_append(dashes, to_dashes(to_polyline(polygon), params)); + result = offset(dashes, stroke_width / 2, join_type, mitter, end_type); + } else { + result = contour_to_polygons(lines_path.polygons, stroke_width, join_type, mitter); + polygons_append(result, offset(lines_path.polylines, stroke_width / 2, join_type, mitter, end_type)); + } + + bool is_non_zero = true; + return Emboss::heal_polygons(result, is_non_zero, param.max_heal_iteration); +} +} // namespace \ No newline at end of file diff --git a/src/libslic3r/NSVGUtils.hpp b/src/libslic3r/NSVGUtils.hpp index d82901f..2a97b8e 100644 --- a/src/libslic3r/NSVGUtils.hpp +++ b/src/libslic3r/NSVGUtils.hpp @@ -1,34 +1,80 @@ #ifndef slic3r_NSVGUtils_hpp_ #define slic3r_NSVGUtils_hpp_ +#include +#include +#include #include "Polygon.hpp" #include "ExPolygon.hpp" +#include "EmbossShape.hpp" // ExPolygonsWithIds #include "nanosvg/nanosvg.h" // load SVG file namespace Slic3r { -// Helper function to work with nano svg -class NSVGUtils +/// +/// Paramreters for conversion curve from SVG to lines in Polygon +/// +struct NSVGLineParams { -public: - NSVGUtils() = delete; + // Smaller will divide curve to more lines + // NOTE: Value is in image scale + double tesselation_tolerance = 10.f; - // inspired by nanosvgrast.h function nsvgRasterize->nsvg__flattenShape - static void flatten_cubic_bez(Polygon &polygon, - float tessTol, - Vec2f p1, - Vec2f p2, - Vec2f p3, - Vec2f p4, - int level); - // convert svg image to ExPolygons - static ExPolygons to_ExPolygons(NSVGimage *image, - float tessTol = 10., - int max_level = 10); - // convert svg paths to Polygons - static Polygons to_polygons(NSVGimage *image, - float tessTol = 10., - int max_level = 10); + // Maximal depth of recursion for conversion curve to lines + int max_level = 10; + + // Multiplicator of point coors + // NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer --> Slic3r::Point + double scale = 1. / SCALING_FACTOR; + + // Flag wether y is negative, when true than y coor is multiplied by -1 + bool is_y_negative = true; + + // Is used only with rounded Stroke + double arc_tolerance = 1.; + + // Maximal count of heal iteration + unsigned max_heal_iteration = 10; + + explicit NSVGLineParams(double tesselation_tolerance): + tesselation_tolerance(tesselation_tolerance), + arc_tolerance(std::pow(tesselation_tolerance, 1/3.)) + {} }; +/// +/// Convert .svg opened by nanoSvg to shapes stored in expolygons with ids +/// +/// Parsed svg file by NanoSvg +/// Smaller will divide curve to more lines +/// NOTE: Value is in image scale +/// Maximal depth for conversion curve to lines +/// Multiplicator of point coors +/// NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer +/// Shapes from svg image - fill + stroke +ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m); + +// help functions - prepare to be tested +/// Flag is y negative, when true than y coor is multiplied by -1 +Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m); + +void bounds(const NSVGimage &image, Vec2f &min, Vec2f &max); + +// read text data from file +std::unique_ptr read_from_disk(const std::string &path); + +using NSVGimage_ptr = std::unique_ptr; +NSVGimage_ptr nsvgParseFromFile(const std::string &svg_file_path, const char *units = "mm", float dpi = 96.0f); +NSVGimage_ptr nsvgParse(const std::string& file_data, const char *units = "mm", float dpi = 96.0f); +NSVGimage *init_image(EmbossShape::SvgFile &svg_file); + +/// +/// Iterate over shapes and calculate count +/// +/// Contain pointer to first shape +/// Count of shapes +size_t get_shapes_count(const NSVGimage &image); + +//void save(const NSVGimage &image, std::ostream &data); +//bool save(const NSVGimage &image, const std::string &svg_file_path); } // namespace Slic3r #endif // slic3r_NSVGUtils_hpp_ diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 255fe34..1f81dcc 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -119,20 +119,18 @@ ExtrusionMultiPath PerimeterGenerator::thick_polyline_to_multi_path(const ThickP const double w = fmax(line.a_width, line.b_width); const Flow new_flow = (role.is_bridge() && flow.bridge()) ? flow : flow.with_width(unscale(w) + flow.height() * float(1. - 0.25 * PI)); - if (path.polyline.points.empty()) { - path.polyline.append(line.a); - path.polyline.append(line.b); + if (path.empty()) { // Convert from spacing to extrusion width based on the extrusion model // of a square extrusion ended with semi circles. + path = { ExtrusionAttributes{ path.role(), new_flow } }; + path.polyline.append(line.a); + path.polyline.append(line.b); #ifdef SLIC3R_DEBUG printf(" filling %f gap\n", flow.width); #endif - path.mm3_per_mm = new_flow.mm3_per_mm(); - path.width = new_flow.width(); - path.height = new_flow.height(); } else { - assert(path.width >= EPSILON); - thickness_delta = scaled(fabs(path.width - new_flow.width())); + assert(path.width() >= EPSILON); + thickness_delta = scaled(fabs(path.width() - new_flow.width())); if (thickness_delta <= merge_tolerance) { // the width difference between this line and the current flow // (of the previous line) width is within the accepted tolerance @@ -325,10 +323,12 @@ static ExtrusionEntityCollection traverse_loops_classic(const PerimeterGenerator extrusion_paths_append( paths, intersection_pl({ polygon }, lower_slices_polygons_clipped), + ExtrusionAttributes{ role_normal, - is_external ? params.ext_mm3_per_mm : params.mm3_per_mm, + ExtrusionFlow{ is_external ? params.ext_mm3_per_mm : params.mm3_per_mm, is_external ? params.ext_perimeter_flow.width() : params.perimeter_flow.width(), - float(params.layer_height)); + float(params.layer_height) + } }); // get overhang paths by checking what parts of this loop fall // outside the grown lower slices (thus where the distance between @@ -336,21 +336,24 @@ static ExtrusionEntityCollection traverse_loops_classic(const PerimeterGenerator extrusion_paths_append( paths, diff_pl({ polygon }, lower_slices_polygons_clipped), + ExtrusionAttributes{ role_overhang, - params.mm3_per_mm_overhang, - params.overhang_flow.width(), - params.overhang_flow.height()); + ExtrusionFlow{ params.mm3_per_mm_overhang, params.overhang_flow.width(), params.overhang_flow.height() } + }); // Reapply the nearest point search for starting point. // We allow polyline reversal because Clipper may have randomly reversed polylines during clipping. chain_and_reorder_extrusion_paths(paths, &paths.front().first_point()); } else { - ExtrusionPath path(role_normal); - path.polyline = polygon.split_at_first_point(); - path.mm3_per_mm = is_external ? params.ext_mm3_per_mm : params.mm3_per_mm; - path.width = is_external ? params.ext_perimeter_flow.width() : params.perimeter_flow.width(); - path.height = float(params.layer_height); - paths.push_back(path); + paths.emplace_back(polygon.split_at_first_point(), + ExtrusionAttributes{ + role_normal, + ExtrusionFlow{ + is_external ? params.ext_mm3_per_mm : params.mm3_per_mm, + is_external ? params.ext_perimeter_flow.width() : params.perimeter_flow.width(), + float(params.layer_height) + } + }); } coll.append(ExtrusionLoop(std::move(paths), loop_role)); @@ -383,11 +386,13 @@ static ExtrusionEntityCollection traverse_loops_classic(const PerimeterGenerator ExtrusionLoop *eloop = static_cast(coll.entities[idx.first]); coll.entities[idx.first] = nullptr; if (loop.is_contour) { - eloop->make_counter_clockwise(); + if (eloop->is_clockwise()) + eloop->reverse_loop(); out.append(std::move(children.entities)); out.entities.emplace_back(eloop); } else { - eloop->make_clockwise(); + if (eloop->is_counter_clockwise()) + eloop->reverse_loop(); out.entities.emplace_back(eloop); out.append(std::move(children.entities)); } @@ -603,10 +608,8 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::P if (extrusion->is_closed) { ExtrusionLoop extrusion_loop(std::move(paths)); // Restore the orientation of the extrusion loop. - if (pg_extrusion.is_contour) - extrusion_loop.make_counter_clockwise(); - else - extrusion_loop.make_clockwise(); + if (pg_extrusion.is_contour == extrusion_loop.is_clockwise()) + extrusion_loop.reverse_loop(); for (auto it = std::next(extrusion_loop.paths.begin()); it != extrusion_loop.paths.end(); ++it) { assert(it->polyline.points.size() >= 2); @@ -960,11 +963,9 @@ std::tuple, Polygons> generate_extra_perimeters_over if (perimeter_polygon.empty()) { // fill possible gaps of single extrusion width Polygons shrinked = intersection(offset(prev, -0.3 * overhang_flow.scaled_spacing()), expanded_overhang_to_cover); - if (!shrinked.empty()) { + if (!shrinked.empty()) extrusion_paths_append(overhang_region, reconnect_polylines(perimeter, overhang_flow.scaled_spacing()), - ExtrusionRole::OverhangPerimeter, overhang_flow.mm3_per_mm(), overhang_flow.width(), - overhang_flow.height()); - } + ExtrusionAttributes{ ExtrusionRole::OverhangPerimeter, overhang_flow }); Polylines fills; ExPolygons gap = shrinked.empty() ? offset_ex(prev, overhang_flow.scaled_spacing() * 0.5) : to_expolygons(shrinked); @@ -975,14 +976,12 @@ std::tuple, Polygons> generate_extra_perimeters_over if (!fills.empty()) { fills = intersection_pl(fills, shrinked_overhang_to_cover); extrusion_paths_append(overhang_region, reconnect_polylines(fills, overhang_flow.scaled_spacing()), - ExtrusionRole::OverhangPerimeter, overhang_flow.mm3_per_mm(), overhang_flow.width(), - overhang_flow.height()); + ExtrusionAttributes{ ExtrusionRole::OverhangPerimeter, overhang_flow }); } break; } else { extrusion_paths_append(overhang_region, reconnect_polylines(perimeter, overhang_flow.scaled_spacing()), - ExtrusionRole::OverhangPerimeter, overhang_flow.mm3_per_mm(), overhang_flow.width(), - overhang_flow.height()); + ExtrusionAttributes{ExtrusionRole::OverhangPerimeter, overhang_flow }); } if (intersection(perimeter_polygon, real_overhang).empty()) { continuation_loops--; } diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index 4012642..6e81aa2 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -1643,6 +1643,7 @@ namespace client // Check whether the table X values are sorted. double x = expr_x.as_d(); + assert(! std::isnan(x)); bool evaluated = false; for (size_t i = 1; i < table.table.size(); ++i) { double x0 = table.table[i - 1].x; @@ -2098,7 +2099,7 @@ namespace client multiplicative_expression.name("multiplicative_expression"); assignment_statement = - variable_reference(_r1)[_a = _1] >> '=' > + (variable_reference(_r1)[_a = _1] >> '=') > ( // Consumes also '(' conditional_expression ')', that means enclosing an expression into braces makes it a single value vector initializer. initializer_list(_r1)[px::bind(&MyContext::vector_variable_assign_initializer_list, _r1, _a, _1)] // Process it before conditional_expression, as conditional_expression requires a vector reference to be augmented with an index. diff --git a/src/libslic3r/Point.cpp b/src/libslic3r/Point.cpp index 457bb44..eae6b50 100644 --- a/src/libslic3r/Point.cpp +++ b/src/libslic3r/Point.cpp @@ -47,14 +47,12 @@ Pointf3s transform(const Pointf3s& points, const Transform3d& t) void Point::rotate(double angle, const Point ¢er) { - double cur_x = (double)(*this)(0); - double cur_y = (double)(*this)(1); + Vec2d cur = this->cast(); double s = ::sin(angle); double c = ::cos(angle); - double dx = cur_x - (double)center(0); - double dy = cur_y - (double)center(1); - (*this)(0) = (coord_t)round( (double)center(0) + c * dx - s * dy ); - (*this)(1) = (coord_t)round( (double)center(1) + c * dy + s * dx ); + auto d = cur - center.cast(); + this->x() = fast_round_up(center.x() + c * d.x() - s * d.y()); + this->y() = fast_round_up(center.y() + s * d.x() + c * d.y()); } bool has_duplicate_points(Points &&pts) diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 542c9a9..3ffa3dd 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -152,6 +152,20 @@ Pointf3s transform(const Pointf3s& points, const Transform3d& t); /// Is positive determinant inline bool has_reflection(const Transform3d &transform) { return transform.matrix().determinant() < 0; } +/// +/// Getter on base of transformation matrix +/// +/// column index +/// source transformation +/// Base of transformation matrix +inline const Vec3d get_base(unsigned index, const Transform3d &transform) { return transform.linear().col(index); } +inline const Vec3d get_x_base(const Transform3d &transform) { return get_base(0, transform); } +inline const Vec3d get_y_base(const Transform3d &transform) { return get_base(1, transform); } +inline const Vec3d get_z_base(const Transform3d &transform) { return get_base(2, transform); } +inline const Vec3d get_base(unsigned index, const Transform3d::LinearPart &transform) { return transform.col(index); } +inline const Vec3d get_x_base(const Transform3d::LinearPart &transform) { return get_base(0, transform); } +inline const Vec3d get_y_base(const Transform3d::LinearPart &transform) { return get_base(1, transform); } +inline const Vec3d get_z_base(const Transform3d::LinearPart &transform) { return get_base(2, transform); } template using Vec = Eigen::Matrix; class Point : public Vec2crd @@ -166,11 +180,12 @@ public: Point(const Point &rhs) { *this = rhs; } explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(std::round(rhs.x())), coord_t(std::round(rhs.y()))) {} // This constructor allows you to construct Point from Eigen expressions + // This constructor has to be implicit (non-explicit) to allow implicit conversion from Eigen expressions. template Point(const Eigen::MatrixBase &other) : Vec2crd(other) {} static Point new_scale(coordf_t x, coordf_t y) { return Point(coord_t(scale_(x)), coord_t(scale_(y))); } - static Point new_scale(const Vec2d &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } - static Point new_scale(const Vec2f &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } + template + static Point new_scale(const Eigen::MatrixBase &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } // This method allows you to assign Eigen expressions to MyVectorType template @@ -547,6 +562,26 @@ inline coord_t align_to_grid(coord_t coord, coord_t spacing, coord_t base) inline Point align_to_grid(Point coord, Point spacing, Point base) { return Point(align_to_grid(coord.x(), spacing.x(), base.x()), align_to_grid(coord.y(), spacing.y(), base.y())); } +// MinMaxLimits +template struct MinMax { T min; T max;}; +template +static bool apply(std::optional &val, const MinMax &limit) { + if (!val.has_value()) return false; + return apply(*val, limit); +} +template +static bool apply(T &val, const MinMax &limit) +{ + if (val > limit.max) { + val = limit.max; + return true; + } + if (val < limit.min) { + val = limit.min; + return true; + } + return false; +} } // namespace Slic3r // start Boost diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 88ac1b0..30e9640 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -437,6 +437,37 @@ bool has_duplicate_points(const Polygons &polys) #endif } +bool remove_same_neighbor(Polygon &polygon) +{ + Points &points = polygon.points; + if (points.empty()) + return false; + auto last = std::unique(points.begin(), points.end()); + + // remove first and last neighbor duplication + if (const Point &last_point = *(last - 1); last_point == points.front()) { + --last; + } + + // no duplicits + if (last == points.end()) + return false; + + points.erase(last, points.end()); + return true; +} + +bool remove_same_neighbor(Polygons &polygons) +{ + if (polygons.empty()) + return false; + bool exist = false; + for (Polygon &polygon : polygons) + exist |= remove_same_neighbor(polygon); + // remove empty polygons + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.points.size() <= 2; }), polygons.end()); + return exist; +} static inline bool is_stick(const Point &p1, const Point &p2, const Point &p3) { Point v1 = p2 - p1; diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index e890a21..1fd74b0 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -109,6 +109,9 @@ inline bool has_duplicate_points(Polygon &&poly) { return has_duplicate_poi inline bool has_duplicate_points(const Polygon &poly) { return has_duplicate_points(poly.points); } bool has_duplicate_points(const Polygons &polys); +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polygon &polygon); +bool remove_same_neighbor(Polygons &polygons); inline double total_length(const Polygons &polylines) { double total = 0; for (Polygons::const_iterator it = polylines.begin(); it != polylines.end(); ++it) @@ -243,6 +246,17 @@ inline Polylines to_polylines(Polygons &&polys) return polylines; } +// close polyline to polygon (connect first and last point in polyline) +inline Polygons to_polygons(const Polylines &polylines) +{ + Polygons out; + out.reserve(polylines.size()); + for (const Polyline &polyline : polylines) { + if (polyline.size()) + out.emplace_back(polyline.points); + } + return out; +} inline Polygons to_polygons(const VecOfPoints &paths) { Polygons out; diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index 5261a8c..1c8b4ad 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -146,8 +146,10 @@ void Polyline::split_at(const Point &point, Polyline* p1, Polyline* p2) const } if (this->points.front() == point) { + //FIXME why is p1 NOT empty as in the case above? *p1 = { point }; *p2 = *this; + return; } auto min_dist2 = std::numeric_limits::max(); @@ -200,6 +202,31 @@ BoundingBox get_extents(const Polylines &polylines) return bb; } +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polyline &polyline) { + Points &points = polyline.points; + if (points.empty()) + return false; + auto last = std::unique(points.begin(), points.end()); + + // no duplicits + if (last == points.end()) + return false; + + points.erase(last, points.end()); + return true; +} + +bool remove_same_neighbor(Polylines &polylines){ + if (polylines.empty()) + return false; + bool exist = false; + for (Polyline &polyline : polylines) + exist |= remove_same_neighbor(polyline); + // remove empty polylines + polylines.erase(std::remove_if(polylines.begin(), polylines.end(), [](const Polyline &p) { return p.points.size() <= 1; }), polylines.end()); + return exist; +} const Point& leftmost_point(const Polylines &polylines) { if (polylines.empty()) diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 0e6dcff..5c9a25f 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -89,6 +89,9 @@ inline bool operator!=(const Polyline &lhs, const Polyline &rhs) { return lhs.po extern BoundingBox get_extents(const Polyline &polyline); extern BoundingBox get_extents(const Polylines &polylines); +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polyline &polyline); +bool remove_same_neighbor(Polylines &polylines); inline double total_length(const Polylines &polylines) { double total = 0; for (const Polyline &pl : polylines) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 915cfc4..7ea6353 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -36,6 +36,9 @@ #include "libslic3r.h" #include "Utils.hpp" #include "PlaceholderParser.hpp" +#include "GCode/Thumbnails.hpp" + +#include "PresetBundle.hpp" using boost::property_tree::ptree; @@ -186,29 +189,6 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem } else { BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Malformed variants field: `%2%`") % id % variants_field; } - //B19 - section.second.get("email", ""); - const auto emails_field = section.second.get("email", ""); - std::vector emails; - if (Slic3r::unescape_strings_cstyle(emails_field, emails)) { - for (const std::string &email_name : emails) { - if (model.email(email_name) == nullptr) - model.emails.emplace_back(VendorProfile::PrinterEmail(email_name)); - } - } else { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Malformed variants field: `%2%`") % id % emails_field; - } - section.second.get("skype", ""); - const auto skypes_field = section.second.get("skype", ""); - std::vector skypes; - if (Slic3r::unescape_strings_cstyle(skypes_field, skypes)) { - for (const std::string &skype_name : skypes) { - if (model.skype(skype_name) == nullptr) - model.skypes.emplace_back(VendorProfile::PrinterSkype(skype_name)); - } - } else { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: Malformed skypes field: `%2%`") % id % skypes_field; - } auto default_materials_field = section.second.get("default_materials", ""); if (default_materials_field.empty()) default_materials_field = section.second.get("default_filaments", ""); @@ -474,13 +454,15 @@ static std::vector s_Preset_print_options { "support_tree_angle", "support_tree_angle_slow", "support_tree_branch_diameter", "support_tree_branch_diameter_angle", "support_tree_branch_diameter_double_wall", "support_tree_top_rate", "support_tree_branch_distance", "support_tree_tip_diameter", "dont_support_bridges", "thick_bridges", "notes", "complete_objects", "extruder_clearance_radius", - "extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "perimeter_extruder", + "extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "gcode_binary","perimeter_extruder", "infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder", "ooze_prevention", "standby_temperature_delta", "interface_shells", "extrusion_width", "first_layer_extrusion_width", "perimeter_extrusion_width", "external_perimeter_extrusion_width", "infill_extrusion_width", "solid_infill_extrusion_width", "top_infill_extrusion_width", "support_material_extrusion_width", "infill_overlap", "infill_anchor", "infill_anchor_max", "bridge_flow_ratio", + "elefant_foot_compensation", "xy_size_compensation", "resolution", "gcode_resolution", "arc_fitting", + "wipe_tower", "wipe_tower_x", "wipe_tower_y", //w12 - "elefant_foot_compensation", "xy_size_compensation", "xy_contour_compensation", "xy_hole_compensation", "threads", "resolution", "gcode_resolution", "wipe_tower", "wipe_tower_x", "wipe_tower_y", + "elefant_foot_compensation", "xy_size_compensation", "xy_contour_compensation", "xy_hole_compensation", "resolution", "gcode_resolution", "wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_cone_angle", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "wipe_tower_bridging", "single_extruder_multi_material_priming", "mmu_segmented_region_max_width", "mmu_segmented_region_interlocking_depth", "wipe_tower_extruder", "wipe_tower_no_sparse_layers", "wipe_tower_extra_spacing", "compatible_printers", "compatible_printers_condition", "inherits", "perimeter_generator", "wall_transition_length", "wall_transition_filter_deviation", "wall_transition_angle", @@ -505,7 +487,8 @@ static std::vector s_Preset_filament_options { "overhang_fan_speed_0", "overhang_fan_speed_1", "overhang_fan_speed_2", "overhang_fan_speed_3", // Retract overrides "filament_retract_length", "filament_retract_lift", "filament_retract_lift_above", "filament_retract_lift_below", "filament_retract_speed", "filament_deretract_speed", "filament_retract_restart_extra", "filament_retract_before_travel", - "filament_retract_layer_change", "filament_wipe", "filament_retract_before_wipe", "filament_retract_length_toolchange", "filament_retract_restart_extra_toolchange", + "filament_retract_layer_change", "filament_wipe", "filament_retract_before_wipe", "filament_retract_length_toolchange", "filament_retract_restart_extra_toolchange", "filament_travel_ramping_lift", + "filament_travel_slope", "filament_travel_max_lift", "filament_travel_lift_before_obstacle", // Profile compatibility "filament_vendor", "compatible_prints", "compatible_prints_condition", "compatible_printers", "compatible_printers_condition", "inherits", //B15 @@ -545,8 +528,6 @@ static std::vector s_Preset_printer_options { "default_print_profile", "inherits", "remaining_times", "silent_mode", "machine_limits_usage", "thumbnails", "thumbnails_format", -//Y18 - "bed_exclude_area", //Y16 "chamber_temperature", "auxiliary_fan", "chamber_fan" }; @@ -805,7 +786,7 @@ static bool profile_print_params_same(const DynamicPrintConfig &cfg_old, const D // when comparing profiles for equality. Ignore them. for (const char *key : { "compatible_prints", "compatible_prints_condition", "compatible_printers", "compatible_printers_condition", "inherits", - "print_settings_id", "filament_settings_id", "sla_print_settings_id", "sla_material_settings_id", "printer_settings_id", + "print_settings_id", "filament_settings_id", "sla_print_settings_id", "sla_material_settings_id", "printer_settings_id", "filament_vendor", "printer_model", "printer_variant", "default_print_profile", "default_filament_profile", "default_sla_print_profile", "default_sla_material_profile", //FIXME remove the print host keys? "print_host", "printhost_apikey", "printhost_cafile" }) @@ -818,7 +799,7 @@ static bool profile_print_params_same(const DynamicPrintConfig &cfg_old, const D // and select it, losing previous modifications. // Only a single profile could be edited at at the same time, which introduces complexity when loading // filament profiles for multi-extruder printers. -std::pair PresetCollection::load_external_preset( +ExternalPreset PresetCollection::load_external_preset( // Path to the profile source file (a G-code, an AMF or 3MF file, a config file) const std::string &path, // Name of the profile, derived from the source file name. @@ -841,7 +822,7 @@ std::pair PresetCollection::load_external_preset( const Preset &edited = this->get_edited_preset(); if ((edited.name == original_name || edited.name == inherits) && profile_print_params_same(edited.config, cfg)) // Just point to that already selected and edited profile. - return std::make_pair(&(*this->find_preset_internal(edited.name)), false); + return ExternalPreset(&(*this->find_preset_internal(edited.name)), false); } // Is there a preset already loaded with the name stored inside the config? std::deque::iterator it = this->find_preset_internal(original_name); @@ -851,11 +832,11 @@ std::pair PresetCollection::load_external_preset( it = this->find_preset_renamed(original_name); found = it != m_presets.end(); } - if (found && profile_print_params_same(it->config, cfg)) { - // The preset exists and it matches the values stored inside config. + if (found && profile_print_params_same(it->config, cfg) && it->is_visible) { + // The preset exists and is visible and it matches the values stored inside config. if (select == LoadAndSelect::Always) this->select_preset(it - m_presets.begin()); - return std::make_pair(&(*it), false); + return ExternalPreset(&(*it), false); } if (! found && select != LoadAndSelect::Never && ! inherits.empty()) { // Try to use a system profile as a base to select the system profile @@ -867,19 +848,25 @@ std::pair PresetCollection::load_external_preset( // The system preset exists and it matches the values stored inside config. if (select == LoadAndSelect::Always) this->select_preset(it - m_presets.begin()); - return std::make_pair(&(*it), false); + return ExternalPreset(&(*it), false); } } if (found) { if (select != LoadAndSelect::Never) { + const size_t idx = it - m_presets.begin(); + // The newly selected preset can be activated AND have to be make as visible. + bool is_installed = !m_presets[idx].is_visible; // Select the existing preset and override it with new values, so that // the differences will be shown in the preset editor against the referenced profile. - this->select_preset(it - m_presets.begin()); + this->select_preset(idx); + + // update dirty state only if it's needed + if (!profile_print_params_same(it->config, cfg)) { // The source config may contain keys from many possible preset types. Just copy those that relate to this preset. // Following keys are not used neither by the UI nor by the slicing core, therefore they are not important - // Erase them from config appl to avoid redundant "dirty" parameter in loaded preset. - for (const char* key : { "print_settings_id", "filament_settings_id", "sla_print_settings_id", "sla_material_settings_id", "printer_settings_id", + // Erase them from config apply to avoid redundant "dirty" parameter in loaded preset. + for (const char* key : { "print_settings_id", "filament_settings_id", "sla_print_settings_id", "sla_material_settings_id", "printer_settings_id", "filament_vendor", "printer_model", "printer_variant", "default_print_profile", "default_filament_profile", "default_sla_print_profile", "default_sla_material_profile" }) keys.erase(std::remove(keys.begin(), keys.end(), key), keys.end()); @@ -888,7 +875,8 @@ std::pair PresetCollection::load_external_preset( // Don't save the newly loaded project as a "saved into project" state. //update_saved_preset_from_current_preset(); assert(this->get_edited_preset().is_dirty); - return std::make_pair(&(*it), this->get_edited_preset().is_dirty); + } + return ExternalPreset(&(*it), this->get_edited_preset().is_dirty, is_installed); } if (inherits.empty()) { // Update the "inherits" field. @@ -919,7 +907,7 @@ std::pair PresetCollection::load_external_preset( // The preset exists and it matches the values stored inside config. if (select == LoadAndSelect::Always) this->select_preset(it - m_presets.begin()); - return std::make_pair(&(*it), false); + return ExternalPreset(&(*it), false); } // Form another profile name. } @@ -929,7 +917,7 @@ std::pair PresetCollection::load_external_preset( if (this->m_idx_selected != size_t(-1) && &this->get_selected_preset() == &preset) this->get_edited_preset().is_external = true; - return std::make_pair(&preset, false); + return ExternalPreset(&preset, false); } Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select) @@ -1350,9 +1338,6 @@ static const std::set independent_from_extruder_number_options = { "filament_ramming_parameters", "gcode_substitutions", "post_process", - "thumbnails", -//Y18 - "bed_exclude_area", }; bool PresetCollection::is_independent_from_extruder_number_option(const std::string& opt_key) @@ -1376,6 +1361,15 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi } else if (opt_key == "default_filament_profile") { // Ignore this field, it is not presented to the user, therefore showing a "modified" flag for this parameter does not help. // Also the length of this field may differ, which may lead to a crash if the block below is used. + } else if (opt_key == "thumbnails") { + // "thumbnails" can not contain extensions in old config but they are valid and use PNG extension by default + // So, check if "thumbnails" is really changed + // We will compare full thumbnails instead of exactly config values + auto [thumbnails, er] = GCodeThumbnails::make_and_check_thumbnail_list(config_this); + auto [thumbnails_new, er_new] = GCodeThumbnails::make_and_check_thumbnail_list(config_other); + if (thumbnails != thumbnails_new || er != er_new) + // if those strings are actually the same, erase them from the list of dirty oprions + diff.emplace_back(opt_key); } else { switch (other_opt->type()) { case coInts: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; @@ -1387,6 +1381,12 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi case coFloatsOrPercents: add_correct_opts_to_diff(opt_key, diff, config_other, config_this); break; default: diff.emplace_back(opt_key); break; } + // "nozzle_diameter" is a vector option which contain info about diameter for each nozzle + // But in the same time size of this vector indicates about count of extruders, + // So, we need to add it to the diff if its size is changed. + if (opt_key == "nozzle_diameter" && + static_cast(this_opt)->size() != static_cast(other_opt)->size()) + diff.emplace_back(opt_key); } } } @@ -1399,7 +1399,14 @@ bool PresetCollection::is_dirty(const Preset *edited, const Preset *reference) { if (edited != nullptr && reference != nullptr) { // Only compares options existing in both configs. - if (! reference->config.equals(edited->config)) + bool is_dirty = !reference->config.equals(edited->config); + if (is_dirty && edited->type != Preset::TYPE_FILAMENT) { + // for non-filaments preset check deep difference for compared configs + // there can be cases (as for thumbnails), when configs can logically equal + // even when their values are not equal. + is_dirty = !deep_diff(edited->config, reference->config).empty(); + } + if (is_dirty) return true; // The "compatible_printers" option key is handled differently from the others: // It is not mandatory. If the key is missing, it means it is compatible with any printer. @@ -1810,7 +1817,8 @@ std::string PhysicalPrinter::get_preset_name(std::string name) // *** PhysicalPrinterCollection *** // ----------------------------------- -PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector& keys) +PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector& keys, PresetBundle* preset_bundle) + :m_preset_bundle_owner(preset_bundle) { // Default config for a physical printer containing all key/value pairs of PhysicalPrinter::printer_options(). for (const std::string &key : keys) { @@ -1821,6 +1829,27 @@ PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector& preset_names, const PrinterPresetCollection& printer_presets) +{ + std::set new_names; + + bool was_renamed{ false }; + for (const std::string& preset_name : preset_names) { + const Preset* preset = printer_presets.find_preset(preset_name); + if (preset) + new_names.emplace(preset_name); + else { + if (const std::string* new_name = printer_presets.get_preset_name_renamed(preset_name)) { + BOOST_LOG_TRIVIAL(warning) << "Printer preset present " << preset_name << "was renamed to: " << *new_name << std::endl; + new_names.emplace(*new_name); + was_renamed = true; + } + } + } + + if (was_renamed) + preset_names = new_names; +} // Load all printers found in dir_path. // Throws an exception on error. void PhysicalPrinterCollection::load_printers( @@ -1839,10 +1868,13 @@ void PhysicalPrinterCollection::load_printers( std::string name = dir_entry.path().filename().string(); // Remove the .ini suffix. name.erase(name.size() - 4); - if (this->find_printer(name, false)) { + if (PhysicalPrinter* found_printer = this->find_printer(name, false)) { // This happens when there's is a preset (most likely legacy one) with the same name as a system preset // that's already been loaded from a bundle. BOOST_LOG_TRIVIAL(warning) << "Printer already present, not loading: " << name; + // But some of used printer_preset might have been renamed. + // Check it and replace with new name(s) if it's needed + update_preset_names_if_were_renamed(found_printer->preset_names, m_preset_bundle_owner->printers); continue; } try { @@ -1856,6 +1888,9 @@ void PhysicalPrinterCollection::load_printers( substitutions.push_back({ name, Preset::TYPE_PHYSICAL_PRINTER, PresetConfigSubstitutions::Source::UserFile, printer.file, std::move(config_substitutions) }); printer.update_from_config(config); printer.loaded = true; + // Some of used printer_preset might have been renamed. + // Check it and replace with new name(s) if it's needed + update_preset_names_if_were_renamed(printer.preset_names, m_preset_bundle_owner->printers); } catch (const std::ifstream::failure& err) { throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + printer.file + "\n\tReason: " + err.what()); @@ -1887,6 +1922,9 @@ void PhysicalPrinterCollection::load_printer(const std::string& path, const std: it->file = path; it->config = std::move(config); it->loaded = true; + // Some of used printer_preset might have been renamed. + // Check it and replace with new name(s) if it's needed + update_preset_names_if_were_renamed(it->preset_names, m_preset_bundle_owner->printers); if (select) this->select_printer(*it); @@ -2142,6 +2180,9 @@ void PhysicalPrinterCollection::select_printer(const std::string& full_name) m_selected_preset = *it->preset_names.begin(); else m_selected_preset = it->get_preset_name(full_name); + // Check if selected preset wasn't renamed and replace it with new name + if (const std::string* new_name = m_preset_bundle_owner->printers.get_preset_name_renamed(m_selected_preset)) + m_selected_preset = *new_name; } void PhysicalPrinterCollection::select_printer(const std::string& printer_name, const std::string& preset_name) @@ -2361,6 +2402,15 @@ namespace PresetUtils { } return true; } + bool compare_vendor_profile_printers(const VendorProfile& vp_old, const VendorProfile& vp_new, std::vector& new_printers) + { + for (const VendorProfile::PrinterModel& model : vp_new.models) + { + if (std::find_if(vp_old.models.begin(), vp_old.models.end(), [model](const VendorProfile::PrinterModel& pm) { return pm.id == model.id; }) == vp_old.models.end()) + new_printers.push_back(model.name); + } + return new_printers.empty(); + } } // namespace PresetUtils } // namespace Slic3r diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index bcde1cf..83e4189 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -41,19 +41,6 @@ public: PrinterVariant(const std::string &name) : name(name) {} std::string name; }; - //B19 - struct PrinterEmail - { - PrinterEmail() {} - PrinterEmail(const std::string &name) : name(name) {} - std::string name; - }; - struct PrinterSkype - { - PrinterSkype() {} - PrinterSkype(const std::string &name) : name(name) {} - std::string name; - }; struct PrinterModel { PrinterModel() {} @@ -62,9 +49,6 @@ public: PrinterTechnology technology; std::string family; std::vector variants; - //B19 - std::vector emails; - std::vector skypes; std::vector default_materials; // Vendor & Printer Model specific print bed model & texture. std::string bed_model; @@ -77,23 +61,7 @@ public: return &v; return nullptr; } - //B19 - PrinterEmail* email(const std::string &name) { - for (auto &v : this->emails) - if (v.name == name) - return &v; - return nullptr; - } - PrinterSkype* skype(const std::string &name) { - for (auto &v : this->skypes) - if (v.name == name) - return &v; - return nullptr; - } - //B19 const PrinterVariant* variant(const std::string &name) const { return const_cast(this)->variant(name); } - const PrinterEmail* email(const std::string &name) const { return const_cast(this)->email(name); } - const PrinterSkype* skype(const std::string &name) const { return const_cast(this)->skype(name); } }; std::vector models; @@ -109,10 +77,7 @@ public: // If `load_all` is false, only the header with basic info (name, version, URLs) is loaded. static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true); static VendorProfile from_ini(const boost::property_tree::ptree &tree, const boost::filesystem::path &path, bool load_all=true); - //B19 size_t num_variants() const { size_t n = 0; for (auto &model : models) n += model.variants.size(); return n; } - size_t num_emails() const { size_t n = 0; for (auto &model : models) n += model.emails.size(); return n; } - size_t num_skypes() const { size_t n = 0; for (auto &model : models) n += model.skypes.size(); return n; } std::vector families() const; bool operator< (const VendorProfile &rhs) const { return this->id < rhs.id; } @@ -308,6 +273,20 @@ struct PresetConfigSubstitutions { ConfigSubstitutions substitutions; }; +// This struct is used to get an information about loaded external preset +struct ExternalPreset +{ + Preset* preset { nullptr }; + bool is_modified { false }; + bool is_installed { false }; + + ExternalPreset(Preset* preset, bool modified, bool installed = false) + : preset(preset) + , is_modified(modified) + , is_installed(installed) {} + + virtual ~ExternalPreset() = default; +}; // Substitutions having been performed during parsing a set of configuration files, for example when starting up // QIDISlicer and reading the user Print / Filament / Printer profiles. using PresetsConfigSubstitutions = std::vector; @@ -359,7 +338,7 @@ public: // Select a profile only if it was modified. OnlyIfModified, }; - std::pair load_external_preset( + ExternalPreset load_external_preset( // Path to the profile source file (a G-code, an AMF or 3MF file, a config file) const std::string &path, // Name of the profile, derived from the source file name. @@ -640,10 +619,7 @@ public: PresetCollection(type, keys, defaults, default_name) {} const Preset& default_preset_for(const DynamicPrintConfig &config) const override; - //B19 const Preset* find_system_preset_by_model_and_variant(const std::string &model_id, const std::string &variant) const; - const Preset* find_system_preset_by_model_and_email(const std::string &model_id, const std::string &email) const; - const Preset* find_system_preset_by_model_and_skype(const std::string &model_id, const std::string &skype) const; bool only_default_printers() const; private: @@ -660,6 +636,7 @@ namespace PresetUtils { std::string system_printer_bed_model(const Preset& preset); std::string system_printer_bed_texture(const Preset& preset); bool vendor_profile_has_all_resources(const VendorProfile& vp); + bool compare_vendor_profile_printers(const VendorProfile& vp_old, const VendorProfile& vp_new, std::vector& new_printers); } // namespace PresetUtils @@ -741,7 +718,7 @@ protected: class PhysicalPrinterCollection { public: - PhysicalPrinterCollection(const std::vector& keys); + PhysicalPrinterCollection(const std::vector& keys, PresetBundle* preset_bundle); typedef std::deque::iterator Iterator; typedef std::deque::const_iterator ConstIterator; @@ -862,6 +839,7 @@ private: // Path to the directory to store the config files into. std::string m_dir_path; + const PresetBundle* m_preset_bundle_owner{ nullptr }; }; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index a69dd42..185fc86 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -22,6 +22,7 @@ #include #include +#include // Store the print/filament/printer presets into a "presets" subdirectory of the Slic3rPE config dir. // This breaks compatibility with the upstream Slic3r if the --datadir is used to switch between the two versions. @@ -43,7 +44,7 @@ PresetBundle::PresetBundle() : sla_materials(Preset::TYPE_SLA_MATERIAL, Preset::sla_material_options(), static_cast(SLAFullPrintConfig::defaults())), sla_prints(Preset::TYPE_SLA_PRINT, Preset::sla_print_options(), static_cast(SLAFullPrintConfig::defaults())), printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast(FullPrintConfig::defaults()), "- default FFF -"), - physical_printers(PhysicalPrinter::printer_options()) + physical_printers(PhysicalPrinter::printer_options(), this) { // The following keys are handled by the UI, they do not have a counterpart in any StaticPrintConfig derived classes, // therefore they need to be handled differently. As they have no counterpart in StaticPrintConfig, they are not being @@ -401,6 +402,11 @@ void PresetBundle::update_system_maps() this->sla_materials.update_map_system_profile_renamed(); this->printers .update_map_system_profile_renamed(); + update_alias_maps(); +} + +void PresetBundle::update_alias_maps() +{ this->prints .update_map_alias_to_profile_name(); this->sla_prints .update_map_alias_to_profile_name(); this->filaments .update_map_alias_to_profile_name(); @@ -650,10 +656,11 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p auto printer_technology = printers.get_selected_preset().printer_technology(); if (printer_technology == ptFFF && ! preferred_selection.filament.empty()) { const std::string& preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_FILAMENT, preferred_selection.filament, 0); - if (auto it = filaments.find_preset_internal(preferred_preset_name); - it != filaments.end() && it->is_visible && it->is_compatible) { + ExtruderFilaments& extruder_frst = this->extruders_filaments.front(); + if (auto it = extruder_frst.find_filament_internal(preferred_preset_name); + it != extruder_frst.end() && it->preset->is_visible && it->is_compatible) { + if (extruder_frst.select_filament(preferred_preset_name)) filaments.select_preset_by_name_strict(preferred_preset_name); - this->extruders_filaments.front().select_filament(filaments.get_selected_preset_name()); } } else if (printer_technology == ptSLA && ! preferred_selection.sla_material.empty()) { const std::string& preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_SLA_MATERIAL, preferred_selection.sla_material); @@ -878,9 +885,16 @@ DynamicPrintConfig PresetBundle::full_sla_config() const ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule) { if (is_gcode_file(path)) { + FILE* file = boost::nowide::fopen(path.c_str(), "rb"); + if (file == nullptr) + throw Slic3r::RuntimeError(format("Error opening file %1%", path)); + std::vector cs_buffer(65536); + const bool is_binary = bgcode::core::is_valid_binary_gcode(*file, true, cs_buffer.data(), cs_buffer.size()) == bgcode::core::EResult::Success; + fclose(file); DynamicPrintConfig config; config.apply(FullPrintConfig::defaults()); - ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, compatibility_rule); + ConfigSubstitutions config_substitutions = is_binary ? config.load_from_binary_gcode_file(path, compatibility_rule) : + config.load_from_gcode_file(path, compatibility_rule); Preset::normalize(config); load_config_file_config(path, true, std::move(config)); return config_substitutions; @@ -937,6 +951,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool { PrinterTechnology printer_technology = Preset::printer_technology(config); + tmp_installed_presets.clear(); // The "compatible_printers" field should not have been exported into a config.ini or a G-code anyway, // but some of the alpha versions of Slic3r did. { @@ -985,19 +1000,23 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // 2) If the loading succeeded, split and load the config into print / filament / printer settings. // First load the print and printer presets. + std::set& tmp_installed_presets_ref = tmp_installed_presets; auto load_preset = [&config, &inherits, &inherits_values, &compatible_printers_condition, &compatible_printers_condition_values, &compatible_prints_condition, &compatible_prints_condition_values, - is_external, &name, &name_or_path] + is_external, &name, &name_or_path, &tmp_installed_presets_ref] (PresetCollection &presets, size_t idx, const std::string &key) { // Split the "compatible_printers_condition" and "inherits" values one by one from a single vector to the print & printer profiles. inherits = inherits_values[idx]; compatible_printers_condition = compatible_printers_condition_values[idx]; if (idx > 0 && idx - 1 < compatible_prints_condition_values.size()) compatible_prints_condition = compatible_prints_condition_values[idx - 1]; - if (is_external) - presets.load_external_preset(name_or_path, name, config.opt_string(key, true), config); + if (is_external) { + ExternalPreset ext_preset = presets.load_external_preset(name_or_path, name, config.opt_string(key, true), config); + if (ext_preset.is_installed) + tmp_installed_presets_ref.emplace(ext_preset.preset->name); + } else presets.load_preset(presets.path_from_name(name), name, config).save(); }; @@ -1019,8 +1038,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool compatible_printers_condition = compatible_printers_condition_values[1]; compatible_prints_condition = compatible_prints_condition_values.front(); Preset *loaded = nullptr; - if (is_external) - loaded = this->filaments.load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config).first; + if (is_external) { + ExternalPreset ext_preset = this->filaments.load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config); + loaded = ext_preset.preset; + if (ext_preset.is_installed) + tmp_installed_presets.emplace(ext_preset.preset->name); + } else { // called from Config Wizard. loaded= &this->filaments.load_preset(this->filaments.path_from_name(name), name, config); @@ -1054,13 +1077,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool cfg.opt_string("compatible_prints_condition", true) = compatible_prints_condition_values[i]; cfg.opt_string("inherits", true) = inherits_values[i + 1]; // Load all filament presets, but only select the first one in the preset dialog. - auto [loaded, modified] = this->filaments.load_external_preset(name_or_path, name, + auto [loaded, modified, installed] = this->filaments.load_external_preset(name_or_path, name, (i < int(old_filament_profile_names->values.size())) ? old_filament_profile_names->values[i] : "", std::move(cfg), any_modified ? PresetCollection::LoadAndSelect::Never : PresetCollection::LoadAndSelect::OnlyIfModified); any_modified |= modified; extr_names[i] = loaded->name; + if (installed) + tmp_installed_presets.emplace(loaded->name); } // Check if some preset was selected after loading from config file. @@ -1100,6 +1125,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool else this->physical_printers.unselect_printer(); } + update_alias_maps(); } // Load the active configuration of a config bundle from a boost property_tree. This is a private method called from load_config_file. @@ -1178,6 +1204,7 @@ ConfigSubstitutions PresetBundle::load_config_file_config_bundle( this->extruders_filaments[i].select_filament(load_one(this->filaments, tmp_bundle.filaments, tmp_bundle.extruders_filaments[i].get_selected_preset_name(), false)); this->update_compatible(PresetSelectCompatibleType::Never); + update_alias_maps(); sort_remove_duplicates(config_substitutions); return config_substitutions; @@ -1667,6 +1694,7 @@ std::pair PresetBundle::load_configbundle( this->update_compatible(PresetSelectCompatibleType::Never); } + update_alias_maps(); return std::make_pair(std::move(substitutions), presets_loaded + ph_printers_loaded); } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 4387b54..fc38ecf 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -78,6 +78,7 @@ public: }; ObsoletePresets obsolete_presets; + std::set tmp_installed_presets; bool has_defauls_only() const { return prints.has_defaults_only() && filaments.has_defaults_only() && printers.has_defaults_only(); } @@ -174,6 +175,8 @@ private: std::vector merge_presets(PresetBundle &&other); // Update renamed_from and alias maps of system profiles. void update_system_maps(); + // Update alias maps + void update_alias_maps(); // Set the is_visible flag for filaments and sla materials, // apply defaults based on enabled printers when no filaments/materials are installed. diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 211a6d0..9502adf 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -60,8 +60,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "autoemit_temperature_commands", "avoid_crossing_perimeters", "avoid_crossing_perimeters_max_detour", - //Y18 - "bed_exclude_area", "bed_shape", "bed_temperature", "before_layer_gcode", @@ -123,7 +121,13 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "perimeter_acceleration", "post_process", "gcode_substitutions", + "gcode_binary", "printer_notes", + "travel_ramping_lift", + "travel_initial_part_length", + "travel_slope", + "travel_max_lift", + "travel_lift_before_obstacle", "retract_before_travel", "retract_before_wipe", "retract_layer_change", @@ -420,13 +424,16 @@ bool Print::sequential_print_horizontal_clearance_valid(const Print& print, Poly // appropriate object distance. Even if I set this to jtMiter the warning still shows up. Geometry::Transformation trafo = model_instance0->get_transformation(); trafo.set_offset({ 0.0, 0.0, model_instance0->get_offset().z() }); - it_convex_hull = map_model_object_to_convex_hull.emplace_hint(it_convex_hull, model_object_id, - offset(print_object->model_object()->convex_hull_2d(trafo.get_matrix()), + Polygon ch2d = print_object->model_object()->convex_hull_2d(trafo.get_matrix()); + Polygons offs_ch2d = offset(ch2d, // Shrink the extruder_clearance_radius a tiny bit, so that if the object arrangement algorithm placed the objects // exactly by satisfying the extruder_clearance_radius, this test will not trigger collision. - float(scale_(0.5 * print.config().extruder_clearance_radius.value - BuildVolume::BedEpsilon)), - jtRound, scale_(0.1)).front()); + float(scale_(0.5 * print.config().extruder_clearance_radius.value - BuildVolume::BedEpsilon)), jtRound, scale_(0.1)); + // for invalid geometries the vector returned by offset() may be empty + if (!offs_ch2d.empty()) + it_convex_hull = map_model_object_to_convex_hull.emplace_hint(it_convex_hull, model_object_id, offs_ch2d.front()); } + if (it_convex_hull != map_model_object_to_convex_hull.end()) { // Make a copy, so it may be rotated for instances. Polygon convex_hull0 = it_convex_hull->second; const double z_diff = Geometry::rotation_diff_z(model_instance0->get_matrix(), print_object->instances().front().model_instance->get_matrix()); @@ -452,6 +459,7 @@ bool Print::sequential_print_horizontal_clearance_valid(const Print& print, Poly convex_hulls_other.emplace_back(std::move(convex_hull)); } } + } if (!intersecting_idxs.empty()) { // use collected indices (inside convex_hulls_other) to update output @@ -942,8 +950,10 @@ void Print::process() tbb::parallel_for(tbb::blocked_range(0, m_objects.size(), 1), [this](const tbb::blocked_range &range) { for (size_t idx = range.begin(); idx < range.end(); ++idx) { - m_objects[idx]->generate_support_material(); - m_objects[idx]->estimate_curled_extrusions(); + PrintObject &obj = *m_objects[idx]; + obj.generate_support_material(); + obj.estimate_curled_extrusions(); + obj.calculate_overhanging_perimeters(); } }, tbb::simple_partitioner()); @@ -996,12 +1006,13 @@ void Print::process() this->set_done(psSkirtBrim); } - std::optional wipe_tower_opt = {}; if (this->has_wipe_tower()) { - m_fake_wipe_tower.set_pos_and_rotation({ m_config.wipe_tower_x, m_config.wipe_tower_y }, m_config.wipe_tower_rotation_angle); - wipe_tower_opt = std::make_optional(&m_fake_wipe_tower); + // These values have to be updated here, not during wipe tower generation. + // When the wipe tower is moved/rotated, it is not regenerated. + m_wipe_tower_data.position = { m_config.wipe_tower_x, m_config.wipe_tower_y }; + m_wipe_tower_data.rotation_angle = m_config.wipe_tower_rotation_angle; } - auto conflictRes = ConflictChecker::find_inter_of_lines_in_diff_objs(m_objects, wipe_tower_opt); + auto conflictRes = ConflictChecker::find_inter_of_lines_in_diff_objs(objects(), m_wipe_tower_data); m_conflict_result = conflictRes; if (conflictRes.has_value()) @@ -1030,7 +1041,7 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor this->set_status(90, message); // Create GCode on heap, it has quite a lot of data. - std::unique_ptr gcode(new GCode); + std::unique_ptr gcode(new GCodeGenerator); gcode->do_export(this, path.c_str(), result, thumbnail_cb); if (m_conflict_result.has_value()) @@ -1146,13 +1157,15 @@ void Print::_make_skirt() } // Extrude the skirt loop. ExtrusionLoop eloop(elrSkirt); - eloop.paths.emplace_back(ExtrusionPath( - ExtrusionPath( + eloop.paths.emplace_back( + ExtrusionAttributes{ ExtrusionRole::Skirt, - (float)mm3_per_mm, // this will be overridden at G-code export time + ExtrusionFlow{ + float(mm3_per_mm), // this will be overridden at G-code export time flow.width(), - (float)first_layer_height // this will be overridden at G-code export time - ))); + float(first_layer_height) // this will be overridden at G-code export time + } + }); eloop.paths.back().polyline = loop.split_at_first_point(); m_skirt.append(eloop); if (m_config.min_skirt_length.value > 0) { @@ -1559,9 +1572,9 @@ void Print::_make_wipe_tower() m_wipe_tower_data.used_filament = wipe_tower.get_used_filament(); m_wipe_tower_data.number_of_toolchanges = wipe_tower.get_number_of_toolchanges(); - const Vec3d origin = Vec3d::Zero(); - m_fake_wipe_tower.set_fake_extrusion_data(wipe_tower.position(), wipe_tower.width(), wipe_tower.get_wipe_tower_height(), config().first_layer_height, m_wipe_tower_data.depth, - m_wipe_tower_data.z_and_depth_pairs, m_wipe_tower_data.brim_width, config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle, {scale_(origin.x()), scale_(origin.y())}); + m_wipe_tower_data.width = wipe_tower.width(); + m_wipe_tower_data.first_layer_height = config().first_layer_height; + m_wipe_tower_data.cone_angle = config().wipe_tower_cone_angle; } @@ -1578,6 +1591,25 @@ std::string Print::output_filename(const std::string &filename_base) const return this->PrintBase::output_filename(m_config.output_filename_format.value, ".gcode", filename_base, &config); } +const std::string PrintStatistics::FilamentUsedG = "filament used [g]"; +const std::string PrintStatistics::FilamentUsedGMask = "; filament used [g] ="; + +const std::string PrintStatistics::TotalFilamentUsedG = "total filament used [g]"; +const std::string PrintStatistics::TotalFilamentUsedGMask = "; total filament used [g] ="; +const std::string PrintStatistics::TotalFilamentUsedGValueMask = "; total filament used [g] = %.2lf\n"; + +const std::string PrintStatistics::FilamentUsedCm3 = "filament used [cm3]"; +const std::string PrintStatistics::FilamentUsedCm3Mask = "; filament used [cm3] ="; + +const std::string PrintStatistics::FilamentUsedMm = "filament used [mm]"; +const std::string PrintStatistics::FilamentUsedMmMask = "; filament used [mm] ="; + +const std::string PrintStatistics::FilamentCost = "filament cost"; +const std::string PrintStatistics::FilamentCostMask = "; filament cost ="; + +const std::string PrintStatistics::TotalFilamentCost = "total filament cost"; +const std::string PrintStatistics::TotalFilamentCostMask = "; total filament cost ="; +const std::string PrintStatistics::TotalFilamentCostValueMask = "; total filament cost = %.2lf\n"; DynamicConfig PrintStatistics::config() const { DynamicConfig config; @@ -1631,80 +1663,5 @@ std::string PrintStatistics::finalize_output_path(const std::string &path_in) co return final_path; } - std::vector FakeWipeTower::getFakeExtrusionPathsFromWipeTower() const - { - float h = height; - float lh = layer_height; - int d = scale_(depth); - int w = scale_(width); - int bd = scale_(brim_width); - Point minCorner = { -bd, -bd }; - Point maxCorner = { minCorner.x() + w + bd, minCorner.y() + d + bd }; - - const auto [cone_base_R, cone_scale_x] = WipeTower::get_wipe_tower_cone_base(width, height, depth, cone_angle); - - std::vector paths; - for (float hh = 0.f; hh < h; hh += lh) { - - if (hh != 0.f) { - // The wipe tower may be getting smaller. Find the depth for this layer. - size_t i = 0; - for (i=0; i= z_and_depth_pairs[i].first && hh < z_and_depth_pairs[i+1].first) - break; - d = scale_(z_and_depth_pairs[i].second); - minCorner = {0.f, -d/2 + scale_(z_and_depth_pairs.front().second/2.f)}; - maxCorner = { minCorner.x() + w, minCorner.y() + d }; - } - - - ExtrusionPath path(ExtrusionRole::WipeTower, 0.0, 0.0, lh); - path.polyline = { minCorner, {maxCorner.x(), minCorner.y()}, maxCorner, {minCorner.x(), maxCorner.y()}, minCorner }; - paths.push_back({ path }); - - // We added the border, now add several parallel lines so we can detect an object that is fully inside the tower. - // For now, simply use fixed spacing of 3mm. - for (coord_t y=minCorner.y()+scale_(3.); y 0.) { - path.polyline.clear(); - double r = cone_base_R * (1 - hh/height); - for (double alpha=0; alpha<2.01*M_PI; alpha+=2*M_PI/20.) - path.polyline.points.emplace_back(Point::new_scale(width/2. + r * std::cos(alpha)/cone_scale_x, depth/2. + r * std::sin(alpha))); - paths.back().emplace_back(path); - if (hh == 0.f) { // Cone brim. - for (float bw=brim_width; bw>0.f; bw-=3.f) { - path.polyline.clear(); - for (double alpha=0; alpha<2.01*M_PI; alpha+=2*M_PI/20.) // see load_wipe_tower_preview, where the same is a bit clearer - path.polyline.points.emplace_back(Point::new_scale( - width/2. + cone_base_R * std::cos(alpha)/cone_scale_x * (1. + cone_scale_x*bw/cone_base_R), - depth/2. + cone_base_R * std::sin(alpha) * (1. + bw/cone_base_R)) - ); - paths.back().emplace_back(path); - } - } - } - - // Only the first layer has brim. - if (hh == 0.f) { - minCorner = minCorner + Point(bd, bd); - maxCorner = maxCorner - Point(bd, bd); - } - } - - // Rotate and translate the tower into the final position. - for (ExtrusionPaths& ps : paths) { - for (ExtrusionPath& p : ps) { - p.polyline.rotate(Geometry::deg2rad(rotation_angle)); - p.polyline.translate(scale_(pos.x()), scale_(pos.y())); - } - } - - return paths; - } } // namespace Slic3r diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f071f28..739932e 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -29,7 +29,7 @@ namespace Slic3r { -class GCode; +class GCodeGenerator; class Layer; class ModelObject; class Print; @@ -67,7 +67,7 @@ enum PrintStep : unsigned int { enum PrintObjectStep : unsigned int { posSlice, posPerimeters, posPrepareInfill, - posInfill, posIroning, posSupportSpotsSearch, posSupportMaterial, posEstimateCurledExtrusions, posCount, + posInfill, posIroning, posSupportSpotsSearch, posSupportMaterial, posEstimateCurledExtrusions, posCalculateOverhangingPerimeters, posCount, }; // A PrintRegion object represents a group of volumes to print @@ -376,6 +376,7 @@ private: void generate_support_spots(); void generate_support_material(); void estimate_curled_extrusions(); + void calculate_overhanging_perimeters(); void slice_volumes(); // Has any support (not counting the raft). @@ -420,38 +421,7 @@ private: FillLightning::GeneratorPtr m_lightning_generator; }; -struct FakeWipeTower -{ - // generate fake extrusion - Vec2f pos; - float width; - float height; - float layer_height; - float depth; - std::vector> z_and_depth_pairs; - float brim_width; - float rotation_angle; - float cone_angle; - Vec2d plate_origin; - void set_fake_extrusion_data(const Vec2f& p, float w, float h, float lh, float d, const std::vector>& zad, float bd, float ra, float ca, const Vec2d& o) - { - pos = p; - width = w; - height = h; - layer_height = lh; - depth = d; - z_and_depth_pairs = zad; - brim_width = bd; - rotation_angle = ra; - cone_angle = ca; - plate_origin = o; - } - - void set_pos_and_rotation(const Vec2f& p, float rotation) { pos = p; rotation_angle = rotation; } - - std::vector getFakeExtrusionPathsFromWipeTower() const; -}; struct WipeTowerData { @@ -472,6 +442,12 @@ struct WipeTowerData float brim_width; float height; + // Data needed to generate fake extrusions for conflict checking. + float width; + float first_layer_height; + float cone_angle; + Vec2d position; + float rotation_angle; void clear() { priming.reset(nullptr); tool_changes.clear(); @@ -479,7 +455,14 @@ struct WipeTowerData used_filament.clear(); number_of_toolchanges = -1; depth = 0.f; + z_and_depth_pairs.clear(); brim_width = 0.f; + height = 0.f; + width = 0.f; + first_layer_height = 0.f; + cone_angle = 0.f; + position = Vec2d::Zero(); + rotation_angle = 0.f; } private: @@ -530,6 +513,20 @@ struct PrintStatistics filament_stats.clear(); printing_extruders.clear(); } + static const std::string FilamentUsedG; + static const std::string FilamentUsedGMask; + static const std::string TotalFilamentUsedG; + static const std::string TotalFilamentUsedGMask; + static const std::string TotalFilamentUsedGValueMask; + static const std::string FilamentUsedCm3; + static const std::string FilamentUsedCm3Mask; + static const std::string FilamentUsedMm; + static const std::string FilamentUsedMmMask; + static const std::string FilamentCost; + static const std::string FilamentCostMask; + static const std::string TotalFilamentCost; + static const std::string TotalFilamentCostMask; + static const std::string TotalFilamentCostValueMask; }; using PrintObjectPtrs = std::vector; @@ -699,14 +696,13 @@ private: Polygons m_sequential_print_clearance_contours; // To allow GCode to set the Print's GCodeExport step status. - friend class GCode; + friend class GCodeGenerator; // To allow GCodeProcessor to emit warnings. friend class GCodeProcessor; // Allow PrintObject to access m_mutex and m_cancel_callback. friend class PrintObject; ConflictResultOpt m_conflict_result; - FakeWipeTower m_fake_wipe_tower; }; } /* slic3r_Print_hpp_ */ diff --git a/src/libslic3r/PrintBase.cpp b/src/libslic3r/PrintBase.cpp index 01c0ecf..dc3078e 100644 --- a/src/libslic3r/PrintBase.cpp +++ b/src/libslic3r/PrintBase.cpp @@ -17,7 +17,7 @@ void PrintTryCancel::operator()() const size_t PrintStateBase::g_last_timestamp = 0; // Update "scale", "input_filename", "input_filename_base" placeholders from the current m_objects. -void PrintBase::update_object_placeholders(DynamicConfig &config, const std::string &default_ext) const +void PrintBase::update_object_placeholders(DynamicConfig &config, const std::string & /* default_output_ext */) const { // get the first input file name std::string input_file; @@ -50,7 +50,7 @@ void PrintBase::update_object_placeholders(DynamicConfig &config, const std::str // get basename with and without suffix const std::string input_filename = boost::filesystem::path(input_file).filename().string(); const std::string input_filename_base = input_filename.substr(0, input_filename.find_last_of(".")); - config.set_key_value("input_filename", new ConfigOptionString(input_filename_base + default_ext)); +// config.set_key_value("input_filename", new ConfigOptionString(input_filename_base + default_output_ext)); config.set_key_value("input_filename_base", new ConfigOptionString(input_filename_base)); } } @@ -66,7 +66,7 @@ std::string PrintBase::output_filename(const std::string &format, const std::str PlaceholderParser::update_timestamp(cfg); this->update_object_placeholders(cfg, default_ext); if (! filename_base.empty()) { - cfg.set_key_value("input_filename", new ConfigOptionString(filename_base + default_ext)); +// cfg.set_key_value("input_filename", new ConfigOptionString(filename_base + default_ext)); cfg.set_key_value("input_filename_base", new ConfigOptionString(filename_base)); } try { diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index 9c95255..600a6cb 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -548,7 +548,7 @@ protected: // To be called by this->output_filename() with the format string pulled from the configuration layer. std::string output_filename(const std::string &format, const std::string &default_ext, const std::string &filename_base, const DynamicConfig *config_override = nullptr) const; // Update "scale", "input_filename", "input_filename_base" placeholders from the current printable ModelObjects. - void update_object_placeholders(DynamicConfig &config, const std::string &default_ext) const; + void update_object_placeholders(DynamicConfig &config, const std::string &default_output_ext) const; Model m_model; DynamicPrintConfig m_full_print_config; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 29e86f4..6447e47 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1,8 +1,10 @@ #include "PrintConfig.hpp" #include "Config.hpp" #include "I18N.hpp" +#include "format.hpp" #include "SLA/SupportTree.hpp" +#include "GCode/Thumbnails.hpp" #include #include @@ -34,6 +36,11 @@ static t_config_enum_names enum_names_from_keys_map(const t_config_enum_values & template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values() { return s_keys_map_##NAME; } \ template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names() { return s_keys_names_##NAME; } +static const t_config_enum_values s_keys_map_ArcFittingType { + { "disabled", int(ArcFittingType::Disabled) }, + { "emit_center", int(ArcFittingType::EmitCenter) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ArcFittingType) static t_config_enum_values s_keys_map_PrinterTechnology { { "FFF", ptFFF }, { "SLA", ptSLA } @@ -199,8 +206,13 @@ static const t_config_enum_values s_keys_map_DraftShield = { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(DraftShield) +static const t_config_enum_values s_keys_map_LabelObjectsStyle = { + { "disabled", int(LabelObjectsStyle::Disabled) }, + { "octoprint", int(LabelObjectsStyle::Octoprint) }, + { "firmware", int(LabelObjectsStyle::Firmware) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(LabelObjectsStyle) static const t_config_enum_values s_keys_map_GCodeThumbnailsFormat = { - { "QIDI", int(GCodeThumbnailsFormat::QIDI) }, { "PNG", int(GCodeThumbnailsFormat::PNG) }, { "JPG", int(GCodeThumbnailsFormat::JPG) }, { "QOI", int(GCodeThumbnailsFormat::QOI) } @@ -259,15 +271,6 @@ void PrintConfigDef::init_common_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionPoints{ Vec2d(0, 0), Vec2d(200, 0), Vec2d(200, 200), Vec2d(0, 200) }); - //Y18 - def = this->add("bed_exclude_area", coPoints); - def->label = L("Bed exclude area"); - def->tooltip = L("Unprintable area in XY plane. For example, X1 Series printers use the front left corner to cut filament during filament change. " - "The area is expressed as polygon by points in following format: \"XxY, XxY, ...\""); - def->mode = comExpert; - def->gui_type = ConfigOptionDef::GUIType::one_string; - def->set_default_value(new ConfigOptionPoints{ Vec2d(0, 0) }); - def = this->add("bed_custom_texture", coString); def->label = L("Bed custom texture"); def->mode = comAdvanced; @@ -288,20 +291,20 @@ void PrintConfigDef::init_common_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.)); - def = this->add("thumbnails", coPoints); + def = this->add("thumbnails", coString); def->label = L("G-code thumbnails"); - def->tooltip = L("Picture sizes to be stored into a .gcode and .sl1 / .sl1s files, in the following format: \"XxY, XxY, ...\""); + def->tooltip = L("Picture sizes to be stored into a .gcode / .bgcode and .sl1 / .sl1s files, in the following format: \"XxY/EXT, XxY/EXT, ...\"\n" + "Currently supported extensions are PNG, QOI and JPG."); def->mode = comExpert; def->gui_type = ConfigOptionDef::GUIType::one_string; - def->set_default_value(new ConfigOptionPoints()); + def->set_default_value(new ConfigOptionString()); def = this->add("thumbnails_format", coEnum); def->label = L("Format of G-code thumbnails"); def->tooltip = L("Format of G-code thumbnails: PNG for best quality, JPG for smallest size, QOI for low memory firmware"); def->mode = comExpert; - //B3 - def->set_enum({ "QIDI","PNG", "JPG", "QOI" }); - def->set_default_value(new ConfigOptionEnum(GCodeThumbnailsFormat::QIDI)); + def->set_enum({"PNG", "JPG", "QOI" }); + def->set_default_value(new ConfigOptionEnum(GCodeThumbnailsFormat::PNG)); def = this->add("layer_height", coFloat); def->label = L("Layer height"); @@ -408,6 +411,16 @@ void PrintConfigDef::init_fff_params() { ConfigOptionDef* def; + def = this->add("arc_fitting", coEnum); + def->label = L("Arc fitting"); + def->tooltip = L("Enable to get a G-code file which has G2 and G3 moves. " + "G-code resolution will be used as the fitting tolerance."); + def->set_enum({ + { "disabled", "Disabled" }, + { "emit_center", "Enabled: G2/3 I J" } + }); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(ArcFittingType::Disabled)); // Maximum extruder temperature, bumped to 1500 to support printing of glass. const int max_temp = 1500; def = this->add("avoid_crossing_curled_overhangs", coBool); @@ -1512,13 +1525,20 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionEnum(gcfRepRapSprinter)); - def = this->add("gcode_label_objects", coBool); + def = this->add("gcode_label_objects", coEnum); def->label = L("Label objects"); - def->tooltip = L("Enable this to add comments into the G-Code labeling print moves with what object they belong to," - " which is useful for the Octoprint CancelObject plugin. This settings is NOT compatible with " - "Single Extruder Multi Material setup and Wipe into Object / Wipe into Infill."); + def->tooltip = L("Selects whether labels should be exported at object boundaries and in what format.\n" + "OctoPrint = comments to be consumed by OctoPrint CancelObject plugin.\n" + "Firmware = firmware specific G-code (it will be chosen based on firmware flavor and it can end up to be empty).\n\n" + "This settings is NOT compatible with Single Extruder Multi Material setup and Wipe into Object / Wipe into Infill."); + + def->set_enum({ + { "disabled", L("Disabled") }, + { "octoprint", L("OctoPrint comments") }, + { "firmware", L("Firmware-specific") } + }); def->mode = comAdvanced; - def->set_default_value(new ConfigOptionBool(0)); + def->set_default_value(new ConfigOptionEnum(LabelObjectsStyle::Disabled)); def = this->add("gcode_substitutions", coStrings); def->label = L("G-code substitutions"); @@ -1526,6 +1546,11 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionStrings()); + def = this->add("gcode_binary", coBool); + def->label = L("Export as binary G-code"); + def->tooltip = L("Export G-code in binary format."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(0)); def = this->add("high_current_on_filament_swap", coBool); def->label = L("High extruder current on filament swap"); def->tooltip = L("It may be beneficial to increase the extruder motor current during the filament exchange" @@ -2121,7 +2146,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Output filename format"); def->tooltip = L("You can use all configuration options as variables inside this template. " "For example: [layer_height], [fill_density] etc. You can also use [timestamp], " - "[year], [month], [day], [hour], [minute], [second], [version], [input_filename], " + "[year], [month], [day], [hour], [minute], [second], [version], " "[input_filename_base], [default_output_extension]."); def->full_width = true; def->mode = comExpert; @@ -2359,7 +2384,7 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionBools { false }); def = this->add("retract_length", coFloats); - def->label = L("Length"); + def->label = L("Retraction length"); def->full_label = L("Retraction Length"); def->tooltip = L("When retraction is triggered, filament is pulled back by the specified amount " "(the length is measured on raw filament, before it enters the extruder)."); @@ -2376,12 +2401,46 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionFloats { 10. }); - def = this->add("retract_lift", coFloats); - def->label = L("Lift Z"); - def->tooltip = L("If you set this to a positive value, Z is quickly raised every time a retraction " - "is triggered. When using multiple extruders, only the setting for the first extruder " - "will be considered."); + def = this->add("travel_slope", coFloats); + def->label = L("Ramping slope angle"); + def->tooltip = L("Slope of the ramp in the initial phase of the travel."); + def->sidetext = L("°"); + def->min = 0; + def->max = 90; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloats{0.0}); + + def = this->add("travel_ramping_lift", coBools); + def->label = L("Use ramping lift"); + def->tooltip = L("Generates a ramping lift instead of lifting the extruder directly upwards. " + "The travel is split into two phases: the ramp and the standard horizontal travel. " + "This option helps reduce stringing."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBools{ false }); + + def = this->add("travel_max_lift", coFloats); + def->label = L("Maximum ramping lift"); + def->tooltip = L("Maximum lift height of the ramping lift. It may not be reached if the next position " + "is close to the old one."); def->sidetext = L("mm"); + def->min = 0; + def->max_literal = 1000; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloats{0.0}); + + def = this->add("travel_lift_before_obstacle", coBools); + def->label = L("Steeper ramp before obstacles"); + def->tooltip = L("If enabled, PrusaSlicer detects obstacles along the travel path and makes the slope steeper " + "in case an obstacle might be hit during the initial phase of the travel."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBools{false}); + def = this->add("retract_lift", coFloats); + def->label = L("Lift height"); + def->tooltip = L("Lift height applied before travel."); + def->sidetext = L("mm"); + def->min = 0; + def->max_literal = 1000; + def->mode = comSimple; def->set_default_value(new ConfigOptionFloats { 0. }); def = this->add("retract_lift_above", coFloats); @@ -2404,7 +2463,7 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionFloats { 0. }); def = this->add("retract_restart_extra", coFloats); - def->label = L("Extra length on restart"); + def->label = L("Deretraction extra length"); def->tooltip = L("When the retraction is compensated after the travel move, the extruder will push " "this additional amount of filament. This setting is rarely needed."); def->sidetext = L("mm"); @@ -2529,7 +2588,7 @@ void PrintConfigDef::init_fff_params() def = this->add("small_perimeter_speed", coFloatOrPercent); def->label = L("Small perimeters"); def->category = L("Speed"); - def->tooltip = L("This separate setting will affect the speed of perimeters having radius <= 4mm " + def->tooltip = L("This separate setting will affect the speed of perimeters having radius <= 6.5mm " "(usually holes). If expressed as percentage (for example: 80%) it will be calculated " "on the perimeters speed setting above. Set to zero for auto."); def->sidetext = L("mm/s or %"); @@ -3144,17 +3203,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); - def = this->add("threads", coInt); - def->label = L("Threads"); - def->tooltip = L("Threads are used to parallelize long-running tasks. Optimal threads number " - "is slightly above the number of available cores/processors."); - def->readonly = true; - def->min = 1; - { - int threads = (unsigned int)boost::thread::hardware_concurrency(); - def->set_default_value(new ConfigOptionInt(threads > 0 ? threads : 2)); - def->cli = ConfigOptionDef::nocli; - } def = this->add("toolchange_gcode", coString); def->label = L("Tool change G-code"); @@ -3235,8 +3283,8 @@ void PrintConfigDef::init_fff_params() def = this->add("use_firmware_retraction", coBool); def->label = L("Use firmware retraction"); - def->tooltip = L("This experimental setting uses G10 and G11 commands to have the firmware " - "handle the retraction. This is only supported in recent Marlin."); + def->tooltip = L("This setting uses G10 and G11 commands to have the firmware " + "handle the retraction. Note that this has to be supported by firmware."); def->mode = comExpert; def->set_default_value(new ConfigOptionBool(false)); @@ -3525,14 +3573,15 @@ void PrintConfigDef::init_fff_params() for (const char *opt_key : { // floats "retract_length", "retract_lift", "retract_lift_above", "retract_lift_below", "retract_speed", + "travel_max_lift", "deretract_speed", "retract_restart_extra", "retract_before_travel", "retract_length_toolchange", "retract_restart_extra_toolchange", //B34 "filament_diameter", "extrusion_multiplier", // bools - "retract_layer_change", "wipe", + "retract_layer_change", "wipe", "travel_lift_before_obstacle", "travel_ramping_lift", // percents - "retract_before_wipe"}) { + "retract_before_wipe", "travel_slope"}) { auto it_opt = options.find(opt_key); assert(it_opt != options.end()); def = this->add_nullable(std::string("filament_") + opt_key, it_opt->second.type); @@ -3567,6 +3616,7 @@ void PrintConfigDef::init_extruder_option_keys() "nozzle_diameter", "min_layer_height", "max_layer_height", "extruder_offset", "retract_length", "retract_lift", "retract_lift_above", "retract_lift_below", "retract_speed", "deretract_speed", "retract_before_wipe", "retract_restart_extra", "retract_before_travel", "wipe", + "travel_slope", "travel_max_lift", "travel_ramping_lift", "travel_lift_before_obstacle", "retract_layer_change", "retract_length_toolchange", "retract_restart_extra_toolchange", "extruder_colour", //B34 "filament_diameter", @@ -3590,6 +3640,10 @@ void PrintConfigDef::init_extruder_option_keys() "retract_restart_extra", "retract_restart_extra_toolchange", "retract_speed", + "travel_lift_before_obstacle", + "travel_max_lift", + "travel_ramping_lift", + "travel_slope", "wipe" }; assert(std::is_sorted(m_extruder_retract_keys.begin(), m_extruder_retract_keys.end())); @@ -4457,6 +4511,10 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "draft_shield" && (value == "1" || value == "0")) { // draft_shield used to be a bool, it was turned into an enum in QIDISlicer 2.4.0. value = value == "1" ? "enabled" : "disabled"; + } else if (opt_key == "gcode_label_objects" && (value == "1" || value == "0")) { + // gcode_label_objects used to be a bool (the behavior was nothing or "octoprint"), it is + // and enum since PrusaSlicer 2.6.2. + value = value == "1" ? "octoprint" : "disabled"; } else if (opt_key == "octoprint_host") { opt_key = "print_host"; } else if (opt_key == "octoprint_cafile") { @@ -4496,6 +4554,35 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va // Don't convert single options here, implement such conversion in PrintConfigDef::handle_legacy() instead. void PrintConfigDef::handle_legacy_composite(DynamicPrintConfig &config) { + if (config.has("thumbnails")) { + std::string extention; + if (config.has("thumbnails_format")) { + if (const ConfigOptionDef* opt = config.def()->get("thumbnails_format")) { + auto label = opt->enum_def->enum_to_label(config.option("thumbnails_format")->getInt()); + if (label.has_value()) + extention = *label; + } + } + + std::string thumbnails_str = config.opt_string("thumbnails"); + auto [thumbnails_list, errors] = GCodeThumbnails::make_and_check_thumbnail_list(thumbnails_str, extention); + + if (errors != enum_bitmask()) { + std::string error_str = "\n" + format("Invalid value provided for parameter %1%: %2%", "thumbnails", thumbnails_str); + error_str += GCodeThumbnails::get_error_string(errors); + throw BadOptionValueException(error_str); + } + + if (!thumbnails_list.empty()) { + const auto& extentions = ConfigOptionEnum::get_enum_names(); + thumbnails_str.clear(); + for (const auto& [ext, size] : thumbnails_list) + thumbnails_str += format("%1%x%2%/%3%, ", size.x(), size.y(), extentions[int(ext)]); + thumbnails_str.resize(thumbnails_str.length() - 2); + + config.set_key_value("thumbnails", new ConfigOptionString(thumbnails_str)); + } + } } const PrintConfigDef print_config_def; @@ -5138,6 +5225,309 @@ void DynamicPrintAndCLIConfig::handle_legacy(t_config_option_key &opt_key, std:: } } +// SlicingStatesConfigDefs + +ReadOnlySlicingStatesConfigDef::ReadOnlySlicingStatesConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("zhop", coFloat); + def->label = L("Current z-hop"); + def->tooltip = L("Contains z-hop present at the beginning of the custom G-code block."); +} + +ReadWriteSlicingStatesConfigDef::ReadWriteSlicingStatesConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("position", coFloats); + def->label = L("Position"); + def->tooltip = L("Position of the extruder at the beginning of the custom G-code block. If the custom G-code travels somewhere else, " + "it should write to this variable so PrusaSlicer knows where it travels from when it gets control back."); + + def = this->add("e_retracted", coFloats); + def->label = L("Retraction"); + def->tooltip = L("Retraction state at the beginning of the custom G-code block. If the custom G-code moves the extruder axis, " + "it should write to this variable so PrusaSlicer deretracts correctly when it gets control back."); + + def = this->add("e_restart_extra", coFloats); + def->label = L("Extra deretraction"); + def->tooltip = L("Currently planned extra extruder priming after deretraction."); + + def = this->add("e_position", coFloats); + def->label = L("Absolute E position"); + def->tooltip = L("Current position of the extruder axis. Only used with absolute extruder addressing."); +} + +OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("current_extruder", coInt); + def->label = L("Current extruder"); + def->tooltip = L("Zero-based index of currently used extruder."); + + def = this->add("current_object_idx", coInt); + def->label = L("Current object index"); + def->tooltip = L("Specific for sequential printing. Zero-based index of currently printed object."); + + def = this->add("has_single_extruder_multi_material_priming", coBool); + def->label = L("Has single extruder MM priming"); + def->tooltip = L("Are the extra multi-material priming regions used in this print?"); + + def = this->add("has_wipe_tower", coBool); + def->label = L("Has wipe tower"); + def->tooltip = L("Whether or not wipe tower is being generated in the print."); + + def = this->add("initial_extruder", coInt); + def->label = L("Initial extruder"); + def->tooltip = L("Zero-based index of the first extruder used in the print. Same as initial_tool."); + + def = this->add("initial_filament_type", coString); + // TRN: Meaning 'filament type of the initial filament' + def->label = L("Initial filament type"); + def->tooltip = L("String containing filament type of the first used extruder."); + + def = this->add("initial_tool", coInt); + def->label = L("Initial tool"); + def->tooltip = L("Zero-based index of the first extruder used in the print. Same as initial_extruder."); + + def = this->add("is_extruder_used", coBools); + def->label = L("Is extruder used?"); + def->tooltip = L("Vector of booleans stating whether a given extruder is used in the print."); +} + +PrintStatisticsConfigDef::PrintStatisticsConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("extruded_volume", coFloats); + def->label = L("Volume per extruder"); + def->tooltip = L("Total filament volume extruded per extruder during the entire print."); + + def = this->add("normal_print_time", coString); + def->label = L("Print time (normal mode)"); + def->tooltip = L("Estimated print time when printed in normal mode (i.e. not in silent mode). Same as print_time."); + + def = this->add("num_printing_extruders", coInt); + def->label = L("Number of printing extruders"); + def->tooltip = L("Number of extruders used during the print."); + + def = this->add("print_time", coString); + def->label = L("Print time (normal mode)"); + def->tooltip = L("Estimated print time when printed in normal mode (i.e. not in silent mode). Same as normal_print_time."); + + def = this->add("printing_filament_types", coString); + def->label = L("Used filament types"); + def->tooltip = L("Comma-separated list of all filament types used during the print."); + + def = this->add("silent_print_time", coString); + def->label = L("Print time (silent mode)"); + def->tooltip = L("Estimated print time when printed in silent mode."); + + def = this->add("total_cost", coFloat); + def->label = L("Total cost"); + def->tooltip = L("Total cost of all material used in the print. Calculated from cost in Filament Settings."); + + def = this->add("total_weight", coFloat); + def->label = L("Total weight"); + def->tooltip = L("Total weight of the print. Calculated from density in Filament Settings."); + + def = this->add("total_wipe_tower_cost", coFloat); + def->label = L("Total wipe tower cost"); + def->tooltip = L("Total cost of the material wasted on the wipe tower. Calculated from cost in Filament Settings."); + + def = this->add("total_wipe_tower_filament", coFloat); + def->label = L("Wipe tower volume"); + def->tooltip = L("Total filament volume extruded on the wipe tower."); + + def = this->add("used_filament", coFloat); + def->label = L("Used filament"); + def->tooltip = L("Total length of filament used in the print."); + + def = this->add("total_toolchanges", coInt); + def->label = L("Total number of toolchanges"); + def->tooltip = L("Number of toolchanges during the print."); + + def = this->add("extruded_volume_total", coFloat); + def->label = L("Total volume"); + def->tooltip = L("Total volume of filament used during the entire print."); + + def = this->add("extruded_weight", coFloats); + def->label = L("Weight per extruder"); + def->tooltip = L("Weight per extruder extruded during the entire print. Calculated from density in Filament Settings."); + + def = this->add("extruded_weight_total", coFloat); + def->label = L("Total weight"); + def->tooltip = L("Total weight of the print. Calculated from density in Filament Settings."); + + def = this->add("total_layer_count", coInt); + def->label = L("Total layer count"); + def->tooltip = L("Number of layers in the entire print."); +} + +ObjectsInfoConfigDef::ObjectsInfoConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("num_objects", coInt); + def->label = L("Number of objects"); + def->tooltip = L("Total number of objects in the print."); + + def = this->add("num_instances", coInt); + def->label = L("Number of instances"); + def->tooltip = L("Total number of object instances in the print, summed over all objects."); + + def = this->add("scale", coStrings); + def->label = L("Scale per object"); + def->tooltip = L("Contains a string with the information about what scaling was applied to the individual objects. " + "Indexing of the objects is zero-based (first object has index 0).\n" + "Example: 'x:100% y:50% z:100%'."); + + def = this->add("input_filename_base", coString); + def->label = L("Input filename without extension"); + def->tooltip = L("Source filename of the first object, without extension."); +} + +DimensionsConfigDef::DimensionsConfigDef() +{ + ConfigOptionDef* def; + + const std::string point_tooltip = L("The vector has two elements: x and y coordinate of the point. Values in mm."); + const std::string bb_size_tooltip = L("The vector has two elements: x and y dimension of the bounding box. Values in mm."); + + def = this->add("first_layer_print_convex_hull", coPoints); + def->label = L("First layer convex hull"); + def->tooltip = L("Vector of points of the first layer convex hull. Each element has the following format: " + "'[x, y]' (x and y are floating-point numbers in mm)."); + + def = this->add("first_layer_print_min", coFloats); + def->label = L("Bottom-left corner of first layer bounding box"); + def->tooltip = point_tooltip; + + def = this->add("first_layer_print_max", coFloats); + def->label = L("Top-right corner of first layer bounding box"); + def->tooltip = point_tooltip; + + def = this->add("first_layer_print_size", coFloats); + def->label = L("Size of the first layer bounding box"); + def->tooltip = bb_size_tooltip; + + def = this->add("print_bed_min", coFloats); + def->label = L("Bottom-left corner of print bed bounding box"); + def->tooltip = point_tooltip; + + def = this->add("print_bed_max", coFloats); + def->label = L("Top-right corner of print bed bounding box"); + def->tooltip = point_tooltip; + + def = this->add("print_bed_size", coFloats); + def->label = L("Size of the print bed bounding box"); + def->tooltip = bb_size_tooltip; +} + +TimestampsConfigDef::TimestampsConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("timestamp", coString); + def->label = L("Timestamp"); + def->tooltip = L("String containing current time in yyyyMMdd-hhmmss format."); + + def = this->add("year", coInt); + def->label = L("Year"); + + def = this->add("month", coInt); + def->label = L("Month"); + + def = this->add("day", coInt); + def->label = L("Day"); + + def = this->add("hour", coInt); + def->label = L("Hour"); + + def = this->add("minute", coInt); + def->label = L("Minute"); + + def = this->add("second", coInt); + def->label = L("Second"); +} + +OtherPresetsConfigDef::OtherPresetsConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("num_extruders", coInt); + def->label = L("Number of extruders"); + def->tooltip = L("Total number of extruders, regardless of whether they are used in the current print."); + + def = this->add("print_preset", coString); + def->label = L("Print preset name"); + def->tooltip = L("Name of the print preset used for slicing."); + + def = this->add("filament_preset", coStrings); + def->label = L("Filament preset name"); + def->tooltip = L("Names of the filament presets used for slicing. The variable is a vector " + "containing one name for each extruder."); + + def = this->add("printer_preset", coString); + def->label = L("Printer preset name"); + def->tooltip = L("Name of the printer preset used for slicing."); + + def = this->add("physical_printer_preset", coString); + def->label = L("Physical printer name"); + def->tooltip = L("Name of the physical printer used for slicing."); +} + + +static std::map s_CustomGcodeSpecificPlaceholders{ + {"start_filament_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id"}}, + {"end_filament_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id"}}, + {"end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id"}}, + {"before_layer_gcode", {"layer_num", "layer_z", "max_layer_z"}}, + {"layer_gcode", {"layer_num", "layer_z", "max_layer_z"}}, + {"toolchange_gcode", {"layer_num", "layer_z", "max_layer_z", "previous_extruder", "next_extruder", "toolchange_z"}}, +}; + +const std::map& custom_gcode_specific_placeholders() +{ + return s_CustomGcodeSpecificPlaceholders; +} + +CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("layer_num", coInt); + def->label = L("Layer number"); + def->tooltip = L("Zero-based index of the current layer (i.e. first layer is number 0)."); + + def = this->add("layer_z", coFloat); + def->label = L("Layer Z"); + def->tooltip = L("Height of the current layer above the print bed, measured to the top of the layer."); + + def = this->add("max_layer_z", coFloat); + def->label = L("Maximal layer Z"); + def->tooltip = L("Height of the last layer above the print bed."); + + def = this->add("filament_extruder_id", coInt); + def->label = L("Current extruder index"); + def->tooltip = L("Zero-based index of currently used extruder (i.e. first extruder has index 0)."); + + def = this->add("previous_extruder", coInt); + def->label = L("Previous extruder"); + def->tooltip = L("Index of the extruder that is being unloaded. The index is zero based (first extruder has index 0)."); + + def = this->add("next_extruder", coInt); + def->label = L("Next extruder"); + def->tooltip = L("Index of the extruder that is being loaded. The index is zero based (first extruder has index 0)."); + + def = this->add("toolchange_z", coFloat); + def->label = L("Toolchange Z"); + def->tooltip = L("Height above the print bed when the toolchange takes place. Usually the same as layer_z, but can be different."); +} + +const CustomGcodeSpecificConfigDef custom_gcode_specific_config_def; uint64_t ModelConfig::s_last_timestamp = 1; static Points to_points(const std::vector &dpts) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 589193b..c24be44 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -30,6 +30,10 @@ namespace Slic3r { +enum class ArcFittingType { + Disabled, + EmitCenter +}; enum GCodeFlavor : unsigned char { gcfRepRapSprinter, gcfRepRapFirmware, gcfRepetier, gcfTeacup, gcfMakerWare, gcfMarlinLegacy, gcfMarlinFirmware, gcfKlipper, gcfSailfish, gcfMach3, gcfMachinekit, gcfSmoothie, gcfNoExtrusion, @@ -124,6 +128,9 @@ enum DraftShield { dsDisabled, dsLimited, dsEnabled }; +enum class LabelObjectsStyle { + Disabled, Octoprint, Firmware +}; enum class PerimeterGeneratorType { // Classic perimeter generator using Clipper offsets with constant extrusion width. @@ -132,15 +139,15 @@ enum class PerimeterGeneratorType // "A framework for adaptive width control of dense contour-parallel toolpaths in fused deposition modeling" ported from Cura. Arachne }; -//B3 enum class GCodeThumbnailsFormat { - QIDI, PNG, JPG, QOI + PNG, JPG, QOI }; #define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \ template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names(); \ template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values(); +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ArcFittingType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrinterTechnology) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(MachineLimitsUsage) @@ -159,6 +166,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLAPillarConnectionMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLASupportTreeType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BrimType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(DraftShield) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(LabelObjectsStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeThumbnailsFormat) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType) @@ -676,6 +684,7 @@ PRINT_CONFIG_CLASS_DEFINE( PRINT_CONFIG_CLASS_DEFINE( GCodeConfig, + ((ConfigOptionEnum, arc_fitting)) ((ConfigOptionBool, autoemit_temperature_commands)) ((ConfigOptionString, before_layer_gcode)) ((ConfigOptionString, between_objects_gcode)) @@ -708,18 +717,23 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloats, filament_multitool_ramming_flow)) ((ConfigOptionBool, gcode_comments)) ((ConfigOptionEnum, gcode_flavor)) - ((ConfigOptionBool, gcode_label_objects)) + ((ConfigOptionEnum, gcode_label_objects)) // Triples of strings: "search pattern", "replace with pattern", "attribs" // where "attribs" are one of: // r - regular expression // i - case insensitive // w - whole word ((ConfigOptionStrings, gcode_substitutions)) + ((ConfigOptionBool, gcode_binary)) ((ConfigOptionString, layer_gcode)) ((ConfigOptionFloat, max_print_speed)) ((ConfigOptionFloat, max_volumetric_speed)) ((ConfigOptionFloat, max_volumetric_extrusion_rate_slope_positive)) ((ConfigOptionFloat, max_volumetric_extrusion_rate_slope_negative)) + ((ConfigOptionBools, travel_ramping_lift)) + ((ConfigOptionFloats, travel_max_lift)) + ((ConfigOptionFloats, travel_slope)) + ((ConfigOptionBools, travel_lift_before_obstacle)) ((ConfigOptionPercents, retract_before_wipe)) ((ConfigOptionFloats, retract_length)) ((ConfigOptionFloats, retract_length_toolchange)) @@ -774,8 +788,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, avoid_crossing_perimeters)) ((ConfigOptionFloatOrPercent, avoid_crossing_perimeters_max_detour)) ((ConfigOptionPoints, bed_shape)) - //Y18 - ((ConfigOptionPoints, bed_exclude_area)) ((ConfigOptionInts, bed_temperature)) //Y16 ((ConfigOptionBool, chamber_temperature)) @@ -856,7 +868,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionInt, standby_temperature_delta)) ((ConfigOptionInts, temperature)) ((ConfigOptionInt, threads)) - ((ConfigOptionPoints, thumbnails)) + ((ConfigOptionString, thumbnails)) ((ConfigOptionEnum, thumbnails_format)) ((ConfigOptionFloat, top_solid_infill_acceleration)) ((ConfigOptionFloat, travel_acceleration)) @@ -1193,6 +1205,67 @@ public: CLIMiscConfigDef(); }; +typedef std::string t_custom_gcode_key; +// This map containes list of specific placeholders for each custom G-code, if any exist +const std::map& custom_gcode_specific_placeholders(); + +// Next classes define placeholders used by GUI::EditGCodeDialog. + +class ReadOnlySlicingStatesConfigDef : public ConfigDef +{ +public: + ReadOnlySlicingStatesConfigDef(); +}; + +class ReadWriteSlicingStatesConfigDef : public ConfigDef +{ +public: + ReadWriteSlicingStatesConfigDef(); +}; + +class OtherSlicingStatesConfigDef : public ConfigDef +{ +public: + OtherSlicingStatesConfigDef(); +}; + +class PrintStatisticsConfigDef : public ConfigDef +{ +public: + PrintStatisticsConfigDef(); +}; + +class ObjectsInfoConfigDef : public ConfigDef +{ +public: + ObjectsInfoConfigDef(); +}; + +class DimensionsConfigDef : public ConfigDef +{ +public: + DimensionsConfigDef(); +}; + +class TimestampsConfigDef : public ConfigDef +{ +public: + TimestampsConfigDef(); +}; + +class OtherPresetsConfigDef : public ConfigDef +{ +public: + OtherPresetsConfigDef(); +}; + +// This classes defines all custom G-code specific placeholders. +class CustomGcodeSpecificConfigDef : public ConfigDef +{ +public: + CustomGcodeSpecificConfigDef(); +}; +extern const CustomGcodeSpecificConfigDef custom_gcode_specific_config_def; // This class defines the command line options representing actions. extern const CLIActionsConfigDef cli_actions_config_def; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 8694d2e..8732483 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -3,7 +3,9 @@ #include "ExPolygon.hpp" #include "Exception.hpp" #include "Flow.hpp" +#include "GCode/ExtrusionProcessor.hpp" #include "KDTreeIndirect.hpp" +#include "Line.hpp" #include "Point.hpp" #include "Polygon.hpp" #include "Polyline.hpp" @@ -533,6 +535,64 @@ void PrintObject::estimate_curled_extrusions() } } +void PrintObject::calculate_overhanging_perimeters() +{ + if (this->set_started(posCalculateOverhangingPerimeters)) { + BOOST_LOG_TRIVIAL(debug) << "Calculating overhanging perimeters - start"; + m_print->set_status(89, _u8L("Calculating overhanging perimeters")); + std::vector extruders; + std::unordered_set regions_with_dynamic_speeds; + for (const PrintRegion *pr : this->print()->m_print_regions) { + if (pr->config().enable_dynamic_overhang_speeds.getBool()) { + regions_with_dynamic_speeds.insert(pr); + } + extruders.clear(); + pr->collect_object_printing_extruders(*this->print(), extruders); + auto cfg = this->print()->config(); + if (std::any_of(extruders.begin(), extruders.end(), + [&cfg](unsigned int extruder_id) { return cfg.enable_dynamic_fan_speeds.get_at(extruder_id); })) { + regions_with_dynamic_speeds.insert(pr); + } + } + + if (!regions_with_dynamic_speeds.empty()) { + std::unordered_map> curled_lines; + std::unordered_map> unscaled_polygons_lines; + for (const Layer *l : this->layers()) { + curled_lines[l->id()] = AABBTreeLines::LinesDistancer{l->curled_lines}; + unscaled_polygons_lines[l->id()] = AABBTreeLines::LinesDistancer{to_unscaled_linesf(l->lslices)}; + } + curled_lines[size_t(-1)] = {}; + unscaled_polygons_lines[size_t(-1)] = {}; + + tbb::parallel_for(tbb::blocked_range(0, m_layers.size()), [this, &curled_lines, &unscaled_polygons_lines, + ®ions_with_dynamic_speeds]( + const tbb::blocked_range &range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); + for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { + auto l = m_layers[layer_idx]; + if (l->id() == 0) { // first layer, do not split + continue; + } + for (LayerRegion *layer_region : l->regions()) { + if (regions_with_dynamic_speeds.find(layer_region->m_region) == regions_with_dynamic_speeds.end()) { + continue; + } + size_t prev_layer_id = l->lower_layer ? l->lower_layer->id() : size_t(-1); + layer_region->m_perimeters = + ExtrusionProcessor::calculate_and_split_overhanging_extrusions(&layer_region->m_perimeters, + unscaled_polygons_lines[prev_layer_id], + curled_lines[l->id()]); + } + } + }); + + m_print->throw_if_canceled(); + BOOST_LOG_TRIVIAL(debug) << "Calculating overhanging perimeters - end"; + } + this->set_done(posCalculateOverhangingPerimeters); + } +} std::pair PrintObject::prepare_adaptive_infill_data( const std::vector> &surfaces_w_bottom_z) const { @@ -644,7 +704,8 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "first_layer_extrusion_width" || opt_key == "perimeter_extrusion_width" || opt_key == "infill_overlap" - || opt_key == "external_perimeters_first") { + || opt_key == "external_perimeters_first" + || opt_key == "arc_fitting") { steps.emplace_back(posPerimeters); } else if ( opt_key == "gap_fill_enabled" @@ -848,7 +909,7 @@ bool PrintObject::invalidate_step(PrintObjectStep step) // propagate to dependent steps if (step == posPerimeters) { - invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning, posSupportSpotsSearch, posEstimateCurledExtrusions }); + invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning, posSupportSpotsSearch, posEstimateCurledExtrusions, posCalculateOverhangingPerimeters }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); } else if (step == posPrepareInfill) { invalidated |= this->invalidate_steps({ posInfill, posIroning, posSupportSpotsSearch}); @@ -857,7 +918,7 @@ bool PrintObject::invalidate_step(PrintObjectStep step) invalidated |= m_print->invalidate_steps({ psSkirtBrim }); } else if (step == posSlice) { invalidated |= this->invalidate_steps({posPerimeters, posPrepareInfill, posInfill, posIroning, posSupportSpotsSearch, - posSupportMaterial, posEstimateCurledExtrusions}); + posSupportMaterial, posEstimateCurledExtrusions, posCalculateOverhangingPerimeters}); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; } else if (step == posSupportMaterial) { @@ -1554,7 +1615,7 @@ void PrintObject::discover_vertical_shells() to_polygons(m_layers[idx_layer + 1]->lslices) : Polygons{}; object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice); - internal_volume = closing(polygonsInternal, SCALED_EPSILON); + internal_volume = closing(polygonsInternal, float(SCALED_EPSILON)); } // The regularization operation may cause scattered tiny drops on the smooth parts of the model, filter them out diff --git a/src/libslic3r/ShortestPath.cpp b/src/libslic3r/ShortestPath.cpp index da1a44e..5f56f19 100644 --- a/src/libslic3r/ShortestPath.cpp +++ b/src/libslic3r/ShortestPath.cpp @@ -1006,16 +1006,20 @@ std::vector> chain_segments_greedy2(SegmentEndPointFunc return chain_segments_greedy_constrained_reversals2_(end_point_func, could_reverse_func, num_segments, start_near); } -std::vector> chain_extrusion_entities(std::vector &entities, const Point *start_near) +std::vector> chain_extrusion_entities(const std::vector &entities, const Point *start_near, const bool reversed) { - auto segment_end_point = [&entities](size_t idx, bool first_point) -> const Point& { return first_point ? entities[idx]->first_point() : entities[idx]->last_point(); }; + auto segment_end_point = [&entities, reversed](size_t idx, bool first_point) -> const Point& { return first_point == reversed ? entities[idx]->last_point() : entities[idx]->first_point(); }; auto could_reverse = [&entities](size_t idx) { const ExtrusionEntity *ee = entities[idx]; return ee->is_loop() || ee->can_reverse(); }; - std::vector> out = chain_segments_greedy_constrained_reversals(segment_end_point, could_reverse, entities.size(), start_near); + std::vector> out = chain_segments_greedy_constrained_reversals( + segment_end_point, could_reverse, entities.size(), start_near); for (std::pair &segment : out) { ExtrusionEntity *ee = entities[segment.first]; if (ee->is_loop()) // Ignore reversals for loops, as the start point equals the end point. segment.second = false; + else if (reversed) + // Input was already reversed. + segment.second = ! segment.second; // Is can_reverse() respected by the reversals? assert(ee->can_reverse() || ! segment.second); } @@ -1041,6 +1045,32 @@ void chain_and_reorder_extrusion_entities(std::vector &entitie reorder_extrusion_entities(entities, chain_extrusion_entities(entities, start_near)); } +ExtrusionEntityReferences chain_extrusion_references(const std::vector &entities, const Point *start_near, const bool reversed) +{ + const std::vector> chain = chain_extrusion_entities(entities, start_near, reversed); + ExtrusionEntityReferences out; + out.reserve(chain.size()); + for (const std::pair &idx : chain) { + assert(entities[idx.first] != nullptr); + out.push_back({ *entities[idx.first], idx.second }); + } + return out; +} + +ExtrusionEntityReferences chain_extrusion_references(const ExtrusionEntityCollection &eec, const Point *start_near, const bool reversed) +{ + if (eec.no_sort) { + ExtrusionEntityReferences out; + out.reserve(eec.entities.size()); + for (const ExtrusionEntity *ee : eec.entities) { + assert(ee != nullptr); + // Never reverse a loop. + out.push_back({ *ee, ! ee->is_loop() && reversed }); + } + return out; + } else + return chain_extrusion_references(eec.entities, start_near, reversed); +} std::vector> chain_extrusion_paths(std::vector &extrusion_paths, const Point *start_near) { auto segment_end_point = [&extrusion_paths](size_t idx, bool first_point) -> const Point& { return first_point ? extrusion_paths[idx].first_point() : extrusion_paths[idx].last_point(); }; diff --git a/src/libslic3r/ShortestPath.hpp b/src/libslic3r/ShortestPath.hpp index 1781c51..f009433 100644 --- a/src/libslic3r/ShortestPath.hpp +++ b/src/libslic3r/ShortestPath.hpp @@ -18,13 +18,24 @@ namespace Slic3r { class ExPolygon; using ExPolygons = std::vector; +// Used by chain_expolygons() std::vector chain_points(const Points &points, Point *start_near = nullptr); +// Used to give layer islands a print order. std::vector chain_expolygons(const ExPolygons &expolygons, Point *start_near = nullptr); -std::vector> chain_extrusion_entities(std::vector &entities, const Point *start_near = nullptr); +// Chain extrusion entities by a shortest distance. Returns the ordered extrusions together with a "reverse" flag. +// Set input "reversed" to true if the vector of "entities" is to be considered to be reversed once already. +std::vector> chain_extrusion_entities(const std::vector &entities, const Point *start_near = nullptr, const bool reversed = false); +// Reorder & reverse extrusion entities in place based on the "chain" ordering. void reorder_extrusion_entities(std::vector &entities, const std::vector> &chain); +// Reorder & reverse extrusion entities in place. void chain_and_reorder_extrusion_entities(std::vector &entities, const Point *start_near = nullptr); +// Chain extrusion entities by a shortest distance. Returns the ordered extrusions together with a "reverse" flag. +// Set input "reversed" to true if the vector of "entities" is to be considered to be reversed. +ExtrusionEntityReferences chain_extrusion_references(const std::vector &entities, const Point *start_near = nullptr, const bool reversed = false); +// The same as above, respect eec.no_sort flag. +ExtrusionEntityReferences chain_extrusion_references(const ExtrusionEntityCollection &eec, const Point *start_near = nullptr, const bool reversed = false); std::vector> chain_extrusion_paths(std::vector &extrusion_paths, const Point *start_near = nullptr); void reorder_extrusion_paths(std::vector &extrusion_paths, std::vector> &chain); void chain_and_reorder_extrusion_paths(std::vector &extrusion_paths, const Point *start_near = nullptr); diff --git a/src/libslic3r/StaticMap.hpp b/src/libslic3r/StaticMap.hpp new file mode 100644 index 0000000..b834027 --- /dev/null +++ b/src/libslic3r/StaticMap.hpp @@ -0,0 +1,331 @@ +#ifndef QIDISLICER_STATICMAP_HPP +#define QIDISLICER_STATICMAP_HPP + +#include +#include +#include +#include +#include + + +namespace Slic3r { + +// This module provides std::map and std::set like structures with fixed number +// of elements and usable at compile or in time constexpr contexts without +// any memory allocations. + +// C++20 emulation utilities to get the missing constexpr functionality in C++17 +namespace static_set_detail { + +// Simple bubble sort but constexpr +template> +constexpr void sort_array(std::array &arr, Cmp cmp = {}) +{ + // A bubble sort will do the job, C++20 will have constexpr std::sort + for (size_t i = 0; i < N - 1; ++i) + { + for (size_t j = 0; j < N - i - 1; ++j) + { + if (!cmp(arr[j], arr[j + 1])) { + T temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } +} + +// Simple emulation of lower_bound with constexpr +template> +constexpr auto array_lower_bound(It from, It to, const V &val, Cmp cmp) +{ + auto N = std::distance(from, to); + std::size_t middle = N / 2; + + if (N == 0) { + return from; // Key not found, return the beginning of the array + } else if (cmp(val, *(from + middle))) { + return array_lower_bound(from, from + middle, val, cmp); + } else if (cmp(*(from + middle), val)) { + return array_lower_bound(from + middle + 1, to, val, cmp); + } else { + return from + middle; // Key found, return an iterator to it + } + + return to; +} + +template> +constexpr auto array_lower_bound(const std::array &arr, + const T &val, + Cmp cmp = {}) +{ + return array_lower_bound(arr.begin(), arr.end(), val, cmp); +} + +template +constexpr std::array, N> +to_array_impl(T (&a)[N], std::index_sequence) +{ + return {{a[I]...}}; +} + +template +constexpr std::array, N> to_array(T (&a)[N]) +{ + return to_array_impl(a, std::make_index_sequence{}); +} + +// Emulating constexpr std::pair +template +struct StaticMapElement { + using KeyType = K; + + constexpr StaticMapElement(const K &k, const V &v): first{k}, second{v} {} + + K first; + V second; +}; + +} // namespace static_set_detail +} // namespace Slic3r + +namespace Slic3r { + +// std::set like set structure +template> +class StaticSet { + std::array m_vals; // building on top of std::array + Cmp m_cmp; + +public: + using value_type = T; + + constexpr StaticSet(const std::array &arr, Cmp cmp = {}) + : m_vals{arr}, m_cmp{cmp} + { + // TODO: C++20 can use std::sort(vals.begin(), vals.end()) + static_set_detail::sort_array(m_vals, m_cmp); + } + + template + constexpr StaticSet(Ts &&...args): m_vals{std::forward(args)...} + { + static_set_detail::sort_array(m_vals, m_cmp); + } + + template + constexpr StaticSet(Cmp cmp, Ts &&...args) + : m_vals{std::forward(args)...}, m_cmp{cmp} + { + static_set_detail::sort_array(m_vals, m_cmp); + } + + constexpr auto find(const T &val) const + { + // TODO: C++20 can use std::lower_bound + auto it = static_set_detail::array_lower_bound(m_vals, val, m_cmp); + if (it != m_vals.end() && ! m_cmp(*it, val) && !m_cmp(val, *it) ) + return it; + + return m_vals.cend(); + } + + constexpr bool empty() const { return m_vals.empty(); } + constexpr size_t size() const { return m_vals.size(); } + + // Can be iterated over + constexpr auto begin() const { return m_vals.begin(); } + constexpr auto end() const { return m_vals.end(); } +}; + +// These are "deduction guides", a C++17 feature. +// Reason is to be able to deduce template arguments from constructor arguments +// e.g.: StaticSet{1, 2, 3} is deduced as StaticSet>, no +// need to state the template types explicitly. +template +StaticSet(T, Vals...) -> + StaticSet && ...), T>, + 1 + sizeof...(Vals)>; + +// Same as above, only with the first argument being a comparison functor +template +StaticSet(Cmp, T, Vals...) -> + StaticSet && ...), T>, + 1 + sizeof...(Vals), + std::enable_if_t, Cmp>>; + +// Specialization for the empty set case. +template +class StaticSet { +public: + constexpr StaticSet() = default; + constexpr auto find(const T &val) const { return nullptr; } + constexpr bool empty() const { return true; } + constexpr size_t size() const { return 0; } + constexpr auto begin() const { return nullptr; } + constexpr auto end() const { return nullptr; } +}; + +// Constructor with no arguments need to be deduced as the specialization for +// empty sets (see above) +StaticSet() -> StaticSet; + + + +// StaticMap definition: + +template +using SMapEl = static_set_detail::StaticMapElement; + +template +struct DefaultCmp { + constexpr bool operator() (const SMapEl &el1, const SMapEl &el2) const + { + return std::less{}(el1.first, el2.first); + } +}; + +// Overriding the default comparison for C style strings, as std::less +// doesn't do the lexicographic comparisons, only the pointer values would be +// compared. Fortunately we can wrap the C style strings with string_views and +// do the comparison with those. +template +struct DefaultCmp { + constexpr bool operator() (const SMapEl &el1, + const SMapEl &el2) const + { + return std::string_view{el1.first} < std::string_view{el2.first}; + } +}; + +template> +class StaticMap { + std::array, N> m_vals; + Cmp m_cmp; + +public: + using value_type = SMapEl; + + constexpr StaticMap(const std::array, N> &arr, Cmp cmp = {}) + : m_vals{arr}, m_cmp{cmp} + { + static_set_detail::sort_array(m_vals, cmp); + } + + constexpr auto find(const K &key) const + { + auto ret = m_vals.end(); + + SMapEl vkey{key, V{}}; + + auto it = static_set_detail::array_lower_bound( + std::begin(m_vals), std::end(m_vals), vkey, m_cmp + ); + + if (it != std::end(m_vals) && ! m_cmp(*it, vkey) && !m_cmp(vkey, *it)) + ret = it; + + return ret; + } + + constexpr const V& at(const K& key) const + { + if (auto it = find(key); it != end()) + return it->second; + + throw std::out_of_range{"No such element"}; + } + + constexpr bool empty() const { return m_vals.empty(); } + constexpr size_t size() const { return m_vals.size(); } + + constexpr auto begin() const { return m_vals.begin(); } + constexpr auto end() const { return m_vals.end(); } +}; + +template +class StaticMap { +public: + constexpr StaticMap() = default; + constexpr auto find(const K &key) const { return nullptr; } + constexpr bool empty() const { return true; } + constexpr size_t size() const { return 0; } + [[noreturn]] constexpr const V& at(const K &) const { throw std::out_of_range{"Map is empty"}; } + constexpr auto begin() const { return nullptr; } + constexpr auto end() const { return nullptr; } +}; + +// Deducing template arguments from the StaticMap constructors is not easy, +// so there is a helper "make" function to be used instead: +// e.g.: auto map = make_staticmap({ {"one", 1}, {"two", 2}}) +// will work, and only the key and value type needs to be specified. No need +// to state the number of elements, that is deduced automatically. +template +constexpr auto make_staticmap(const SMapEl (&arr) [N]) +{ + return StaticMap{static_set_detail ::to_array(arr), DefaultCmp{}}; +} + +template +constexpr auto make_staticmap(const SMapEl (&arr) [N], Cmp cmp) +{ + return StaticMap{static_set_detail ::to_array(arr), cmp}; +} + +// Override for empty maps +template> +constexpr auto make_staticmap() +{ + return StaticMap{}; +} + +// Override which uses a c++ array as the initializer +template> +constexpr auto make_staticmap(const std::array, N> &arr, Cmp cmp = {}) +{ + return StaticMap{arr, cmp}; +} + +// Helper function to get a specific element from a set, returning a std::optional +// which is more convinient than working with iterators +template +constexpr std::enable_if_t, std::optional> +query(const StaticSet &sset, const T &val) +{ + std::optional ret; + if (auto it = sset.find(val); it != sset.end()) + ret = *it; + + return ret; +} + +template +constexpr std::enable_if_t, std::optional> +query(const StaticMap &sset, const KeyT &val) +{ + std::optional ret; + + if (auto it = sset.find(val); it != sset.end()) + ret = it->second; + + return ret; +} + +template +constexpr std::enable_if_t, bool> +contains(const StaticSet &sset, const T &val) +{ + return sset.find(val) != sset.end(); +} + +template +constexpr std::enable_if_t, bool> +contains(const StaticMap &smap, const KeyT &key) +{ + return smap.find(key) != smap.end(); +} + +} // namespace Slic3r + +#endif // STATICMAP_HPP diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index dce57a7..4fb4ce9 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -485,8 +485,7 @@ static inline void fill_expolygon_generate_paths( extrusion_entities_append_paths( dst, std::move(polylines), - role, - flow.mm3_per_mm(), flow.width(), flow.height()); + { role, flow }); } static inline void fill_expolygons_generate_paths( @@ -644,7 +643,7 @@ static inline void tree_supports_generate_paths( ExPolygons level2 = offset2_ex({ expoly }, -1.5 * flow.scaled_width(), 0.5 * flow.scaled_width()); if (level2.size() == 1) { Polylines polylines; - extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), + extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length), { ExtrusionRole::SupportMaterial, flow }, // Disable reversal of the path, always start with the anchor, always print CCW. false); expoly = level2.front(); @@ -750,7 +749,7 @@ static inline void tree_supports_generate_paths( } ExtrusionEntitiesPtr &out = eec ? eec->entities : dst; - extrusion_entities_append_paths(out, std::move(polylines), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), + extrusion_entities_append_paths(out, std::move(polylines), { ExtrusionRole::SupportMaterial, flow }, // Disable reversal of the path, always start with the anchor, always print CCW. false); if (eec) { @@ -794,7 +793,7 @@ static inline void fill_expolygons_with_sheath_generate_paths( eec->no_sort = true; } ExtrusionEntitiesPtr &out = no_sort ? eec->entities : dst; - extrusion_entities_append_paths(out, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height()); + extrusion_entities_append_paths(out, draw_perimeters(expoly, clip_length), { ExtrusionRole::SupportMaterial, flow }); // Fill in the rest. fill_expolygons_generate_paths(out, offset_ex(expoly, float(-0.4 * spacing)), filler, fill_params, density, role, flow); if (no_sort && ! eec->empty()) @@ -1106,7 +1105,7 @@ void LoopInterfaceProcessor::generate(SupportGeneratorLayerExtruded &top_contact extrusion_entities_append_paths( top_contact_layer.extrusions, std::move(loop_lines), - ExtrusionRole::SupportMaterialInterface, flow.mm3_per_mm(), flow.width(), flow.height()); + { ExtrusionRole::SupportMaterialInterface, flow }); } #ifdef SLIC3R_DEBUG @@ -1148,24 +1147,20 @@ static void modulate_extrusion_by_overlapping_layers( ExtrusionPath *extrusion_path_template = dynamic_cast(extrusions_in_out.front()); assert(extrusion_path_template != nullptr); ExtrusionRole extrusion_role = extrusion_path_template->role(); - float extrusion_width = extrusion_path_template->width; + float extrusion_width = extrusion_path_template->width(); struct ExtrusionPathFragment { - ExtrusionPathFragment() : mm3_per_mm(-1), width(-1), height(-1) {}; - ExtrusionPathFragment(double mm3_per_mm, float width, float height) : mm3_per_mm(mm3_per_mm), width(width), height(height) {}; + ExtrusionFlow flow; Polylines polylines; - double mm3_per_mm; - float width; - float height; }; // Split the extrusions by the overlapping layers, reduce their extrusion rate. // The last path_fragment is from this_layer. std::vector path_fragments( n_overlapping_layers + 1, - ExtrusionPathFragment(extrusion_path_template->mm3_per_mm, extrusion_path_template->width, extrusion_path_template->height)); + ExtrusionPathFragment{ extrusion_path_template->attributes() }); // Don't use it, it will be released. extrusion_path_template = nullptr; @@ -1242,8 +1237,8 @@ static void modulate_extrusion_by_overlapping_layers( path_fragments.back().polylines = diff_pl(path_fragments.back().polylines, polygons_trimming); // Adjust the extrusion parameters for a reduced layer height and a non-bridging flow (nozzle_dmr = -1, does not matter). assert(this_layer.print_z > overlapping_layer.print_z); - frag.height = float(this_layer.print_z - overlapping_layer.print_z); - frag.mm3_per_mm = Flow(frag.width, frag.height, -1.f).mm3_per_mm(); + frag.flow.height = float(this_layer.print_z - overlapping_layer.print_z); + frag.flow.mm3_per_mm = Flow(frag.flow.width, frag.flow.height, -1.f).mm3_per_mm(); #ifdef SLIC3R_DEBUG svg.draw(frag.polylines, dbg_index_to_color(i_overlapping_layer), scale_(0.1)); #endif /* SLIC3R_DEBUG */ @@ -1326,15 +1321,14 @@ static void modulate_extrusion_by_overlapping_layers( ExtrusionPath *path = multipath.paths.empty() ? nullptr : &multipath.paths.back(); if (path != nullptr) { // Verify whether the path is compatible with the current fragment. - assert(this_layer.layer_type == SupporLayerType::BottomContact || path->height != frag.height || path->mm3_per_mm != frag.mm3_per_mm); - if (path->height != frag.height || path->mm3_per_mm != frag.mm3_per_mm) { + assert(this_layer.layer_type == SupporLayerType::BottomContact || path->height() != frag.flow.height || path->mm3_per_mm() != frag.flow.mm3_per_mm); + if (path->height() != frag.flow.height || path->mm3_per_mm() != frag.flow.mm3_per_mm) path = nullptr; - } // Merging with the previous path. This can only happen if the current layer was reduced by a base layer, which was split into a base and interface layer. } if (path == nullptr) { // Allocate a new path. - multipath.paths.push_back(ExtrusionPath(extrusion_role, frag.mm3_per_mm, frag.width, frag.height)); + multipath.paths.emplace_back(ExtrusionAttributes{ extrusion_role, frag.flow }); path = &multipath.paths.back(); } // The Clipper library may flip the order of the clipped polylines arbitrarily. @@ -1369,8 +1363,8 @@ static void modulate_extrusion_by_overlapping_layers( } // If there are any non-consumed fragments, add them separately. //FIXME this shall not happen, if the Clipper works as expected and all paths split to fragments could be re-connected. - for (auto it_fragment = path_fragments.begin(); it_fragment != path_fragments.end(); ++ it_fragment) - extrusion_entities_append_paths(extrusions_in_out, std::move(it_fragment->polylines), extrusion_role, it_fragment->mm3_per_mm, it_fragment->width, it_fragment->height); + for (ExtrusionPathFragment &fragment : path_fragments) + extrusion_entities_append_paths(extrusions_in_out, std::move(fragment.polylines), { extrusion_role, fragment.flow }); } // Support layer that is covered by some form of dense interface. @@ -1732,7 +1726,8 @@ void generate_support_toolpaths( // Filler and its parameters filler, float(density), // Extrusion parameters - ExtrusionRole::SupportMaterialInterface, interface_flow); + interface_as_base ? ExtrusionRole::SupportMaterial : ExtrusionRole::SupportMaterialInterface, + interface_flow); } }; const bool top_interfaces = config.support_material_interface_layers.value != 0; diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index a21a48b..d91d492 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1023,7 +1023,7 @@ namespace SupportMaterialInternal { assert(expansion_scaled >= 0.f); for (const ExtrusionPath &ep : loop.paths) if (ep.role() == ExtrusionRole::OverhangPerimeter && ! ep.polyline.empty()) { - float exp = 0.5f * (float)scale_(ep.width) + expansion_scaled; + float exp = 0.5f * (float)scale_(ep.width()) + expansion_scaled; if (ep.is_closed()) { if (ep.size() >= 3) { // This is a complete loop. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 7451ba0..3743221 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1,4 +1,3 @@ - #include "TreeSupport.hpp" #include "TreeSupportCommon.hpp" #include "SupportCommon.hpp" @@ -70,7 +69,7 @@ static inline void validate_range(const MultiPoint &mp) validate_range(mp.points); } -static inline void validate_range(const Polygons &polygons) +[[maybe_unused]] static inline void validate_range(const Polygons &polygons) { for (const Polygon &p : polygons) validate_range(p); @@ -94,6 +93,7 @@ static inline void validate_range(const LineInformations &lines) validate_range(l); } +/* static inline void check_self_intersections(const Polygons &polygons, const std::string_view message) { #ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 @@ -101,6 +101,7 @@ static inline void check_self_intersections(const Polygons &polygons, const std: ::MessageBoxA(nullptr, (std::string("TreeSupport infill self intersections: ") + std::string(message)).c_str(), "Bug detected!", MB_OK | MB_SYSTEMMODAL | MB_SETFOREGROUND | MB_ICONWARNING); #endif // TREE_SUPPORT_SHOW_ERRORS_WIN32 } +*/ static inline void check_self_intersections(const ExPolygon &expoly, const std::string_view message) { #ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index e1ae58f..2eee1f1 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -48,45 +48,29 @@ #ifdef DEBUG_FILES #include #include "libslic3r/Color.hpp" +constexpr bool debug_files = true; +#else +constexpr bool debug_files = false; #endif -namespace Slic3r { -class ExtrusionLine -{ -public: - ExtrusionLine() : a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0), origin_entity(nullptr) {} - ExtrusionLine(const Vec2f &a, const Vec2f &b, float len, const ExtrusionEntity *origin_entity) +namespace Slic3r::SupportSpotsGenerator { + +ExtrusionLine::ExtrusionLine() : a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0), origin_entity(nullptr) {} +ExtrusionLine::ExtrusionLine(const Vec2f &a, const Vec2f &b, float len, const ExtrusionEntity *origin_entity) : a(a), b(b), len(len), origin_entity(origin_entity) {} - ExtrusionLine(const Vec2f &a, const Vec2f &b) +ExtrusionLine::ExtrusionLine(const Vec2f &a, const Vec2f &b) : a(a), b(b), len((a-b).norm()), origin_entity(nullptr) {} - bool is_external_perimeter() const +bool ExtrusionLine::is_external_perimeter() const { assert(origin_entity != nullptr); return origin_entity->role().is_external_perimeter(); } - Vec2f a; - Vec2f b; - float len; - const ExtrusionEntity *origin_entity; - - std::optional support_point_generated = {}; - float form_quality = 1.0f; - float curled_up_height = 0.0f; - - static const constexpr int Dim = 2; - using Scalar = Vec2f::Scalar; -}; - -auto get_a(ExtrusionLine &&l) { return l.a; } -auto get_b(ExtrusionLine &&l) { return l.b; } - -namespace SupportSpotsGenerator { using LD = AABBTreeLines::LinesDistancer; @@ -146,14 +130,7 @@ public: } }; -struct SliceConnection -{ - float area{}; - Vec3f centroid_accumulator = Vec3f::Zero(); - Vec2f second_moment_of_area_accumulator = Vec2f::Zero(); - float second_moment_of_area_covariance_accumulator{}; - - void add(const SliceConnection &other) +void SliceConnection::add(const SliceConnection &other) { this->area += other.area; this->centroid_accumulator += other.centroid_accumulator; @@ -161,7 +138,7 @@ struct SliceConnection this->second_moment_of_area_covariance_accumulator += other.second_moment_of_area_covariance_accumulator; } - void print_info(const std::string &tag) +void SliceConnection::print_info(const std::string &tag) const { Vec3f centroid = centroid_accumulator / area; Vec2f variance = (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); @@ -172,7 +149,25 @@ struct SliceConnection std::cout << "variance: " << variance.x() << " " << variance.y() << std::endl; std::cout << "covariance: " << covariance << std::endl; } -}; +Integrals::Integrals (const Polygons& polygons) { + for (const Polygon &polygon : polygons) { + Vec2f p0 = unscaled(polygon.first_point()).cast(); + for (size_t i = 2; i < polygon.points.size(); i++) { + Vec2f p1 = unscaled(polygon.points[i - 1]).cast(); + Vec2f p2 = unscaled(polygon.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + + this->area += sign * area; + this->x_i += sign * first_moment_of_area; + this->x_i_squared += sign * second_moment_area; + this->xy += sign * second_moment_of_area_covariance; + } + } +} SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) { @@ -195,22 +190,11 @@ SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) Polygons overlap = intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(slice_polys, below_bb), ClipperUtils::clip_clipper_polygons_with_subject_bbox(below_polys, slice_bb)); - for (const Polygon &poly : overlap) { - Vec2f p0 = unscaled(poly.first_point()).cast(); - for (size_t i = 2; i < poly.points.size(); i++) { - Vec2f p1 = unscaled(poly.points[i - 1]).cast(); - Vec2f p2 = unscaled(poly.points[i]).cast(); - - float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; - - auto [area, first_moment_of_area, second_moment_area, - second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); - connection.area += sign * area; - connection.centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); - connection.second_moment_of_area_accumulator += sign * second_moment_area; - connection.second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; - } - } + const Integrals integrals{overlap}; + connection.area += integrals.area; + connection.centroid_accumulator += Vec3f(integrals.x_i.x(), integrals.x_i.y(), layer->print_z * integrals.area); + connection.second_moment_of_area_accumulator += integrals.x_i_squared; + connection.second_moment_of_area_covariance_accumulator += integrals.xy; return connection; }; @@ -253,28 +237,6 @@ float get_flow_width(const LayerRegion *region, ExtrusionRole role) return region->flow(FlowRole::frPerimeter).width(); } -std::vector to_short_lines(const ExtrusionEntity *e, float length_limit) -{ - assert(!e->is_collection()); - Polyline pl = e->as_polyline(); - std::vector lines; - lines.reserve(pl.points.size() * 1.5f); - for (int point_idx = 0; point_idx < int(pl.points.size()) - 1; ++point_idx) { - Vec2f start = unscaled(pl.points[point_idx]).cast(); - Vec2f next = unscaled(pl.points[point_idx + 1]).cast(); - Vec2f v = next - start; // vector from next to current - float dist_to_next = v.norm(); - v.normalize(); - int lines_count = int(std::ceil(dist_to_next / length_limit)); - float step_size = dist_to_next / lines_count; - for (int i = 0; i < lines_count; ++i) { - Vec2f a(start + v * (i * step_size)); - Vec2f b(start + v * ((i + 1) * step_size)); - lines.emplace_back(a, b, (a-b).norm(), e); - } - } - return lines; -} float estimate_curled_up_height( float distance, float curvature, float layer_height, float flow_width, float prev_line_curled_height, Params params) @@ -328,9 +290,9 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit return {}; } const float flow_width = get_flow_width(layer_region, entity->role()); - std::vector annotated_points = estimate_points_properties(entity->as_polyline().points, - prev_layer_boundary, flow_width, - params.bridge_distance); + std::vector annotated_points = + ExtrusionProcessor::estimate_points_properties(entity->as_polyline().points, prev_layer_boundary, + flow_width, params.bridge_distance); std::vector lines_out; lines_out.reserve(annotated_points.size()); @@ -339,8 +301,8 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit std::optional bridging_dir{}; for (size_t i = 0; i < annotated_points.size(); ++i) { - ExtendedPoint &curr_point = annotated_points[i]; - const ExtendedPoint &prev_point = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + ExtrusionProcessor::ExtendedPoint &curr_point = annotated_points[i]; + const ExtrusionProcessor::ExtendedPoint &prev_point = i > 0 ? annotated_points[i - 1] : annotated_points[i]; SupportPointCause potential_cause = std::abs(curr_point.curvature) > 0.1 ? SupportPointCause::FloatingBridgeAnchor : SupportPointCause::LongBridge; @@ -382,17 +344,17 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit const float flow_width = get_flow_width(layer_region, entity->role()); // Compute only unsigned distance - prev_layer_lines can contain unconnected paths, thus the sign of the distance is unreliable - std::vector annotated_points = estimate_points_properties(entity->as_polyline().points, - prev_layer_lines, flow_width, - params.bridge_distance); + std::vector annotated_points = + ExtrusionProcessor::estimate_points_properties(entity->as_polyline().points, prev_layer_lines, + flow_width, params.bridge_distance); std::vector lines_out; lines_out.reserve(annotated_points.size()); float bridged_distance = annotated_points.front().position != annotated_points.back().position ? (params.bridge_distance + 1.0f) : 0.0f; for (size_t i = 0; i < annotated_points.size(); ++i) { - ExtendedPoint &curr_point = annotated_points[i]; - const ExtendedPoint &prev_point = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + ExtrusionProcessor::ExtendedPoint &curr_point = annotated_points[i]; + const ExtrusionProcessor::ExtendedPoint &prev_point = i > 0 ? annotated_points[i - 1] : annotated_points[i]; float line_len = (prev_point.position - curr_point.position).norm(); ExtrusionLine line_out{prev_point.position.cast(), curr_point.position.cast(), line_len, entity}; @@ -445,20 +407,92 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } } -class ObjectPart -{ -public: - float volume{}; - Vec3f volume_centroid_accumulator = Vec3f::Zero(); - float sticking_area{}; - Vec3f sticking_centroid_accumulator = Vec3f::Zero(); - Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero(); - float sticking_second_moment_of_area_covariance_accumulator{}; - bool connected_to_bed = false; +/** + * Calculates the second moment of area over an arbitrary polygon. + * + * Important note: The calculated moment is for an axis with origin at + * the polygon centroid! + * + * @param integrals Integrals over the polygon area. + * @param axis_direction Direction of the rotation axis going through centroid. + */ +float compute_second_moment( + const Integrals& integrals, + const Vec2f& axis_direction +) { + // Second moment of area for any axis intersecting coordinate system origin + // can be evaluated using the second moments of area calculated for the coordinate + // system axis and the moment product (int xy). + // The equation is derived appling known formulas for the moment of inertia + // to a plannar problem. One can reason about second moment + // of area by by setting density to 1 in the moment of inertia formulas. + const auto area = integrals.area; + const auto I_xx = integrals.x_i_squared.y(); + const auto I_yy = integrals.x_i_squared.x(); + const auto I_xy = -integrals.xy; - ObjectPart() = default; + const Vec2f centroid = integrals.x_i / area; - void add(const ObjectPart &other) + Matrix2f moment_tensor{}; + moment_tensor << + I_xx, I_xy, + I_xy, I_yy; + + const float moment_at_0_0 = axis_direction.transpose() * moment_tensor * axis_direction; + + // Apply parallel axis theorem to move the moment to centroid + using line_alg::distance_to_infinite_squared; + + const Linef axis_at_0_0 = {{0, 0}, axis_direction.cast()}; + + const double distance = distance_to_infinite_squared(axis_at_0_0, centroid.cast()); + return moment_at_0_0 - area * distance; +} + +ObjectPart::ObjectPart( + const std::vector& extrusion_collections, + const bool connected_to_bed, + const coordf_t print_head_z, + const coordf_t layer_height, + const std::optional& brim +) { + if (connected_to_bed) { + this->connected_to_bed = true; + } + + const auto bottom_z = print_head_z - layer_height; + const auto center_z = print_head_z - layer_height / 2; + + for (const ExtrusionEntityCollection* collection : extrusion_collections) { + if (collection->empty()) { + continue; + } + + const Polygons polygons{collection->polygons_covered_by_width()}; + + const Integrals integrals{polygons}; + const float volume = integrals.area * layer_height; + this->volume += volume; + this->volume_centroid_accumulator += to_3d(integrals.x_i, center_z * integrals.area) / integrals.area * volume; + + if (this->connected_to_bed) { + this->sticking_area += integrals.area; + this->sticking_centroid_accumulator += to_3d(integrals.x_i, bottom_z * integrals.area); + this->sticking_second_moment_of_area_accumulator += integrals.x_i_squared; + this->sticking_second_moment_of_area_covariance_accumulator += integrals.xy; + } + } + + if (brim) { + Integrals integrals{*brim}; + this->sticking_area += integrals.area; + this->sticking_centroid_accumulator += to_3d(integrals.x_i, bottom_z * integrals.area); + this->sticking_second_moment_of_area_accumulator += integrals.x_i_squared; + this->sticking_second_moment_of_area_covariance_accumulator += integrals.xy; + } +} + +void ObjectPart::add(const ObjectPart &other) { this->connected_to_bed = this->connected_to_bed || other.connected_to_bed; this->volume_centroid_accumulator += other.volume_centroid_accumulator; @@ -469,7 +503,7 @@ public: this->sticking_second_moment_of_area_covariance_accumulator += other.sticking_second_moment_of_area_covariance_accumulator; } - void add_support_point(const Vec3f &position, float sticking_area) +void ObjectPart::add_support_point(const Vec3f &position, float sticking_area) { this->sticking_area += sticking_area; this->sticking_centroid_accumulator += sticking_area * position; @@ -477,43 +511,21 @@ public: this->sticking_second_moment_of_area_covariance_accumulator += sticking_area * position.x() * position.y(); } - float compute_directional_xy_variance(const Vec2f &line_dir, - const Vec3f ¢roid_accumulator, - const Vec2f &second_moment_of_area_accumulator, - const float &second_moment_of_area_covariance_accumulator, - const float &area) const - { - assert(area > 0); - Vec3f centroid = centroid_accumulator / area; - Vec2f variance = (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); - float covariance = second_moment_of_area_covariance_accumulator / area - centroid.x() * centroid.y(); - // Var(aX+bY)=a^2*Var(X)+b^2*Var(Y)+2*a*b*Cov(X,Y) - float directional_xy_variance = line_dir.x() * line_dir.x() * variance.x() + line_dir.y() * line_dir.y() * variance.y() + - 2.0f * line_dir.x() * line_dir.y() * covariance; -#ifdef DETAILED_DEBUG_LOGS - BOOST_LOG_TRIVIAL(debug) << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z(); - BOOST_LOG_TRIVIAL(debug) << "variance: " << variance.x() << " " << variance.y(); - BOOST_LOG_TRIVIAL(debug) << "covariance: " << covariance; - BOOST_LOG_TRIVIAL(debug) << "directional_xy_variance: " << directional_xy_variance; -#endif - return directional_xy_variance; - } - float compute_elastic_section_modulus(const Vec2f &line_dir, +float ObjectPart::compute_elastic_section_modulus( + const Vec2f &line_dir, const Vec3f &extreme_point, - const Vec3f ¢roid_accumulator, - const Vec2f &second_moment_of_area_accumulator, - const float &second_moment_of_area_covariance_accumulator, - const float &area) const - { - float directional_xy_variance = compute_directional_xy_variance(line_dir, centroid_accumulator, second_moment_of_area_accumulator, - second_moment_of_area_covariance_accumulator, area); - if (directional_xy_variance < EPSILON) { return 0.0f; } - Vec3f centroid = centroid_accumulator / area; + const Integrals& integrals +) const { + float second_moment_of_area = compute_second_moment(integrals, Vec2f{-line_dir.y(), line_dir.x()}); + + if (second_moment_of_area < EPSILON) { return 0.0f; } + + Vec2f centroid = integrals.x_i / integrals.area; float extreme_fiber_dist = line_alg::distance_to(Linef(centroid.head<2>().cast(), (centroid.head<2>() + Vec2f(line_dir.y(), -line_dir.x())).cast()), extreme_point.head<2>().cast()); - float elastic_section_modulus = area * directional_xy_variance / extreme_fiber_dist; + float elastic_section_modulus = second_moment_of_area / extreme_fiber_dist; #ifdef DETAILED_DEBUG_LOGS BOOST_LOG_TRIVIAL(debug) << "extreme_fiber_dist: " << extreme_fiber_dist; @@ -523,12 +535,18 @@ public: return elastic_section_modulus; } - std::tuple is_stable_while_extruding(const SliceConnection &connection, +std::tuple ObjectPart::is_stable_while_extruding(const SliceConnection &connection, const ExtrusionLine &extruded_line, const Vec3f &extreme_point, float layer_z, const Params ¶ms) const { + // Note that exteme point is calculated for the current layer, while it should + // be computed for the first layer. The shape of the first layer however changes a lot, + // during support points additions (for organic supports it is not even clear how) + // and during merging. Using the current layer is heuristics and also small optimization, + // as the AABB tree for it is calculated anyways. This heuristic should usually be + // on the safe side. Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; float mass = this->volume * params.filament_density; @@ -543,19 +561,17 @@ public: { if (this->sticking_area < EPSILON) return {1.0f, SupportPointCause::UnstableFloatingPart}; + Integrals integrals; + integrals.area = this->sticking_area; + integrals.x_i = this->sticking_centroid_accumulator.head<2>(); + integrals.x_i_squared = this->sticking_second_moment_of_area_accumulator; + integrals.xy = this->sticking_second_moment_of_area_covariance_accumulator; Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; - float bed_yield_torque = -compute_elastic_section_modulus(line_dir, extreme_point, this->sticking_centroid_accumulator, - this->sticking_second_moment_of_area_accumulator, - this->sticking_second_moment_of_area_covariance_accumulator, - this->sticking_area) * - params.get_bed_adhesion_yield_strength(); + float bed_yield_torque = -compute_elastic_section_modulus(line_dir, extreme_point, integrals) * params.get_bed_adhesion_yield_strength(); Vec2f bed_weight_arm = (mass_centroid.head<2>() - bed_centroid.head<2>()); float bed_weight_arm_len = bed_weight_arm.norm(); - float bed_weight_dir_xy_variance = compute_directional_xy_variance(bed_weight_arm, this->sticking_centroid_accumulator, - this->sticking_second_moment_of_area_accumulator, - this->sticking_second_moment_of_area_covariance_accumulator, - this->sticking_area); + float bed_weight_dir_xy_variance = compute_second_moment(integrals, {-bed_weight_arm.y(), bed_weight_arm.x()}) / this->sticking_area; float bed_weight_sign = bed_weight_arm_len < 2.0f * sqrt(bed_weight_dir_xy_variance) ? -1.0f : 1.0f; float bed_weight_torque = bed_weight_sign * bed_weight_arm_len * weight; @@ -595,11 +611,13 @@ public: Vec3f conn_centroid = connection.centroid_accumulator / connection.area; if (layer_z - conn_centroid.z() < 3.0f) { return {-1.0f, SupportPointCause::WeakObjectPart}; } - float conn_yield_torque = compute_elastic_section_modulus(line_dir, extreme_point, connection.centroid_accumulator, - connection.second_moment_of_area_accumulator, - connection.second_moment_of_area_covariance_accumulator, - connection.area) * - params.material_yield_strength; + Integrals integrals; + integrals.area = connection.area; + integrals.x_i = connection.centroid_accumulator.head<2>(); + integrals.x_i_squared = connection.second_moment_of_area_accumulator; + integrals.xy = connection.second_moment_of_area_covariance_accumulator; + + float conn_yield_torque = compute_elastic_section_modulus(line_dir, extreme_point, integrals) * params.material_yield_strength; float conn_weight_arm = (conn_centroid.head<2>() - mass_centroid.head<2>()).norm(); if (layer_z - conn_centroid.z() < 30.0) { @@ -631,92 +649,59 @@ public: return {conn_total_torque / conn_conflict_torque_arm, SupportPointCause::WeakObjectPart}; } } -}; -// return new object part and actual area covered by extrusions -std::tuple build_object_part_from_slice(const size_t &slice_idx, const Layer *layer, const Params& params) -{ - ObjectPart new_object_part; - float area_covered_by_extrusions = 0; - const LayerSlice& slice = layer->lslices_ex.at(slice_idx); - - auto add_extrusions_to_object = [&new_object_part, &area_covered_by_extrusions, ¶ms](const ExtrusionEntity *e, - const LayerRegion *region) { - float flow_width = get_flow_width(region, e->role()); - const Layer *l = region->layer(); - float slice_z = l->slice_z; - float height = l->height; - std::vector lines = to_short_lines(e, 5.0); - for (const ExtrusionLine &line : lines) { - float volume = line.len * height * flow_width * PI / 4.0f; - area_covered_by_extrusions += line.len * flow_width; - new_object_part.volume += volume; - new_object_part.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), slice_z) * volume; - - if (int(l->id()) == params.raft_layers_count) { // layer attached on bed/raft - new_object_part.connected_to_bed = true; - float sticking_area = line.len * flow_width; - new_object_part.sticking_area += sticking_area; - Vec2f middle = Vec2f((line.a + line.b) / 2.0f); - new_object_part.sticking_centroid_accumulator += sticking_area * to_3d(middle, slice_z); - // Bottom infill lines can be quite long, and algined, so the middle approximaton used above does not work - Vec2f dir = (line.b - line.a).normalized(); - float segment_length = flow_width; // segments of size flow_width - for (float segment_middle_dist = std::min(line.len, segment_length * 0.5f); segment_middle_dist < line.len; - segment_middle_dist += segment_length) { - Vec2f segment_middle = line.a + segment_middle_dist * dir; - new_object_part.sticking_second_moment_of_area_accumulator += segment_length * flow_width * - segment_middle.cwiseProduct(segment_middle); - new_object_part.sticking_second_moment_of_area_covariance_accumulator += segment_length * flow_width * - segment_middle.x() * segment_middle.y(); - } - } - } - }; +std::vector gather_extrusions(const LayerSlice& slice, const Layer* layer) { + // TODO reserve might be good, benchmark + std::vector result; for (const auto &island : slice.islands) { const LayerRegion *perimeter_region = layer->get_region(island.perimeters.region()); for (size_t perimeter_idx : island.perimeters) { - for (const ExtrusionEntity *perimeter : - static_cast(perimeter_region->perimeters().entities[perimeter_idx])->entities) { - add_extrusions_to_object(perimeter, perimeter_region); - } + auto collection = static_cast( + perimeter_region->perimeters().entities[perimeter_idx] + ); + result.push_back(collection); } for (const LayerExtrusionRange &fill_range : island.fills) { const LayerRegion *fill_region = layer->get_region(fill_range.region()); for (size_t fill_idx : fill_range) { - for (const ExtrusionEntity *fill : - static_cast(fill_region->fills().entities[fill_idx])->entities) { - add_extrusions_to_object(fill, fill_region); + auto collection = static_cast( + fill_region->fills().entities[fill_idx] + ); + result.push_back(collection); } } + const ExtrusionEntityCollection& collection = perimeter_region->thin_fills(); + result.push_back(&collection); } - for (size_t thin_fill_idx : island.thin_fills) { - add_extrusions_to_object(perimeter_region->thin_fills().entities[thin_fill_idx], perimeter_region); + return result; } +bool has_brim(const Layer* layer, const Params& params){ + return + int(layer->id()) == params.raft_layers_count + && params.raft_layers_count == 0 + && params.brim_type != BrimType::btNoBrim + && params.brim_width > 0.0; } - // BRIM HANDLING - if (int(layer->id()) == params.raft_layers_count && params.raft_layers_count == 0 && params.brim_type != BrimType::btNoBrim && - params.brim_width > 0.0) { - // TODO: The algorithm here should take into account that multiple slices may have coliding Brim areas and the final brim area is - // smaller, +Polygons get_brim(const ExPolygon& slice_polygon, const BrimType brim_type, const float brim_width) { + // TODO: The algorithm here should take into account that multiple slices may + // have coliding Brim areas and the final brim area is smaller, // thus has lower adhesion. For now this effect will be neglected. - ExPolygon slice_poly = layer->lslices[slice_idx]; ExPolygons brim; - if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly) { - Polygon brim_hole = slice_poly.contour; + if (brim_type == BrimType::btOuterAndInner || brim_type == BrimType::btOuterOnly) { + Polygon brim_hole = slice_polygon.contour; brim_hole.reverse(); - Polygons c = expand(slice_poly.contour, scale_(params.brim_width)); // For very small polygons, the expand may result in empty vector, even thought the input is correct. + Polygons c = expand(slice_polygon.contour, scale_(brim_width)); // For very small polygons, the expand may result in empty vector, even thought the input is correct. if (!c.empty()) { brim.push_back(ExPolygon{c.front(), brim_hole}); } } - if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btInnerOnly) { - Polygons brim_contours = slice_poly.holes; + if (brim_type == BrimType::btOuterAndInner || brim_type == BrimType::btInnerOnly) { + Polygons brim_contours = slice_polygon.holes; polygons_reverse(brim_contours); for (const Polygon &brim_contour : brim_contours) { - Polygons brim_holes = shrink({brim_contour}, scale_(params.brim_width)); + Polygons brim_holes = shrink({brim_contour}, scale_(brim_width)); polygons_reverse(brim_holes); ExPolygon inner_brim{brim_contour}; inner_brim.holes = brim_holes; @@ -724,26 +709,7 @@ std::tuple build_object_part_from_slice(const size_t &slice_i } } - for (const Polygon &poly : to_polygons(brim)) { - Vec2f p0 = unscaled(poly.first_point()).cast(); - for (size_t i = 2; i < poly.points.size(); i++) { - Vec2f p1 = unscaled(poly.points[i - 1]).cast(); - Vec2f p2 = unscaled(poly.points[i]).cast(); - - float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; - - auto [area, first_moment_of_area, second_moment_area, - second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); - new_object_part.sticking_area += sign * area; - new_object_part.sticking_centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), - layer->print_z * area); - new_object_part.sticking_second_moment_of_area_accumulator += sign * second_moment_area; - new_object_part.sticking_second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; - } - } - } - - return {new_object_part, area_covered_by_extrusions}; + return to_polygons(brim); } class ActiveObjectParts @@ -785,105 +751,54 @@ public: } }; -std::tuple check_stability(const PrintObject *po, - const PrecomputedSliceConnections &precomputed_slices_connections, - const PrintTryCancel &cancel_func, - const Params ¶ms) +// Function that is used when new support point is generated. It will update the ObjectPart stability, weakest conneciton info, +// and the support presence grid and add the point to the issues. +void reckon_new_support_point(ObjectPart &part, + SliceConnection &weakest_conn, + SupportPoints &supp_points, + SupportGridFilter &supports_presence_grid, + const SupportPoint& support_point, + bool is_global = false) { - SupportPoints supp_points{}; - SupportGridFilter supports_presence_grid(po, params.min_distance_between_support_points); - ActiveObjectParts active_object_parts{}; - PartialObjects partial_objects{}; - LD prev_layer_ext_perim_lines; - - std::unordered_map prev_slice_idx_to_object_part_mapping; - std::unordered_map next_slice_idx_to_object_part_mapping; - std::unordered_map prev_slice_idx_to_weakest_connection; - std::unordered_map next_slice_idx_to_weakest_connection; - - auto remember_partial_object = [&active_object_parts, &partial_objects](size_t object_part_id) { - auto object_part = active_object_parts.access(object_part_id); - if (object_part.volume > EPSILON) { - partial_objects.emplace_back(object_part.volume_centroid_accumulator / object_part.volume, object_part.volume, - object_part.connected_to_bed); + // if position is taken and point is for global stability (force > 0) or we are too close to the bed, do not add + // This allows local support points (e.g. bridging) to be generated densely + if ((supports_presence_grid.position_taken(support_point.position) && is_global)) { + return; } - }; - for (size_t layer_idx = 0; layer_idx < po->layer_count(); ++layer_idx) { - cancel_func(); - const Layer *layer = po->get_layer(layer_idx); - float bottom_z = layer->bottom_z(); - auto create_support_point_position = [bottom_z](const Vec2f &layer_pos) { return Vec3f{layer_pos.x(), layer_pos.y(), bottom_z}; }; - - for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { - const LayerSlice &slice = layer->lslices_ex.at(slice_idx); - auto [new_part, covered_area] = build_object_part_from_slice(slice_idx, layer, params); - const SliceConnection &connection_to_below = precomputed_slices_connections[layer_idx][slice_idx]; - -#ifdef DETAILED_DEBUG_LOGS - std::cout << "SLICE IDX: " << slice_idx << std::endl; - for (const auto &link : slice.overlaps_below) { - std::cout << "connected to slice below: " << link.slice_idx << " by area : " << link.area << std::endl; + float area = support_point.spot_radius * support_point.spot_radius * float(PI); + // add the stability effect of the point only if the spot is not taken, so that the densely created local support points do + // not add unrealistic amount of stability to the object (due to overlaping of local support points) + if (!(supports_presence_grid.position_taken(support_point.position))) { + part.add_support_point(support_point.position, area); } - connection_to_below.print_info("CONNECTION TO BELOW"); -#endif - if (connection_to_below.area < EPSILON) { // new object part emerging - size_t part_id = active_object_parts.insert(new_part); - next_slice_idx_to_object_part_mapping.emplace(slice_idx, part_id); - next_slice_idx_to_weakest_connection.emplace(slice_idx, connection_to_below); - } else { - size_t final_part_id{}; - SliceConnection transfered_weakest_connection{}; - // MERGE parts - { - std::unordered_set parts_ids; - for (const auto &link : slice.overlaps_below) { - size_t part_id = active_object_parts.get_flat_id(prev_slice_idx_to_object_part_mapping.at(link.slice_idx)); - parts_ids.insert(part_id); - transfered_weakest_connection.add(prev_slice_idx_to_weakest_connection.at(link.slice_idx)); - } + supp_points.push_back(support_point); + supports_presence_grid.take_position(support_point.position); - final_part_id = *parts_ids.begin(); - for (size_t part_id : parts_ids) { - if (final_part_id != part_id) { - remember_partial_object(part_id); - active_object_parts.merge(part_id, final_part_id); - } + // The support point also increases the stability of the weakest connection of the object, which should be reflected + if (weakest_conn.area > EPSILON) { // Do not add it to the weakest connection if it is not valid - does not exist + weakest_conn.area += area; + weakest_conn.centroid_accumulator += support_point.position * area; + weakest_conn.second_moment_of_area_accumulator += area * + support_point.position.head<2>().cwiseProduct(support_point.position.head<2>()); + weakest_conn.second_moment_of_area_covariance_accumulator += area * support_point.position.x() * support_point.position.y(); } } - auto estimate_conn_strength = [bottom_z](const SliceConnection &conn) { - if (conn.area < EPSILON) { // connection is empty, does not exists. Return max strength so that it is not picked as the - // weakest connection. - return INFINITY; - } - Vec3f centroid = conn.centroid_accumulator / conn.area; - Vec2f variance = (conn.second_moment_of_area_accumulator / conn.area - - centroid.head<2>().cwiseProduct(centroid.head<2>())); - float xy_variance = variance.x() + variance.y(); - float arm_len_estimate = std::max(1.0f, bottom_z - (conn.centroid_accumulator.z() / conn.area)); - return conn.area * sqrt(xy_variance) / arm_len_estimate; +struct LocalSupports { + std::vector> unstable_lines_per_slice; + std::vector> ext_perim_lines_per_slice; }; -#ifdef DETAILED_DEBUG_LOGS - connection_to_below.print_info("new_weakest_connection"); - transfered_weakest_connection.print_info("transfered_weakest_connection"); -#endif +struct EnitityToCheck +{ + const ExtrusionEntity *e; + const LayerRegion *region; + size_t slice_idx; +}; - if (estimate_conn_strength(transfered_weakest_connection) > estimate_conn_strength(connection_to_below)) { - transfered_weakest_connection = connection_to_below; - } - next_slice_idx_to_weakest_connection.emplace(slice_idx, transfered_weakest_connection); - next_slice_idx_to_object_part_mapping.emplace(slice_idx, final_part_id); - ObjectPart &part = active_object_parts.access(final_part_id); - part.add(new_part); - } - } - - prev_slice_idx_to_object_part_mapping = next_slice_idx_to_object_part_mapping; - next_slice_idx_to_object_part_mapping.clear(); - prev_slice_idx_to_weakest_connection = next_slice_idx_to_weakest_connection; - next_slice_idx_to_weakest_connection.clear(); +// TODO DRY: Very similar to gather extrusions. +std::vector gather_entities_to_check(const Layer* layer) { auto get_flat_entities = [](const ExtrusionEntity *e) { std::vector entities; @@ -902,12 +817,6 @@ std::tuple check_stability(const PrintObject return entities; }; - struct EnitityToCheck - { - const ExtrusionEntity *e; - const LayerRegion *region; - size_t slice_idx; - }; std::vector entities_to_check; for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { const LayerSlice &slice = layer->lslices_ex.at(slice_idx); @@ -931,23 +840,43 @@ std::tuple check_stability(const PrintObject } } } + return entities_to_check; +} - AABBTreeLines::LinesDistancer prev_layer_boundary = layer->lower_layer != nullptr ? - AABBTreeLines::LinesDistancer{ - to_unscaled_linesf(layer->lower_layer->lslices)} : - AABBTreeLines::LinesDistancer{}; +LocalSupports compute_local_supports( + const std::vector& entities_to_check, + const std::optional& previous_layer_boundary, + const LD& prev_layer_ext_perim_lines, + size_t slices_count, + const Params& params +) { + std::vector> unstable_lines_per_slice(slices_count); + std::vector> ext_perim_lines_per_slice(slices_count); - std::vector> unstable_lines_per_slice(layer->lslices_ex.size()); - std::vector> ext_perim_lines_per_slice(layer->lslices_ex.size()); + AABBTreeLines::LinesDistancer prev_layer_boundary_distancer = + (previous_layer_boundary ? AABBTreeLines::LinesDistancer{*previous_layer_boundary} : AABBTreeLines::LinesDistancer{}); + if constexpr (debug_files) { + for (const auto &e_to_check : entities_to_check) { + for (const auto &line : check_extrusion_entity_stability(e_to_check.e, e_to_check.region, prev_layer_ext_perim_lines, + prev_layer_boundary_distancer, params)) { + if (line.support_point_generated.has_value()) { + unstable_lines_per_slice[e_to_check.slice_idx].push_back(line); + } + if (line.is_external_perimeter()) { + ext_perim_lines_per_slice[e_to_check.slice_idx].push_back(line); + } + } + } + } else { tbb::parallel_for(tbb::blocked_range(0, entities_to_check.size()), - [&entities_to_check, &prev_layer_ext_perim_lines, &prev_layer_boundary, &unstable_lines_per_slice, + [&entities_to_check, &prev_layer_ext_perim_lines, &prev_layer_boundary_distancer, &unstable_lines_per_slice, &ext_perim_lines_per_slice, ¶ms](tbb::blocked_range r) { for (size_t entity_idx = r.begin(); entity_idx < r.end(); ++entity_idx) { const auto &e_to_check = entities_to_check[entity_idx]; for (const auto &line : check_extrusion_entity_stability(e_to_check.e, e_to_check.region, prev_layer_ext_perim_lines, - prev_layer_boundary, params)) { + prev_layer_boundary_distancer, params)) { if (line.support_point_generated.has_value()) { unstable_lines_per_slice[e_to_check.slice_idx].push_back(line); } @@ -957,81 +886,207 @@ std::tuple check_stability(const PrintObject } } }); + } + return {unstable_lines_per_slice, ext_perim_lines_per_slice}; +} + +struct SliceMappings +{ + std::unordered_map index_to_object_part_mapping; + std::unordered_map index_to_weakest_connection; +}; + +std::optional to_partial_object(const ObjectPart& part) { + if (part.volume > EPSILON) { + return PartialObject{part.volume_centroid_accumulator / part.volume, part.volume, + part.connected_to_bed}; + } + return {}; +} + +SliceMappings update_active_object_parts(const Layer *layer, + const Params ¶ms, + const std::vector &precomputed_slice_connections, + const SliceMappings &previous_slice_mappings, + ActiveObjectParts &active_object_parts, + PartialObjects &partial_objects) +{ + SliceMappings new_slice_mappings; + for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { + const LayerSlice &slice = layer->lslices_ex.at(slice_idx); + const std::vector extrusion_collections{gather_extrusions(slice, layer)}; + const bool connected_to_bed = int(layer->id()) == params.raft_layers_count; + + const std::optional brim{ + has_brim(layer, params) ? + std::optional{get_brim(layer->lslices[slice_idx], params.brim_type, params.brim_width)} : + std::nullopt + }; + ObjectPart new_part{ + extrusion_collections, + connected_to_bed, + layer->print_z, + layer->height, + brim + }; + + const SliceConnection &connection_to_below = precomputed_slice_connections[slice_idx]; + +#ifdef DETAILED_DEBUG_LOGS + std::cout << "SLICE IDX: " << slice_idx << std::endl; + for (const auto &link : slice.overlaps_below) { + std::cout << "connected to slice below: " << link.slice_idx << " by area : " << link.area << std::endl; + } + connection_to_below.print_info("CONNECTION TO BELOW"); +#endif + + if (connection_to_below.area < EPSILON) { // new object part emerging + size_t part_id = active_object_parts.insert(new_part); + new_slice_mappings.index_to_object_part_mapping.emplace(slice_idx, part_id); + new_slice_mappings.index_to_weakest_connection.emplace(slice_idx, connection_to_below); + } else { + size_t final_part_id{}; + SliceConnection transfered_weakest_connection{}; + // MERGE parts + { + std::unordered_set parts_ids; + for (const auto &link : slice.overlaps_below) { + size_t part_id = active_object_parts.get_flat_id(previous_slice_mappings.index_to_object_part_mapping.at(link.slice_idx)); + parts_ids.insert(part_id); + transfered_weakest_connection.add(previous_slice_mappings.index_to_weakest_connection.at(link.slice_idx)); + } + + final_part_id = *parts_ids.begin(); + for (size_t part_id : parts_ids) { + if (final_part_id != part_id) { + auto object_part = active_object_parts.access(part_id); + if (auto object = to_partial_object(object_part)) { + partial_objects.push_back(std::move(*object)); + } + active_object_parts.merge(part_id, final_part_id); + } + } + } + const float bottom_z = layer->bottom_z(); + auto estimate_conn_strength = [bottom_z](const SliceConnection &conn) { + if (conn.area < EPSILON) { // connection is empty, does not exists. Return max strength so that it is not picked as the + // weakest connection. + return INFINITY; + } + Vec3f centroid = conn.centroid_accumulator / conn.area; + Vec2f variance = (conn.second_moment_of_area_accumulator / conn.area - + centroid.head<2>().cwiseProduct(centroid.head<2>())); + float xy_variance = variance.x() + variance.y(); + float arm_len_estimate = std::max(1.0f, bottom_z - (conn.centroid_accumulator.z() / conn.area)); + return conn.area * sqrt(xy_variance) / arm_len_estimate; + }; + +#ifdef DETAILED_DEBUG_LOGS + connection_to_below.print_info("new_weakest_connection"); + transfered_weakest_connection.print_info("transfered_weakest_connection"); +#endif + + if (estimate_conn_strength(transfered_weakest_connection) > estimate_conn_strength(connection_to_below)) { + transfered_weakest_connection = connection_to_below; + } + new_slice_mappings.index_to_weakest_connection.emplace(slice_idx, transfered_weakest_connection); + new_slice_mappings.index_to_object_part_mapping.emplace(slice_idx, final_part_id); + ObjectPart &part = active_object_parts.access(final_part_id); + part.add(new_part); + } + } + return new_slice_mappings; + } + +void reckon_global_supports(const tbb::concurrent_vector &external_perimeter_lines, + const coordf_t layer_bottom_z, + const Params ¶ms, + ObjectPart &part, + SliceConnection &weakest_connection, + SupportPoints &supp_points, + SupportGridFilter &supports_presence_grid) +{ + LD current_slice_lines_distancer({external_perimeter_lines.begin(), external_perimeter_lines.end()}); + float unchecked_dist = params.min_distance_between_support_points + 1.0f; + + for (const ExtrusionLine &line : external_perimeter_lines) { + if ((unchecked_dist + line.len < params.min_distance_between_support_points && + line.curled_up_height < params.curling_tolerance_limit) || + line.len < EPSILON) { + unchecked_dist += line.len; + } else { + unchecked_dist = line.len; + Vec2f pivot_site_search_point = Vec2f(line.b + (line.b - line.a).normalized() * 300.0f); + auto [dist, nidx, nearest_point] = current_slice_lines_distancer.distance_from_lines_extra(pivot_site_search_point); + Vec3f position = to_3d(nearest_point, layer_bottom_z); + auto [force, cause] = part.is_stable_while_extruding(weakest_connection, line, position, layer_bottom_z, params); + if (force > 0) { + SupportPoint support_point{cause, position, params.support_points_interface_radius}; + reckon_new_support_point(part, weakest_connection, supp_points, supports_presence_grid, support_point, true); + } + } + } + } +std::tuple check_stability(const PrintObject *po, + const PrecomputedSliceConnections &precomputed_slices_connections, + const PrintTryCancel &cancel_func, + const Params ¶ms) +{ + SupportPoints supp_points{}; + SupportGridFilter supports_presence_grid(po, params.min_distance_between_support_points); + ActiveObjectParts active_object_parts{}; + PartialObjects partial_objects{}; + LD prev_layer_ext_perim_lines; + + SliceMappings slice_mappings; + + + for (size_t layer_idx = 0; layer_idx < po->layer_count(); ++layer_idx) { + cancel_func(); + const Layer *layer = po->get_layer(layer_idx); + float bottom_z = layer->bottom_z(); + + slice_mappings = update_active_object_parts(layer, params, precomputed_slices_connections[layer_idx], slice_mappings, active_object_parts, partial_objects); + + std::optional prev_layer_boundary = layer->lower_layer != nullptr ? + std::optional{to_unscaled_linesf(layer->lower_layer->lslices)} : + std::nullopt; + + LocalSupports local_supports{ + compute_local_supports(gather_entities_to_check(layer), prev_layer_boundary, prev_layer_ext_perim_lines, layer->lslices_ex.size(), params)}; std::vector current_layer_ext_perims_lines{}; current_layer_ext_perims_lines.reserve(prev_layer_ext_perim_lines.get_lines().size()); // All object parts updated, and for each slice we have coresponding weakest connection. // We can now check each slice and its corresponding weakest connection and object part for stability. for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { - ObjectPart &part = active_object_parts.access(prev_slice_idx_to_object_part_mapping[slice_idx]); - SliceConnection &weakest_conn = prev_slice_idx_to_weakest_connection[slice_idx]; + ObjectPart &part = active_object_parts.access(slice_mappings.index_to_object_part_mapping[slice_idx]); + SliceConnection &weakest_conn = slice_mappings.index_to_weakest_connection[slice_idx]; -#ifdef DETAILED_DEBUG_LOGS - weakest_conn.print_info("weakest connection info: "); -#endif - // Function that is used when new support point is generated. It will update the ObjectPart stability, weakest conneciton info, - // and the support presence grid and add the point to the issues. - auto reckon_new_support_point = [&part, &weakest_conn, &supp_points, &supports_presence_grid, ¶ms, - &layer_idx](SupportPointCause cause, const Vec3f &support_point, float force, - const Vec2f &dir) { - // if position is taken and point is for global stability (force > 0) or we are too close to the bed, do not add - // This allows local support points (e.g. bridging) to be generated densely - if ((supports_presence_grid.position_taken(support_point) && force > 0) || layer_idx <= 1) { - return; - } - - float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); - // add the stability effect of the point only if the spot is not taken, so that the densely created local support points do - // not add unrealistic amount of stability to the object (due to overlaping of local support points) - if (!(supports_presence_grid.position_taken(support_point))) { - part.add_support_point(support_point, area); - } - - float radius = params.support_points_interface_radius; - supp_points.emplace_back(cause, support_point, force, radius, dir); - supports_presence_grid.take_position(support_point); - - // The support point also increases the stability of the weakest connection of the object, which should be reflected - if (weakest_conn.area > EPSILON) { // Do not add it to the weakest connection if it is not valid - does not exist - weakest_conn.area += area; - weakest_conn.centroid_accumulator += support_point * area; - weakest_conn.second_moment_of_area_accumulator += area * support_point.head<2>().cwiseProduct(support_point.head<2>()); - weakest_conn.second_moment_of_area_covariance_accumulator += area * support_point.x() * support_point.y(); - } - }; - - for (const auto &l : unstable_lines_per_slice[slice_idx]) { - assert(l.support_point_generated.has_value()); - reckon_new_support_point(*l.support_point_generated, create_support_point_position(l.b), float(-EPSILON), Vec2f::Zero()); - } - - LD current_slice_lines_distancer({ext_perim_lines_per_slice[slice_idx].begin(), ext_perim_lines_per_slice[slice_idx].end()}); - float unchecked_dist = params.min_distance_between_support_points + 1.0f; - - for (const ExtrusionLine &line : current_slice_lines_distancer.get_lines()) { - if ((unchecked_dist + line.len < params.min_distance_between_support_points && line.curled_up_height < params.curling_tolerance_limit) || - line.len < EPSILON) { - unchecked_dist += line.len; - } else { - unchecked_dist = line.len; - Vec2f pivot_site_search_point = Vec2f(line.b + (line.b - line.a).normalized() * 300.0f); - auto [dist, nidx, - nearest_point] = current_slice_lines_distancer.distance_from_lines_extra(pivot_site_search_point); - Vec3f support_point = create_support_point_position(nearest_point); - auto [force, cause] = part.is_stable_while_extruding(weakest_conn, line, support_point, bottom_z, params); - if (force > 0) { - reckon_new_support_point(cause, support_point, force, (line.b - line.a).normalized()); - } + if (layer_idx > 1) { + for (const auto &l : local_supports.unstable_lines_per_slice[slice_idx]) { + assert(l.support_point_generated.has_value()); + SupportPoint support_point{*l.support_point_generated, to_3d(l.b, bottom_z), + params.support_points_interface_radius}; + reckon_new_support_point(part, weakest_conn, supp_points, supports_presence_grid, support_point); } } - current_layer_ext_perims_lines.insert(current_layer_ext_perims_lines.end(), current_slice_lines_distancer.get_lines().begin(), - current_slice_lines_distancer.get_lines().end()); + + const tbb::concurrent_vector &external_perimeter_lines = local_supports.ext_perim_lines_per_slice[slice_idx]; + if (layer_idx > 1) { + reckon_global_supports(external_perimeter_lines, bottom_z, params, part, weakest_conn, supp_points, supports_presence_grid); + } + + current_layer_ext_perims_lines.insert(current_layer_ext_perims_lines.end(), external_perimeter_lines.begin(), external_perimeter_lines.end()); } // slice iterations prev_layer_ext_perim_lines = LD(current_layer_ext_perims_lines); } // layer iterations - for (const auto& active_obj_pair : prev_slice_idx_to_object_part_mapping) { - remember_partial_object(active_obj_pair.second); + for (const auto& active_obj_pair : slice_mappings.index_to_object_part_mapping) { + auto object_part = active_object_parts.access(active_obj_pair.second); + if (auto object = to_partial_object(object_part)) { + partial_objects.push_back(std::move(*object)); + } } return {supp_points, partial_objects}; @@ -1107,11 +1162,12 @@ void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, Polygon pol(pl.points); pol.make_counter_clockwise(); - auto annotated_points = estimate_points_properties(pol.points, prev_layer_lines, flow_width); + auto annotated_points = ExtrusionProcessor::estimate_points_properties(pol.points, prev_layer_lines, + flow_width); for (size_t i = 0; i < annotated_points.size(); ++i) { - const ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; - const ExtendedPoint &b = annotated_points[i]; + const ExtrusionProcessor::ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + const ExtrusionProcessor::ExtendedPoint &b = annotated_points[i]; ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), extrusion}; @@ -1182,11 +1238,13 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) Points extrusion_pts; extrusion->collect_points(extrusion_pts); float flow_width = get_flow_width(layer_region, extrusion->role()); - auto annotated_points = estimate_points_properties(extrusion_pts, prev_layer_lines, flow_width, + auto annotated_points = ExtrusionProcessor::estimate_points_properties(extrusion_pts, + prev_layer_lines, + flow_width, params.bridge_distance); for (size_t i = 0; i < annotated_points.size(); ++i) { - const ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; - const ExtendedPoint &b = annotated_points[i]; + const ExtrusionProcessor::ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + const ExtrusionProcessor::ExtendedPoint &b = annotated_points[i]; ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), extrusion}; @@ -1196,7 +1254,8 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) prev_layer_lines.get_line(bottom_line_idx); // correctify the distance sign using slice polygons - float sign = (prev_layer_boundary.distance_from_lines(middle.cast()) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; + float sign = (prev_layer_boundary.distance_from_lines(middle.cast()) + 0.5f * flow_width) < 0.0f ? -1.0f : + 1.0f; line_out.curled_up_height = estimate_curled_up_height(middle_distance * sign, 0.5 * (a.curvature + b.curvature), l->height, flow_width, bottom_line.curled_up_height, params); @@ -1320,4 +1379,3 @@ std::vector> gather_issues(const SupportPoint } } // namespace SupportSpotsGenerator -} // namespace Slic3r diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index a946159..4f7369e 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -100,8 +100,8 @@ enum class SupportPointCause { // Note that the force is only the difference - the amount needed to stabilize the object again. struct SupportPoint { - SupportPoint(SupportPointCause cause, const Vec3f &position, float force, float spot_radius, const Vec2f &direction) - : cause(cause), position(position), force(force), spot_radius(spot_radius), direction(direction) + SupportPoint(SupportPointCause cause, const Vec3f &position, float spot_radius) + : cause(cause), position(position), spot_radius(spot_radius) {} bool is_local_extrusion_support() const @@ -113,17 +113,9 @@ struct SupportPoint SupportPointCause cause; // reason why this support point was generated. Used for the user alerts // position is in unscaled coords. The z coordinate is aligned with the layers bottom_z coordiantes Vec3f position; - // force that destabilizes the object to the point of falling/breaking. g*mm/s^2 units - // It is valid only for global_object_support. For local extrusion support points, the force is -EPSILON - // values gathered from large XL model: Min : 0 | Max : 18713800 | Average : 1361186 | Median : 329103 - // For reference 18713800 is weight of 1.8 Kg object, 329103 is weight of 0.03 Kg - // The final sliced object weight was approx 0.5 Kg - float force; // Expected spot size. The support point strength is calculated from the area defined by this value. // Currently equal to the support_points_interface_radius parameter above float spot_radius; - // direction of the fall of the object (z part is neglected) - Vec2f direction; }; using SupportPoints = std::vector; @@ -143,6 +135,104 @@ struct PartialObject bool connected_to_bed; }; +/** + * Unsacled values of integrals over a polygonal domain. + */ +class Integrals{ + public: + /** + * Construct integral x_i int x_i^2 (i=1,2), xy and integral 1 (area). + * + * @param polygons List of polygons specifing the domain. + */ + explicit Integrals(const Polygons& polygons); + + // TODO refactor and delete the default constructor + Integrals() = default; + + float area{}; + Vec2f x_i{Vec2f::Zero()}; + Vec2f x_i_squared{Vec2f::Zero()}; + float xy{}; +}; + +float compute_second_moment( + const Integrals& integrals, + const Vec2f& axis_direction +); + +class ExtrusionLine +{ +public: + ExtrusionLine(); + ExtrusionLine(const Vec2f &a, const Vec2f &b, float len, const ExtrusionEntity *origin_entity); + ExtrusionLine(const Vec2f &a, const Vec2f &b); + + bool is_external_perimeter() const; + + Vec2f a; + Vec2f b; + float len; + const ExtrusionEntity *origin_entity; + + std::optional support_point_generated = {}; + float form_quality = 1.0f; + float curled_up_height = 0.0f; + + static const constexpr int Dim = 2; + using Scalar = Vec2f::Scalar; +}; + +struct SliceConnection +{ + float area{}; + Vec3f centroid_accumulator = Vec3f::Zero(); + Vec2f second_moment_of_area_accumulator = Vec2f::Zero(); + float second_moment_of_area_covariance_accumulator{}; + + void add(const SliceConnection &other); + + void print_info(const std::string &tag) const; +}; + +Polygons get_brim(const ExPolygon& slice_polygon, const BrimType brim_type, const float brim_width); + +class ObjectPart +{ +public: + float volume{}; + Vec3f volume_centroid_accumulator = Vec3f::Zero(); + float sticking_area{}; + Vec3f sticking_centroid_accumulator = Vec3f::Zero(); + Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero(); + float sticking_second_moment_of_area_covariance_accumulator{}; + bool connected_to_bed = false; + + ObjectPart( + const std::vector& extrusion_collections, + const bool connected_to_bed, + const coordf_t print_head_z, + const coordf_t layer_height, + const std::optional& brim + ); + + void add(const ObjectPart &other); + + void add_support_point(const Vec3f &position, float sticking_area); + + + float compute_elastic_section_modulus( + const Vec2f &line_dir, + const Vec3f &extreme_point, + const Integrals& integrals + ) const; + + std::tuple is_stable_while_extruding(const SliceConnection &connection, + const ExtrusionLine &extruded_line, + const Vec3f &extreme_point, + float layer_z, + const Params ¶ms) const; +}; using PartialObjects = std::vector; // Both support points and partial objects are sorted from the lowest z to the highest diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 09d39c4..ec7cae8 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -52,4 +52,8 @@ // Enable OpenGL debug messages using debug context #define ENABLE_OPENGL_DEBUG_OPTION (1 && ENABLE_GL_CORE_PROFILE) -#endif // _qidislicer_technologies_h_ +// Enable imgui dialog which allows to set the parameters used to export binarized gcode +#define ENABLE_BINARIZED_GCODE_DEBUG_WINDOW 0 + + +#endif // _prusaslicer_technologies_h_ diff --git a/src/libslic3r/TextConfiguration.hpp b/src/libslic3r/TextConfiguration.hpp index 1eaae55..8414dae 100644 --- a/src/libslic3r/TextConfiguration.hpp +++ b/src/libslic3r/TextConfiguration.hpp @@ -26,14 +26,6 @@ struct FontProp // When not set value is zero and is not stored std::optional line_gap; // [in font point] - // Z depth of text - float emboss; // [in mm] - - // Flag that text should use surface cutted from object - // FontProp::distance should without value - // FontProp::emboss should be positive number - // Note: default value is false - bool use_surface; // positive value mean wider character shape // negative value mean tiner character shape @@ -45,16 +37,6 @@ struct FontProp // When not set value is zero and is not stored std::optional skew; // [ration x:y] - // distance from surface point - // used for move over model surface - // When not set value is zero and is not stored - std::optional distance; // [in mm] - - // Angle of rotation around emboss direction (Z axis) - // It is calculate on the fly from volume world transformation - // only StyleManager keep actual value for comparision with style - // When not set value is zero and is not stored - std::optional angle; // [in radians] form -Pi to Pi // Parameter for True Type Font collections // Select index of font in collection @@ -92,54 +74,38 @@ struct FontProp /// /// Y size of text [in mm] /// Z size of text [in mm] - FontProp(float line_height = 10.f, float depth = 2.f) : emboss(depth), size_in_mm(line_height), use_surface(false), per_glyph(false) + FontProp(float line_height = 10.f) : size_in_mm(line_height), per_glyph(false) {} bool operator==(const FontProp& other) const { return char_gap == other.char_gap && line_gap == other.line_gap && - use_surface == other.use_surface && per_glyph == other.per_glyph && align == other.align && - is_approx(emboss, other.emboss) && is_approx(size_in_mm, other.size_in_mm) && is_approx(boldness, other.boldness) && - is_approx(skew, other.skew) && - is_approx(distance, other.distance) && - is_approx(angle, other.angle); + is_approx(skew, other.skew); } // undo / redo stack recovery template void save(Archive &ar) const { - ar(emboss, use_surface, size_in_mm, per_glyph, align.first, align.second); + ar(size_in_mm, per_glyph, align.first, align.second); cereal::save(ar, char_gap); cereal::save(ar, line_gap); cereal::save(ar, boldness); cereal::save(ar, skew); - cereal::save(ar, distance); - cereal::save(ar, angle); cereal::save(ar, collection_number); - cereal::save(ar, family); - cereal::save(ar, face_name); - cereal::save(ar, style); - cereal::save(ar, weight); } template void load(Archive &ar) { - ar(emboss, use_surface, size_in_mm, per_glyph, align.first, align.second); + ar(size_in_mm, per_glyph, align.first, align.second); cereal::load(ar, char_gap); cereal::load(ar, line_gap); cereal::load(ar, boldness); cereal::load(ar, skew); - cereal::load(ar, distance); - cereal::load(ar, angle); cereal::load(ar, collection_number); - cereal::load(ar, family); - cereal::load(ar, face_name); - cereal::load(ar, style); - cereal::load(ar, weight); } }; @@ -193,9 +159,7 @@ struct EmbossStyle } // undo / redo stack recovery - template void serialize(Archive &ar){ - ar(name, path, type, prop); - } + template void serialize(Archive &ar){ ar(name, path, type, prop); } }; // Emboss style name inside vector is unique @@ -216,21 +180,9 @@ struct TextConfiguration // Embossed text value std::string text = "None"; - // !!! Volume stored in .3mf has transformed vertices. - // (baked transformation into vertices position) - // Only place for fill this is when load from .3mf - // This is correct volume transformation - std::optional fix_3mf_tr; // undo / redo stack recovery - template void save(Archive &ar) const{ - ar(text, style); - cereal::save(ar, fix_3mf_tr); - } - template void load(Archive &ar){ - ar(text, style); - cereal::load(ar, fix_3mf_tr); - } + template void serialize(Archive &ar) { ar(style, text); } }; } // namespace Slic3r diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index 236fa22..329c199 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -385,7 +385,7 @@ bool TriangleMesh::has_zero_volume() const const Vec3d sz = size(); const double volume_val = sz.x() * sz.y() * sz.z(); - return is_approx(volume_val, 0.0); + return is_approx(volume_val, 0., 0.1); } std::vector TriangleMesh::split() const diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index da696e1..79b2f31 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1939,7 +1939,7 @@ std::vector slice_mesh_ex( &expolygons); #if 0 -//#ifndef _NDEBUG +//#ifndef NDEBUG // Test whether the expolygons in a single layer overlap. for (size_t i = 0; i < expolygons.size(); ++ i) for (size_t j = i + 1; j < expolygons.size(); ++ j) { @@ -1948,7 +1948,7 @@ std::vector slice_mesh_ex( } #endif #if 0 -//#ifndef _NDEBUG +//#ifndef NDEBUG for (const ExPolygon &ex : expolygons) { assert(! has_duplicate_points(ex.contour)); for (const Polygon &hole : ex.holes) @@ -1956,7 +1956,7 @@ std::vector slice_mesh_ex( assert(! has_duplicate_points(ex)); } assert(!has_duplicate_points(expolygons)); -#endif // _NDEBUG +#endif // NDEBUG //FIXME simplify if (this_mode == MeshSlicingParams::SlicingMode::PositiveLargestContour) keep_largest_contour_only(expolygons); @@ -1968,7 +1968,7 @@ std::vector slice_mesh_ex( expolygons = std::move(simplified); } #if 0 -//#ifndef _NDEBUG +//#ifndef NDEBUG for (const ExPolygon &ex : expolygons) { assert(! has_duplicate_points(ex.contour)); for (const Polygon &hole : ex.holes) @@ -1976,7 +1976,7 @@ std::vector slice_mesh_ex( assert(! has_duplicate_points(ex)); } assert(! has_duplicate_points(expolygons)); -#endif // _NDEBUG +#endif // NDEBUG } }); // BOOST_LOG_TRIVIAL(debug) << "slice_mesh make_expolygons in parallel - end"; diff --git a/src/libslic3r/Triangulation.cpp b/src/libslic3r/Triangulation.cpp index f5978d3..81d8761 100644 --- a/src/libslic3r/Triangulation.cpp +++ b/src/libslic3r/Triangulation.cpp @@ -61,7 +61,7 @@ inline bool has_self_intersection( lines.reserve(constrained_half_edges.size()); for (const auto &he : constrained_half_edges) lines.emplace_back(points[he.first], points[he.second]); - return !intersection_points(lines).empty(); + return !get_intersections(lines).empty(); } } // namespace priv diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 2f5258f..48f9b29 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -53,6 +53,10 @@ const std::string& sys_shapes_dir(); // Return a full path to the custom shapes gallery directory. std::string custom_shapes_dir(); +// Set a path with shapes gallery files. +void set_custom_gcodes_dir(const std::string &path); +// Return a full path to the system shapes gallery directory. +const std::string& custom_gcodes_dir(); // Set a path with preset files. void set_data_dir(const std::string &path); // Return a full path to the GUI resource files. diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index e3ee0bf..2add2c9 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -193,6 +193,17 @@ const std::string& sys_shapes_dir() return g_sys_shapes_dir; } +static std::string g_custom_gcodes_dir; + +void set_custom_gcodes_dir(const std::string &dir) +{ + g_custom_gcodes_dir = dir; +} + +const std::string& custom_gcodes_dir() +{ + return g_custom_gcodes_dir; +} // Translate function callback, to call wxWidgets translate function to convert non-localized UTF8 string to a localized one. Slic3r::I18N::translate_fn_type Slic3r::I18N::translate_fn = nullptr; diff --git a/src/platform/osx/Info.plist.in b/src/platform/osx/Info.plist.in index c99cd9e..dfb7eef 100644 --- a/src/platform/osx/Info.plist.in +++ b/src/platform/osx/Info.plist.in @@ -109,6 +109,23 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + bgcode + BGCODE + + CFBundleTypeIconFile + bgcode.icns + CFBundleTypeName + BGCODE + CFBundleTypeRole + Viewer + LISsAppleDefaultForType + + LSHandlerRank + Alternate + CFBundleURLTypes diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 0a6c7d1..7d8571d 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -18,33 +18,6 @@ set(SLIC3R_GUI_SOURCES GUI/BackgroundSlicingProcess.hpp GUI/BitmapCache.cpp GUI/BitmapCache.hpp - GUI/Widgets/StateColor.cpp - GUI/Widgets/StateColor.hpp - GUI/Widgets/WebView.cpp - GUI/Widgets/WebView.hpp - GUI/Widgets/Label.cpp - GUI/Widgets/Label.hpp - GUI/Widgets/StateHandler.cpp - GUI/Widgets/StateHandler.hpp - GUI/Widgets/StaticBox.cpp - GUI/Widgets/StaticBox.hpp - GUI/Widgets/Button.cpp - GUI/Widgets/Button.hpp - GUI/Widgets/CheckBoxInWT.cpp - GUI/Widgets/CheckBoxInWT.hpp - GUI/Widgets/ComboBox.cpp - GUI/Widgets/ComboBox.hpp - GUI/Widgets/TextInput.cpp - GUI/Widgets/TextInput.hpp - GUI/Widgets/TextCtrl.h - GUI/Widgets/RoundedRectangle.cpp - GUI/Widgets/RoundedRectangle.hpp - GUI/Widgets/RadioBox.cpp - GUI/Widgets/RadioBox.hpp - GUI/Widgets/DropDown.cpp - GUI/Widgets/DropDown.hpp - GUI/Widgets/PopupWindow.cpp - GUI/Widgets/PopupWindow.hpp GUI/ConfigSnapshotDialog.cpp GUI/ConfigSnapshotDialog.hpp GUI/calib_dlg.cpp @@ -96,6 +69,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoSeam.hpp GUI/Gizmos/GLGizmoSimplify.cpp GUI/Gizmos/GLGizmoSimplify.hpp + GUI/Gizmos/GLGizmoSVG.cpp + GUI/Gizmos/GLGizmoSVG.hpp GUI/Gizmos/GLGizmoMmuSegmentation.cpp GUI/Gizmos/GLGizmoMmuSegmentation.hpp GUI/Gizmos/GLGizmoMeasure.cpp @@ -138,6 +113,8 @@ set(SLIC3R_GUI_SOURCES GUI/PresetComboBoxes.cpp GUI/BitmapComboBox.hpp GUI/BitmapComboBox.cpp + GUI/EditGCodeDialog.hpp + GUI/EditGCodeDialog.cpp GUI/SavePresetDialog.hpp GUI/SavePresetDialog.cpp GUI/PhysicalPrinterDialog.hpp @@ -223,6 +200,8 @@ set(SLIC3R_GUI_SOURCES GUI/FirmwareDialog.hpp GUI/PrintHostDialogs.cpp GUI/PrintHostDialogs.hpp + GUI/WifiConfigDialog.cpp + GUI/WifiConfigDialog.hpp GUI/Jobs/Job.hpp GUI/Jobs/Worker.hpp GUI/Jobs/BoostThreadWorker.hpp @@ -274,6 +253,38 @@ set(SLIC3R_GUI_SOURCES GUI/DesktopIntegrationDialog.hpp GUI/HintNotification.cpp GUI/HintNotification.hpp + GUI/Widgets/BitmapToggleButton.cpp + GUI/Widgets/BitmapToggleButton.hpp + GUI/Widgets/Button.cpp + GUI/Widgets/Button.hpp + GUI/Widgets/CheckBox.cpp + GUI/Widgets/CheckBox.hpp + GUI/Widgets/ComboBox.cpp + GUI/Widgets/ComboBox.hpp + GUI/Widgets/DropDown.cpp + GUI/Widgets/DropDown.hpp + GUI/Widgets/StateColor.cpp + GUI/Widgets/StateColor.hpp + GUI/Widgets/WebView.cpp + GUI/Widgets/WebView.hpp + GUI/Widgets/Label.cpp + GUI/Widgets/Label.hpp + GUI/Widgets/StateHandler.cpp + GUI/Widgets/StateHandler.hpp + GUI/Widgets/StaticBox.cpp + GUI/Widgets/StaticBox.hpp + GUI/Widgets/SpinInput.cpp + GUI/Widgets/SpinInput.hpp + GUI/Widgets/SwitchButton.cpp + GUI/Widgets/SwitchButton.hpp + GUI/Widgets/TextInput.cpp + GUI/Widgets/TextInput.hpp + GUI/Widgets/TextCtrl.h + GUI/Widgets/RoundedRectangle.cpp + GUI/Widgets/RoundedRectangle.hpp + GUI/Widgets/PopupWindow.cpp + GUI/Widgets/PopupWindow.hpp + GUI/Widgets/UIColors.hpp GUI/FileArchiveDialog.cpp GUI/FileArchiveDialog.hpp GUI/Downloader.cpp @@ -294,8 +305,6 @@ set(SLIC3R_GUI_SOURCES Utils/Duet.hpp Utils/EmbossStyleManager.cpp Utils/EmbossStyleManager.hpp - Utils/EmbossStylesSerializable.cpp - Utils/EmbossStylesSerializable.hpp Utils/FlashAir.cpp Utils/FlashAir.hpp Utils/FontConfigHelp.cpp @@ -328,6 +337,8 @@ set(SLIC3R_GUI_SOURCES Utils/WinRegistry.hpp Utils/WxFontUtils.cpp Utils/WxFontUtils.hpp + Utils/WifiScanner.hpp + Utils/WifiScanner.cpp ) find_package(NanoSVG REQUIRED) @@ -337,6 +348,8 @@ if (APPLE) Utils/RetinaHelperImpl.mm Utils/MacDarkMode.mm Utils/MacUtils.mm + Utils/WifiScannerMac.h + Utils/WifiScannerMac.mm GUI/RemovableDriveManagerMM.mm GUI/RemovableDriveManagerMM.h GUI/Mouse3DHandlerMac.mm @@ -344,12 +357,13 @@ if (APPLE) GUI/InstanceCheckMac.h ) FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration) + FIND_LIBRARY(COREWLAN_LIBRARY CoreWLAN) endif () add_library(libslic3r_gui STATIC ${SLIC3R_GUI_SOURCES}) if (WIN32) - target_include_directories(libslic3r_gui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include) + target_include_directories(libslic3r_gui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/+WebView2/include) endif() foreach(_source IN ITEMS ${SLIC3R_GUI_SOURCES}) @@ -367,14 +381,14 @@ if (MSVC) elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(libslic3r_gui ${DBUS_LIBRARIES}) elseif (APPLE) - target_link_libraries(libslic3r_gui ${DISKARBITRATION_LIBRARY}) + target_link_libraries(libslic3r_gui ${DISKARBITRATION_LIBRARY} ${COREWLAN_LIBRARY}) endif() -if (SLIC3R_STATIC) +#if (SLIC3R_STATIC) # FIXME: This was previously exported by wx-config but the wxWidgets # cmake build forgets this and the build fails in debug mode (or on raspberry release) - target_compile_definitions(libslic3r_gui PUBLIC -DwxDEBUG_LEVEL=0) -endif() +# target_compile_definitions(libslic3r_gui PUBLIC -DwxDEBUG_LEVEL=0) +#endif() if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE) diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 55b1188..b5d5009 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -212,6 +212,10 @@ const std::array GLVolume::MODEL_COLOR = { { { 0.5f, 1.0f, 0.5f, 1.0f }, { 0.5f, 0.5f, 1.0f, 1.0f } } }; +const ColorRGBA GLVolume::NEGATIVE_VOLUME_COLOR = { 0.2f, 0.2f, 0.2f, 0.5f }; +const ColorRGBA GLVolume::PARAMETER_MODIFIER_COLOR = { 1.0, 1.0f, 0.2f, 0.5f }; +const ColorRGBA GLVolume::SUPPORT_BLOCKER_COLOR = { 1.0f, 0.2f, 0.2f, 0.5f }; +const ColorRGBA GLVolume::SUPPORT_ENFORCER_COLOR = { 0.2f, 0.2f, 1.0f, 0.5f }; GLVolume::GLVolume(float r, float g, float b, float a) : m_sla_shift_z(0.0) @@ -242,7 +246,7 @@ GLVolume::GLVolume(float r, float g, float b, float a) void GLVolume::set_render_color(bool force_transparent) { - bool outside = is_outside || is_below_printbed(); + bool outside = is_outside || (!is_modifier && is_below_printbed()); if (force_native_color || force_neutral_color) { if (outside && shader_outside_printer_detection_enabled) @@ -282,16 +286,13 @@ ColorRGBA color_from_model_volume(const ModelVolume& model_volume) { ColorRGBA color; if (model_volume.is_negative_volume()) - color = { 0.2f, 0.2f, 0.2f, 1.0f }; + color = GLVolume::NEGATIVE_VOLUME_COLOR; else if (model_volume.is_modifier()) - color = { 1.0, 1.0f, 0.2f, 1.0f }; + color = GLVolume::PARAMETER_MODIFIER_COLOR; else if (model_volume.is_support_blocker()) - color = { 1.0f, 0.2f, 0.2f, 1.0f }; + color = GLVolume::SUPPORT_BLOCKER_COLOR; else if (model_volume.is_support_enforcer()) - color = { 0.2f, 0.2f, 1.0f, 1.0f }; - - if (!model_volume.is_model_part()) - color.a(0.5f); + color = GLVolume::SUPPORT_ENFORCER_COLOR; return color; } @@ -1446,8 +1447,8 @@ void _3DScene::extrusionentity_to_verts(const ExtrusionPath& extrusion_path, flo polyline.remove_duplicate_points(); polyline.translate(copy); const Lines lines = polyline.lines(); - std::vector widths(lines.size(), extrusion_path.width); - std::vector heights(lines.size(), extrusion_path.height); + std::vector widths(lines.size(), extrusion_path.width()); + std::vector heights(lines.size(), extrusion_path.height()); thick_lines_to_verts(lines, widths, heights, false, print_z, geometry); } @@ -1463,8 +1464,8 @@ void _3DScene::extrusionentity_to_verts(const ExtrusionLoop& extrusion_loop, flo polyline.translate(copy); const Lines lines_this = polyline.lines(); append(lines, lines_this); - widths.insert(widths.end(), lines_this.size(), extrusion_path.width); - heights.insert(heights.end(), lines_this.size(), extrusion_path.height); + widths.insert(widths.end(), lines_this.size(), extrusion_path.width()); + heights.insert(heights.end(), lines_this.size(), extrusion_path.height()); } thick_lines_to_verts(lines, widths, heights, true, print_z, geometry); } @@ -1481,8 +1482,8 @@ void _3DScene::extrusionentity_to_verts(const ExtrusionMultiPath& extrusion_mult polyline.translate(copy); const Lines lines_this = polyline.lines(); append(lines, lines_this); - widths.insert(widths.end(), lines_this.size(), extrusion_path.width); - heights.insert(heights.end(), lines_this.size(), extrusion_path.height); + widths.insert(widths.end(), lines_this.size(), extrusion_path.width()); + heights.insert(heights.end(), lines_this.size(), extrusion_path.height()); } thick_lines_to_verts(lines, widths, heights, false, print_z, geometry); } diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index e4efd37..5268adc 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -59,6 +59,10 @@ public: static const ColorRGBA SLA_PAD_COLOR; static const ColorRGBA NEUTRAL_COLOR; static const std::array MODEL_COLOR; + static const ColorRGBA NEGATIVE_VOLUME_COLOR; + static const ColorRGBA PARAMETER_MODIFIER_COLOR; + static const ColorRGBA SUPPORT_BLOCKER_COLOR; + static const ColorRGBA SUPPORT_ENFORCER_COLOR; enum EHoverState : unsigned char { diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 13554dc..7b10099 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -3,6 +3,7 @@ #include "GUI.hpp" #include "MainFrame.hpp" #include "format.hpp" +#include "libslic3r/GCode/Thumbnails.hpp" #include #include @@ -175,9 +176,21 @@ void BackgroundSlicingProcess::process_sla() const std::string export_path = m_sla_print->print_statistics().finalize_output_path(m_export_path); - ThumbnailsList thumbnails = this->render_thumbnails( - ThumbnailsParams{current_print()->full_print_config().option("thumbnails")->values, true, true, true, true}); + auto [thumbnails_list, errors] = GCodeThumbnails::make_and_check_thumbnail_list(current_print()->full_print_config()); + if (errors != enum_bitmask()) { + std::string error_str = format("Invalid thumbnails value:"); + error_str += GCodeThumbnails::get_error_string(errors); + throw Slic3r::ExportError(error_str); + } + + Vec2ds sizes; + if (!thumbnails_list.empty()) { + sizes.reserve(thumbnails_list.size()); + for (const auto& [format, size] : thumbnails_list) + sizes.emplace_back(size); + } + ThumbnailsList thumbnails = this->render_thumbnails(ThumbnailsParams{sizes, true, true, true, true }); m_sla_print->export_print(export_path, thumbnails); m_print->set_status(100, GUI::format(_L("Masked SLA file exported to %1%"), export_path)); diff --git a/src/slic3r/GUI/BedShapeDialog.cpp b/src/slic3r/GUI/BedShapeDialog.cpp index 759b176..a342ce4 100644 --- a/src/slic3r/GUI/BedShapeDialog.cpp +++ b/src/slic3r/GUI/BedShapeDialog.cpp @@ -140,7 +140,10 @@ void BedShapeDialog::build_dialog(const ConfigOptionPoints& default_pt, const Co auto main_sizer = new wxBoxSizer(wxVERTICAL); main_sizer->Add(m_panel, 1, wxEXPAND); - main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10); + wxStdDialogButtonSizer* buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxGetApp().SetWindowVariantForButton(buttons->GetAffirmativeButton()); + wxGetApp().SetWindowVariantForButton(buttons->GetCancelButton()); + main_sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10); wxGetApp().UpdateDlgDarkUI(this, true); @@ -204,6 +207,7 @@ void BedShapePanel::build_panel(const ConfigOptionPoints& default_pt, const Conf line.full_width = 1; line.widget = [this](wxWindow* parent) { wxButton* shape_btn = new wxButton(parent, wxID_ANY, _L("Load shape from STL...")); + wxGetApp().SetWindowVariantForButton(shape_btn); wxSizer* shape_sizer = new wxBoxSizer(wxHORIZONTAL); shape_sizer->Add(shape_btn, 1, wxEXPAND); @@ -284,6 +288,7 @@ wxPanel* BedShapePanel::init_texture_panel() line.full_width = 1; line.widget = [this](wxWindow* parent) { wxButton* load_btn = new wxButton(parent, wxID_ANY, _L("Load...")); + wxGetApp().SetWindowVariantForButton(load_btn); wxSizer* load_sizer = new wxBoxSizer(wxHORIZONTAL); load_sizer->Add(load_btn, 1, wxEXPAND); @@ -293,6 +298,7 @@ wxPanel* BedShapePanel::init_texture_panel() filename_sizer->Add(filename_lbl, 1, wxEXPAND); wxButton* remove_btn = new wxButton(parent, wxID_ANY, _L("Remove")); + wxGetApp().SetWindowVariantForButton(remove_btn); wxSizer* remove_sizer = new wxBoxSizer(wxHORIZONTAL); remove_sizer->Add(remove_btn, 1, wxEXPAND); @@ -355,6 +361,7 @@ wxPanel* BedShapePanel::init_model_panel() line.full_width = 1; line.widget = [this](wxWindow* parent) { wxButton* load_btn = new wxButton(parent, wxID_ANY, _L("Load...")); + wxGetApp().SetWindowVariantForButton(load_btn); wxSizer* load_sizer = new wxBoxSizer(wxHORIZONTAL); load_sizer->Add(load_btn, 1, wxEXPAND); @@ -363,6 +370,7 @@ wxPanel* BedShapePanel::init_model_panel() filename_sizer->Add(filename_lbl, 1, wxEXPAND); wxButton* remove_btn = new wxButton(parent, wxID_ANY, _L("Remove")); + wxGetApp().SetWindowVariantForButton(remove_btn); wxSizer* remove_sizer = new wxBoxSizer(wxHORIZONTAL); remove_sizer->Add(remove_btn, 1, wxEXPAND); diff --git a/src/slic3r/GUI/BitmapCache.cpp b/src/slic3r/GUI/BitmapCache.cpp index 4a90575..f2a06e6 100644 --- a/src/slic3r/GUI/BitmapCache.cpp +++ b/src/slic3r/GUI/BitmapCache.cpp @@ -405,9 +405,11 @@ wxBitmapBundle* BitmapCache::from_svg(const std::string& bitmap_name, unsigned t // map of color replaces std::map replaces; if (dark_mode) - replaces["\"#808080\""] = "\"#FFFFFF\""; + replaces["#808080"] = "#FFFFFF"; if (!new_color.empty()) - replaces["\"#ED6B21\""] = "\"" + new_color + "\""; + replaces["#ED6B21"] = new_color; + + replaces["#ButtonBG"] = dark_mode ? "#4E4E4E" : "#828282"; std::string str; nsvgGetDataFromFileWithReplace(Slic3r::var(bitmap_name + ".svg").c_str(), str, replaces); @@ -461,9 +463,9 @@ wxBitmap* BitmapCache::load_svg(const std::string &bitmap_name, unsigned target_ // map of color replaces std::map replaces; if (dark_mode) - replaces["\"#808080\""] = "\"#FFFFFF\""; + replaces["#808080"] = "#FFFFFF"; if (!new_color.empty()) - replaces["\"#ED6B21\""] = "\"" + new_color + "\""; + replaces["#ED6B21"] = new_color; NSVGimage *image = nsvgParseFromFileWithReplace(Slic3r::var(bitmap_name + ".svg").c_str(), "px", 96.0f, replaces); if (image == nullptr) diff --git a/src/slic3r/GUI/BitmapComboBox.cpp b/src/slic3r/GUI/BitmapComboBox.cpp index 55a77cf..f1bb67e 100644 --- a/src/slic3r/GUI/BitmapComboBox.cpp +++ b/src/slic3r/GUI/BitmapComboBox.cpp @@ -62,10 +62,11 @@ BitmapComboBox::BitmapComboBox(wxWindow* parent, int n/* = 0*/, const wxString choices[]/* = NULL*/, long style/* = 0*/) : - wxBitmapComboBox(parent, id, value, pos, size, n, choices, style) +// wxBitmapComboBox(parent, id, value, pos, size, n, choices, style) + ::ComboBox(parent, id, value, pos, size, n, choices, style | DD_NO_CHECK_ICON) { SetFont(Slic3r::GUI::wxGetApp().normal_font()); -#ifdef _WIN32 +#if 0 //#ifdef _WIN32 // Workaround for ignoring CBN_EDITCHANGE events, which are processed after the content of the combo box changes, so that // the index of the item inside CBN_EDITCHANGE may no more be valid. EnableTextChangedEvents(false); @@ -75,11 +76,12 @@ BitmapComboBox::BitmapComboBox(wxWindow* parent, #endif /* _WIN32 */ } +#if 0 BitmapComboBox::~BitmapComboBox() { } -#ifdef _WIN32 +//#ifdef _WIN32 int BitmapComboBox::Append(const wxString& item) { diff --git a/src/slic3r/GUI/BitmapComboBox.hpp b/src/slic3r/GUI/BitmapComboBox.hpp index 545213f..23da5d7 100644 --- a/src/slic3r/GUI/BitmapComboBox.hpp +++ b/src/slic3r/GUI/BitmapComboBox.hpp @@ -4,6 +4,7 @@ #include #include +#include "Widgets/ComboBox.hpp" #include "GUI_Utils.hpp" // --------------------------------- @@ -13,7 +14,8 @@ namespace Slic3r { namespace GUI { // BitmapComboBox used to presets list on Sidebar and Tabs -class BitmapComboBox : public wxBitmapComboBox +//class BitmapComboBox : public wxBitmapComboBox +class BitmapComboBox : public ::ComboBox { public: BitmapComboBox(wxWindow* parent, @@ -24,6 +26,7 @@ BitmapComboBox(wxWindow* parent, int n = 0, const wxString choices[] = NULL, long style = 0); +#if 0 ~BitmapComboBox(); #ifdef _WIN32 @@ -36,7 +39,7 @@ BitmapComboBox(wxWindow* parent, protected: -#ifdef _WIN32 +//#ifdef _WIN32 bool MSWOnDraw(WXDRAWITEMSTRUCT* item) override; void DrawBackground_(wxDC& dc, const wxRect& rect, int WXUNUSED(item), int flags) const; public: diff --git a/src/slic3r/GUI/ButtonsDescription.cpp b/src/slic3r/GUI/ButtonsDescription.cpp index e6fd519..d830a4a 100644 --- a/src/slic3r/GUI/ButtonsDescription.cpp +++ b/src/slic3r/GUI/ButtonsDescription.cpp @@ -74,7 +74,7 @@ wxBitmapBundle * ModePaletteComboBox::get_bmp(const std::vector &pa // Create the bitmap with color bars. std::vector bmps; for (const auto& color : palette) { - bmps.emplace_back(get_bmp_bundle("mode", icon_height, color)); + bmps.emplace_back(get_bmp_bundle("mode", icon_height, icon_height, color)); bmps.emplace_back(get_empty_bmp_bundle(wxOSX ? 5 : 6, icon_height)); } bmp_bndl = bitmap_cache().insert_bndl(bitmap_key, bmps); diff --git a/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/src/slic3r/GUI/ConfigSnapshotDialog.cpp index 1e5c468..e7de751 100644 --- a/src/slic3r/GUI/ConfigSnapshotDialog.cpp +++ b/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -137,7 +137,7 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db // text html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO); { - wxFont font = get_default_font(this); + wxFont font = this->GetFont();// get_default_font(this); #ifdef __WXMSW__ const int fs = font.GetPointSize(); const int fs1 = static_cast(0.8f*fs); @@ -156,7 +156,8 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db } wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxCLOSE); - wxGetApp().UpdateDarkUI(static_cast(this->FindWindowById(wxID_CLOSE, this))); + wxGetApp().SetWindowVariantForButton(buttons->GetCancelButton()); + wxGetApp().UpdateDarkUI(buttons->GetCancelButton()); this->SetEscapeId(wxID_CLOSE); this->Bind(wxEVT_BUTTON, &ConfigSnapshotDialog::onCloseDialog, this, wxID_CLOSE); vsizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3); diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 951d5c7..137a40f 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -415,6 +415,9 @@ PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxSt title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); title_sizer->Add(sel_none); + wxGetApp().SetWindowVariantForButton(sel_all_std); + wxGetApp().SetWindowVariantForButton(sel_all); + wxGetApp().SetWindowVariantForButton(sel_none); wxGetApp().UpdateDarkUI(sel_all_std); wxGetApp().UpdateDarkUI(sel_all); wxGetApp().UpdateDarkUI(sel_none); @@ -742,6 +745,8 @@ PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxStrin wxGetApp().UpdateDarkUI(sel_all); wxGetApp().UpdateDarkUI(sel_none); + wxGetApp().SetWindowVariantForButton(sel_all); + wxGetApp().SetWindowVariantForButton(sel_none); grid->Add(new wxBoxSizer(wxHORIZONTAL)); grid->Add(new wxBoxSizer(wxHORIZONTAL)); grid->Add(new wxBoxSizer(wxHORIZONTAL)); @@ -780,6 +785,14 @@ PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxStrin reload_presets(); set_compatible_printers_html_window(std::vector(), false); } +void PageMaterials::check_and_update_presets(bool force_reload_presets /*= false*/) +{ + if (presets_loaded) + return; + wizard_p()->update_materials(materials->technology); +// if (force_reload_presets) + reload_presets(); +} void PageMaterials::on_paint() { } @@ -845,10 +858,8 @@ void PageMaterials::set_compatible_printers_html_window(const std::vector" "" "" - "" "%s

%s" "
" - "
" "" "" , bgr_clr_str @@ -870,7 +881,6 @@ void PageMaterials::set_compatible_printers_html_window(const std::vector" "" "" - "" "%s

%s" "" "" @@ -891,7 +901,6 @@ void PageMaterials::set_compatible_printers_html_window(const std::vector" "
" "
" - "
" "" "" ); @@ -899,7 +908,7 @@ void PageMaterials::set_compatible_printers_html_window(const std::vectorSetFonts(font.GetFaceName(), font.GetFaceName(), size); @@ -1293,10 +1302,7 @@ void PageMaterials::clear() void PageMaterials::on_activate() { - if (! presets_loaded) { - wizard_p()->update_materials(materials->technology); - reload_presets(); - } + check_and_update_presets(true); first_paint = true; } @@ -1329,8 +1335,7 @@ PageUpdate::PageUpdate(ConfigWizard *parent) , preset_update(true) { const AppConfig *app_config = wxGetApp().app_config; - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); + auto boldfont = wxGetApp().bold_font(); auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); box_slic3r->SetValue(app_config->get("notify_release") != "none"); @@ -1409,6 +1414,7 @@ Worker::Worker(wxWindow* parent) this->Add(m_input_path, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); auto* button_path = new wxButton(m_parent, wxID_ANY, _L("Browse")); + wxGetApp().SetWindowVariantForButton(button_path); this->Add(button_path, 0, wxEXPAND | wxTOP | wxLEFT, 5); button_path->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { boost::filesystem::path chosen_dest(boost::nowide::narrow(m_input_path->GetValue())); @@ -1481,9 +1487,11 @@ PageDownloader::PageDownloader(ConfigWizard* parent) const auto bgr_clr_str = wxGetApp().get_html_bg_color(parent); const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); - const wxString link = format_wxstr("%1%", "qidi3d.com"); + const wxString link = format_wxstr("%1%", "printables.com"); - const wxString main_text = format_wxstr(_L("You can get more information about the printer and %2% from the %1%." + // TRN ConfigWizard : Downloader : %1% = "printables.com", %2% = "PrusaSlicer" + const wxString main_text = format_wxstr(_L("If enabled, you will be able to open models from the %1% " + "online database with a single click (using a %2% logo button)." ), link, SLIC3R_APP_NAME); const wxFont& font = this->GetFont(); @@ -1643,7 +1651,6 @@ PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) append(cb_3mf); append(cb_stl); - append(cb_step); // append(cb_gcode); } #endif // _WIN32 @@ -1967,7 +1974,7 @@ void PageDiameters::apply_custom_config(DynamicPrintConfig &config) set_extrusion_width("solid_infill_extrusion_width", 0.45); } -class SpinCtrlDouble: public wxSpinCtrlDouble +class SpinCtrlDouble: public ::SpinInputDouble { public: SpinCtrlDouble(wxWindow* parent) @@ -1977,10 +1984,7 @@ public: #else long style = wxSP_ARROW_KEYS; #endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this->GetText()); -#endif + Create(parent, "", wxEmptyString, wxDefaultPosition, wxSize(6* wxGetApp().em_unit(), -1), style); this->Refresh(); } ~SpinCtrlDouble() {} @@ -2506,7 +2510,7 @@ void ConfigWizard::priv::load_vendors() } } appconfig_new.set_section(section_name, section_new); - }; + } } void ConfigWizard::priv::add_page(ConfigWizardPage *page) @@ -2588,142 +2592,57 @@ void ConfigWizard::priv::set_run_reason(RunReason run_reason) void ConfigWizard::priv::update_materials(Technology technology) { + auto add_material = [](Materials& materials, PresetAliases& aliases, const Preset& preset, const Preset* printer = nullptr) { + if (!materials.containts(&preset)) { + materials.push(&preset); + if (!preset.alias.empty()) + aliases[preset.alias].emplace(&preset); + } + if (printer) { + materials.add_printer(printer); + materials.compatibility_counter[preset.alias].insert(printer); + } + }; if ((any_fff_selected || custom_printer_in_bundle || custom_printer_selected) && (technology & T_FFF)) { filaments.clear(); aliases_fff.clear(); - // Iterate filaments in all bundles - for (const auto &pair : bundles) { - for (const auto &filament : pair.second.preset_bundle->filaments) { - // Check if filament is already added - if (filaments.containts(&filament)) - continue; + for (const auto &[name, bundle] : bundles) { + for (const auto &filament : bundle.preset_bundle->filaments) { // Iterate printers in all bundles - for (const auto &printer : pair.second.preset_bundle->printers) { + for (const auto &printer : bundle.preset_bundle->printers) { if (!printer.is_visible || printer.printer_technology() != ptFFF) continue; // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { - if (!filaments.containts(&filament)) { - filaments.push(&filament); - if (!filament.alias.empty()) - aliases_fff[filament.alias].insert(filament.name); - } - filaments.add_printer(&printer); - } + if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) + add_material(filaments, aliases_fff, filament, &printer); } // template filament bundle has no printers - filament would be never added - if(pair.second.vendor_profile&& pair.second.vendor_profile->templates_profile && pair.second.preset_bundle->printers.begin() == pair.second.preset_bundle->printers.end()) - { - if (!filaments.containts(&filament)) { - filaments.push(&filament); - if (!filament.alias.empty()) - aliases_fff[filament.alias].insert(filament.name); - } + if(bundle.vendor_profile && bundle.vendor_profile->templates_profile && bundle.preset_bundle->printers.begin() == bundle.preset_bundle->printers.end()) + add_material(filaments, aliases_fff, filament); } } } - // count compatible printers - for (const auto& preset : filaments.presets) { - // skip template filaments - if (preset->vendor && preset->vendor->templates_profile) - continue; - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { - continue; - } - // find all aliases (except templates) - std::vector idx_with_same_alias; - for (size_t i = 0; i < filaments.presets.size(); ++i) { - if (preset->alias == filaments.presets[i]->alias && ((filaments.presets[i]->vendor && !filaments.presets[i]->vendor->templates_profile) || !filaments.presets[i]->vendor)) - idx_with_same_alias.push_back(i); - } - // check compatibility with each printer - size_t counter = 0; - for (const auto& printer : filaments.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) - continue; - bool compatible = false; - // Test other materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - filaments.compatibility_counter.emplace_back(preset->alias, counter); - } - } if (any_sla_selected && (technology & T_SLA)) { sla_materials.clear(); aliases_sla.clear(); // Iterate SLA materials in all bundles - for (const auto &pair : bundles) { - for (const auto &material : pair.second.preset_bundle->sla_materials) { - // Check if material is already added - if (sla_materials.containts(&material)) - continue; + for (const auto& [name, bundle] : bundles) { + for (const auto &material : bundle.preset_bundle->sla_materials) { // Iterate printers in all bundles // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. - for (const auto& printer : pair.second.preset_bundle->printers) { + for (const auto& printer : bundle.preset_bundle->printers) { if(!printer.is_visible || printer.printer_technology() != ptSLA) continue; // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) // Check if material is already added - if(!sla_materials.containts(&material)) { - sla_materials.push(&material); - if (!material.alias.empty()) - aliases_sla[material.alias].insert(material.name); - } - sla_materials.add_printer(&printer); + add_material(sla_materials, aliases_sla, material, &printer); } } } } - // count compatible printers - for (const auto& preset : sla_materials.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < sla_materials.presets.size(); ++i) { - if(preset->alias == sla_materials.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : sla_materials.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - sla_materials.compatibility_counter.emplace_back(preset->alias, counter); - } - } } void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) @@ -2856,21 +2775,14 @@ bool ConfigWizard::priv::on_bnt_finish() index->go_to(page_downloader); return false; } - /* When Filaments or Sla Materials pages are activated, - * materials for this pages are automaticaly updated and presets are reloaded. - * - * But, if _Finish_ button was clicked without activation of those pages - * (for example, just some printers were added/deleted), + /* If some printers were added/deleted, but related MaterialPage wasn't activated, * than last changes wouldn't be updated for filaments/materials. - * SO, do that before close of Wizard + * SO, do that before check_and_install_missing_materials() */ - update_materials(T_ANY); - if (any_fff_selected) - page_filaments->reload_presets(); - if (any_sla_selected) - page_sla_materials->reload_presets(); + page_filaments->check_and_update_presets(); + page_sla_materials->check_and_update_presets(); - // theres no need to check that filament is selected if we have only custom printer + // there's no need to check that filament is selected if we have only custom printer if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; // check, that there is selected at least one filament/material return check_and_install_missing_materials(T_ANY); @@ -3360,8 +3272,8 @@ void ConfigWizard::priv::update_presets_in_config(const std::string& section, co // add or delete presets had a same alias auto it = aliases.find(alias_key); if (it != aliases.end()) - for (const std::string& name : it->second) - update(section, name); + for (const Preset* preset : it->second) + update(section, preset->name); } bool ConfigWizard::priv::check_fff_selected() diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 51700f9..ce141e8 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -27,6 +27,7 @@ #include "SavePresetDialog.hpp" #include "wxExtensions.hpp" +#include "Widgets/SpinInput.hpp" namespace fs = boost::filesystem; @@ -291,6 +292,7 @@ struct PageMaterials: ConfigWizardPage PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name); + void check_and_update_presets(bool force_reload_presets = false); void reload_presets(); void update_lists(int sel_type, int sel_vendor, int last_selected_printer = -1); void on_material_highlighted(int sel_material); @@ -316,8 +318,8 @@ struct Materials Technology technology; // use vector for the presets to purpose of save of presets sorting in the bundle std::vector presets; - // String is alias of material, size_t number of compatible counters - std::vector> compatibility_counter; + // String is alias of material, set is set of compatible printers + std::map> compatibility_counter; std::set types; std::set printers; @@ -351,7 +353,7 @@ struct Materials size_t get_printer_counter(const Preset* preset) { for (auto it : compatibility_counter) { if (it.first == preset->alias) - return it.second; + return it.second.size(); } return 0; } @@ -506,8 +508,8 @@ struct PageDiameters: ConfigWizardPage struct PageTemperatures: ConfigWizardPage { - wxSpinCtrlDouble *spin_extr; - wxSpinCtrlDouble *spin_bed; + ::SpinInputDouble *spin_extr; + ::SpinInputDouble *spin_bed; PageTemperatures(ConfigWizard *parent); virtual void apply_custom_config(DynamicPrintConfig &config); @@ -576,7 +578,7 @@ wxDEFINE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); // ConfigWizard private data -typedef std::map> PresetAliases; +typedef std::map> PresetAliases; struct ConfigWizard::priv { @@ -589,14 +591,14 @@ struct ConfigWizard::priv // PrinterPickers state. Materials filaments; // Holds available filament presets and their types & vendors Materials sla_materials; // Ditto for SLA materials - PresetAliases aliases_fff; // Map of aliase to preset names - PresetAliases aliases_sla; // Map of aliase to preset names + PresetAliases aliases_fff; // Map of alias to material presets + PresetAliases aliases_sla; // Map of alias to material presets std::unique_ptr custom_config; // Backing for custom printer definition bool any_fff_selected; // Used to decide whether to display Filaments page bool any_sla_selected; // Used to decide whether to display SLA Materials page bool custom_printer_selected { false }; // New custom printer is requested bool custom_printer_in_bundle { false }; // Older custom printer already exists when wizard starts - // Set to true if there are none FFF printers on the main FFF page. If true, only SLA printers are shown (not even custum printers) + // Set to true if there are none FFF printers on the main FFF page. If true, only SLA printers are shown (not even custom printers) bool only_sla_mode { false }; bool template_profile_selected { false }; // This bool has one purpose - to tell that template profile should be installed if its not (because it cannot be added to appconfig) diff --git a/src/slic3r/GUI/EditGCodeDialog.cpp b/src/slic3r/GUI/EditGCodeDialog.cpp new file mode 100644 index 0000000..7b166d6 --- /dev/null +++ b/src/slic3r/GUI/EditGCodeDialog.cpp @@ -0,0 +1,726 @@ +#include "EditGCodeDialog.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "MainFrame.hpp" +#include "format.hpp" +#include "Tab.hpp" +#include "wxExtensions.hpp" +#include "BitmapCache.hpp" +#include "ExtraRenderers.hpp" +#include "MsgDialog.hpp" +#include "Plater.hpp" + +#include "libslic3r/PlaceholderParser.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/Print.hpp" + +namespace Slic3r { +namespace GUI { + +//------------------------------------------ +// EditGCodeDialog +//------------------------------------------ + +EditGCodeDialog::EditGCodeDialog(wxWindow* parent, const std::string& key, const std::string& value) : + // TRN: This is title of a dialog. The argument is the name of the currently edited custom G-code. + DPIDialog(parent, wxID_ANY, format_wxstr(_L("Edit Custom G-code (%1%)"), key), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + SetFont(wxGetApp().normal_font()); + wxGetApp().UpdateDarkUI(this); + + int border = 10; + int em = em_unit(); + + // append info line with link on printables.com + wxHtmlWindow* html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxSize(60 * em, 5 * em), wxHW_SCROLLBAR_NEVER); + + html_window->Bind(wxEVT_HTML_LINK_CLICKED, [](wxHtmlLinkEvent& event) { + wxGetApp().open_browser_with_warning_dialog(event.GetLinkInfo().GetHref()); + event.Skip(false); + }); + + const auto text_clr = wxGetApp().get_label_clr_default(); + const auto bgr_clr_str = wxGetApp().get_html_bg_color(parent); + const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); + + //TRN this word-combination is a part of phraze "For more information about placeholders and its use visit our help page" + const wxString link = format_wxstr("%2%", "help.prusa3d.com/article/macros_1775", _L("help page")); + + // TRN ConfigWizard : Downloader : %1% = "help page" + const wxString main_text = format_wxstr(_L("For more information about placeholders and its use visit our %1%."), link); + + const wxFont& font = this->GetFont(); + const int fs = font.GetPointSize(); + int size[] = { fs,fs,fs,fs,fs,fs,fs }; + html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + + html_window->SetPage(format_wxstr( + "" + "%3%" + "" + , bgr_clr_str + , text_clr_str + , main_text + )); + + + wxStaticText* label_top = new wxStaticText(this, wxID_ANY, _L("Built-in placeholders (Double click item to add to G-code)") + ":"); + + auto* grid_sizer = new wxFlexGridSizer(1, 3, 5, 15); + grid_sizer->SetFlexibleDirection(wxBOTH); + + m_params_list = new ParamsViewCtrl(this, wxSize(em * 45, em * 70)); + m_params_list->SetFont(wxGetApp().code_font()); + wxGetApp().UpdateDarkUI(m_params_list); + + m_add_btn = new ScalableButton(this, wxID_ANY, "add_copies"); + m_add_btn->SetToolTip(_L("Add selected placeholder to G-code")); + + m_gcode_editor = new wxTextCtrl(this, wxID_ANY, value, wxDefaultPosition, wxSize(em * 75, em * 70), wxTE_MULTILINE +#ifdef _WIN32 + | wxBORDER_SIMPLE +#endif + ); + m_gcode_editor->SetFont(wxGetApp().code_font()); + m_gcode_editor->SetInsertionPointEnd(); + wxGetApp().UpdateDarkUI(m_gcode_editor); + + grid_sizer->Add(m_params_list, 1, wxEXPAND); + grid_sizer->Add(m_add_btn, 0, wxALIGN_CENTER_VERTICAL); + grid_sizer->Add(m_gcode_editor, 2, wxEXPAND); + + grid_sizer->AddGrowableRow(0, 1); + grid_sizer->AddGrowableCol(0, 1); + grid_sizer->AddGrowableCol(2, 1); + + m_param_label = new wxStaticText(this, wxID_ANY, _L("Select placeholder")); + m_param_label->SetFont(wxGetApp().bold_font()); + + m_param_description = new wxStaticText(this, wxID_ANY, wxEmptyString); + + wxStdDialogButtonSizer* btns = this->CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxGetApp().UpdateDarkUI(this->FindWindowById(wxID_OK, this)); + wxGetApp().UpdateDarkUI(this->FindWindowById(wxID_CANCEL, this)); + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + topSizer->Add(html_window , 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(label_top , 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(grid_sizer , 1, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(m_param_label , 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(m_param_description , 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(btns , 0, wxEXPAND | wxALL, border); + + SetSizer(topSizer); + topSizer->SetSizeHints(this); + + this->Fit(); + this->Layout(); + + this->CenterOnScreen(); + + init_params_list(key); + bind_list_and_button(); +} + +EditGCodeDialog::~EditGCodeDialog() +{ + // To avoid redundant process of wxEVT_DATAVIEW_SELECTION_CHANGED after dialog distroing (on Linux) + // unbind this event from params_list + m_params_list->Unbind(wxEVT_DATAVIEW_SELECTION_CHANGED, &EditGCodeDialog::selection_changed, this); +} + +std::string EditGCodeDialog::get_edited_gcode() const +{ + return into_u8(m_gcode_editor->GetValue()); +} + +static ParamType get_type(const std::string& opt_key, const ConfigOptionDef& opt_def) +{ + return opt_def.is_scalar() ? ParamType::Scalar : ParamType::Vector; +} + +void EditGCodeDialog::init_params_list(const std::string& custom_gcode_name) +{ + const auto& custom_gcode_placeholders = custom_gcode_specific_placeholders(); + const auto& specific_params = custom_gcode_placeholders.count(custom_gcode_name) > 0 ? + custom_gcode_placeholders.at(custom_gcode_name) : t_config_option_keys({}); + + // Add slicing states placeholders + + wxDataViewItem slicing_state = m_params_list->AppendGroup(_L("[Global] Slicing state"), "custom-gcode_slicing-state_global"); + if (!cgp_ro_slicing_states_config_def.empty()) { + wxDataViewItem read_only = m_params_list->AppendSubGroup(slicing_state, _L("Read only"), "lock_closed"); + for (const auto& [opt_key, def]: cgp_ro_slicing_states_config_def.options) + m_params_list->AppendParam(read_only, get_type(opt_key, def), opt_key); + } + + if (!cgp_rw_slicing_states_config_def.empty()) { + wxDataViewItem read_write = m_params_list->AppendSubGroup(slicing_state, _L("Read write"), "lock_open"); + for (const auto& [opt_key, def] : cgp_rw_slicing_states_config_def.options) + m_params_list->AppendParam(read_write, get_type(opt_key, def), opt_key); + } + + // add other universal params, which are related to slicing state + + if (!cgp_other_slicing_states_config_def.empty()) { + slicing_state = m_params_list->AppendGroup(_L("Slicing state"), "custom-gcode_slicing-state"); + for (const auto& [opt_key, def] : cgp_other_slicing_states_config_def.options) + m_params_list->AppendParam(slicing_state, get_type(opt_key, def), opt_key); + } + + // Add universal placeholders + + { + // Add print statistics subgroup + + if (!cgp_print_statistics_config_def.empty()) { + wxDataViewItem statistics = m_params_list->AppendGroup(_L("Print statistics"), "custom-gcode_stats"); + for (const auto& [opt_key, def] : cgp_print_statistics_config_def.options) + m_params_list->AppendParam(statistics, get_type(opt_key, def), opt_key); + } + + // Add objects info subgroup + + if (!cgp_objects_info_config_def.empty()) { + wxDataViewItem objects_info = m_params_list->AppendGroup(_L("Objects info"), "custom-gcode_object-info"); + for (const auto& [opt_key, def] : cgp_objects_info_config_def.options) + m_params_list->AppendParam(objects_info, get_type(opt_key, def), opt_key); + } + + // Add dimensions subgroup + + if (!cgp_dimensions_config_def.empty()) { + wxDataViewItem dimensions = m_params_list->AppendGroup(_L("Dimensions"), "custom-gcode_measure"); + for (const auto& [opt_key, def] : cgp_dimensions_config_def.options) + m_params_list->AppendParam(dimensions, get_type(opt_key, def), opt_key); + } + + // Add timestamp subgroup + + if (!cgp_timestamps_config_def.empty()) { + wxDataViewItem dimensions = m_params_list->AppendGroup(_L("Timestamps"), "time"); + for (const auto& [opt_key, def] : cgp_timestamps_config_def.options) + m_params_list->AppendParam(dimensions, get_type(opt_key, def), opt_key); + } + } + + // Add specific placeholders + + if (!specific_params.empty()) { + // TRN: The argument is the name of currently edited custom gcode. The string starts a section of placeholders only available in this gcode. + wxDataViewItem group = m_params_list->AppendGroup(format_wxstr(_L("Specific for %1%"), custom_gcode_name), "custom-gcode_gcode"); + for (const auto& opt_key : specific_params) + if (custom_gcode_specific_config_def.has(opt_key)) { + auto def = custom_gcode_specific_config_def.get(opt_key); + m_params_list->AppendParam(group, get_type(opt_key, *def), opt_key); + } + m_params_list->Expand(group); + } + + // Add placeholders from presets + + wxDataViewItem presets = add_presets_placeholders(); + // add other params which are related to presets + if (!cgp_other_presets_config_def.empty()) + for (const auto& [opt_key, def] : cgp_other_presets_config_def.options) + m_params_list->AppendParam(presets, get_type(opt_key, def), opt_key); +} + +wxDataViewItem EditGCodeDialog::add_presets_placeholders() +{ + auto get_set_from_vec = [](const std::vector&vec) { + return std::set(vec.begin(), vec.end()); + }; + + const bool is_fff = wxGetApp().plater()->printer_technology() == ptFFF; + const std::set print_options = get_set_from_vec(is_fff ? Preset::print_options() : Preset::sla_print_options()); + const std::set material_options = get_set_from_vec(is_fff ? Preset::filament_options() : Preset::sla_material_options()); + const std::set printer_options = get_set_from_vec(is_fff ? Preset::printer_options() : Preset::sla_printer_options()); + + const auto&full_config = wxGetApp().preset_bundle->full_config(); + + wxDataViewItem group = m_params_list->AppendGroup(_L("Presets"), "cog"); + + wxDataViewItem print = m_params_list->AppendSubGroup(group, _L("Print settings"), "cog"); + for (const auto&opt : print_options) + if (const ConfigOption *optptr = full_config.optptr(opt)) + m_params_list->AppendParam(print, optptr->is_scalar() ? ParamType::Scalar : ParamType::Vector, opt); + + wxDataViewItem material = m_params_list->AppendSubGroup(group, _(is_fff ? L("Filament settings") : L("SLA Materials settings")), is_fff ? "spool" : "resin"); + for (const auto&opt : material_options) + if (const ConfigOption *optptr = full_config.optptr(opt)) + m_params_list->AppendParam(material, optptr->is_scalar() ? ParamType::Scalar : ParamType::FilamentVector, opt); + + wxDataViewItem printer = m_params_list->AppendSubGroup(group, _L("Printer settings"), is_fff ? "printer" : "sla_printer"); + for (const auto&opt : printer_options) + if (const ConfigOption *optptr = full_config.optptr(opt)) + m_params_list->AppendParam(printer, optptr->is_scalar() ? ParamType::Scalar : ParamType::Vector, opt); + + return group; +} + +void EditGCodeDialog::add_selected_value_to_gcode() +{ + const wxString val = m_params_list->GetSelectedValue(); + if (val.IsEmpty()) + return; + + m_gcode_editor->WriteText(m_gcode_editor->GetInsertionPoint() == m_gcode_editor->GetLastPosition() ? "\n" + val : val); + + if (val.Last() == ']') { + const long new_pos = m_gcode_editor->GetInsertionPoint(); + if (val[val.Len() - 2] == '[') + m_gcode_editor->SetInsertionPoint(new_pos - 1); // set cursor into brackets + else + m_gcode_editor->SetSelection(new_pos - 17, new_pos - 1); // select "current_extruder" + } + + m_gcode_editor->SetFocus(); +} + +void EditGCodeDialog::selection_changed(wxDataViewEvent& evt) +{ + wxString label; + wxString description; + + const std::string opt_key = m_params_list->GetSelectedParamKey(); + if (!opt_key.empty()) { + const ConfigOptionDef* def { nullptr }; + + const auto& full_config = wxGetApp().preset_bundle->full_config(); + if (const ConfigDef* config_def = full_config.def(); config_def && config_def->has(opt_key)) { + def = config_def->get(opt_key); + } + else { + for (const ConfigDef* config: std::initializer_list { + &custom_gcode_specific_config_def, + &cgp_ro_slicing_states_config_def, + &cgp_rw_slicing_states_config_def, + &cgp_other_slicing_states_config_def, + &cgp_print_statistics_config_def, + &cgp_objects_info_config_def, + &cgp_dimensions_config_def, + &cgp_timestamps_config_def, + &cgp_other_presets_config_def + }) { + if (config->has(opt_key)) { + def = config->get(opt_key); + break; + } + } + } + + if (def) { + const ConfigOptionType scalar_type = def->is_scalar() ? def->type : static_cast(def->type - coVectorType); + wxString type_str = scalar_type == coNone ? "none" : + scalar_type == coFloat ? "float" : + scalar_type == coInt ? "integer" : + scalar_type == coString ? "string" : + scalar_type == coPercent ? "percent" : + scalar_type == coFloatOrPercent ? "float or percent" : + scalar_type == coPoint ? "point" : + scalar_type == coBool ? "bool" : + scalar_type == coEnum ? "enum" : "undef"; + if (!def->is_scalar()) + type_str += "[]"; + + label = (!def || (def->full_label.empty() && def->label.empty()) ) ? format_wxstr("%1%\n(%2%)", opt_key, type_str) : + (!def->full_label.empty() && !def->label.empty() ) ? + format_wxstr("%1% > %2%\n(%3%)", _(def->full_label), _(def->label), type_str) : + format_wxstr("%1%\n(%2%)", def->label.empty() ? _(def->full_label) : _(def->label), type_str); + + if (def) + description = get_wraped_wxString(_(def->tooltip), 120); + } + else + label = "Undef optptr"; + } + + m_param_label->SetLabel(label); + m_param_description->SetLabel(description); + + Layout(); +} + +void EditGCodeDialog::bind_list_and_button() +{ + m_params_list->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, &EditGCodeDialog::selection_changed, this); + + m_params_list->Bind(wxEVT_DATAVIEW_ITEM_ACTIVATED, [this](wxDataViewEvent& ) { + add_selected_value_to_gcode(); + }); + + m_add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + add_selected_value_to_gcode(); + }); +} + +void EditGCodeDialog::on_dpi_changed(const wxRect&suggested_rect) +{ + const int& em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL }); + + const wxSize& size = wxSize(45 * em, 35 * em); + SetMinSize(size); + + Fit(); + Refresh(); +} + +void EditGCodeDialog::on_sys_color_changed() +{ + m_add_btn->sys_color_changed(); +} + + + +const std::map ParamsInfo { +// Type BitmapName + { ParamType::Scalar, "custom-gcode_single" }, + { ParamType::Vector, "custom-gcode_vector" }, + { ParamType::FilamentVector,"custom-gcode_vector-index" }, +}; + +static void make_bold(wxString& str) +{ +#if defined(SUPPORTS_MARKUP) && !defined(__APPLE__) + str = format_wxstr("%1%", str); +#endif +} + +// ---------------------------------------------------------------------------- +// ParamsModelNode: a node inside ParamsModel +// ---------------------------------------------------------------------------- + +ParamsNode::ParamsNode(const wxString& group_name, const std::string& icon_name) +: icon_name(icon_name) +, text(group_name) +{ + make_bold(text); +} + +ParamsNode::ParamsNode( ParamsNode * parent, + const wxString& sub_group_name, + const std::string& icon_name) + : m_parent(parent) + , icon_name(icon_name) + , text(sub_group_name) +{ + make_bold(text); +} + +ParamsNode::ParamsNode( ParamsNode* parent, + ParamType param_type, + const std::string& param_key) + : m_parent(parent) + , m_param_type(param_type) + , m_container(false) + , param_key(param_key) +{ + text = from_u8(param_key); + if (param_type == ParamType::Vector) + text += "[]"; + else if (param_type == ParamType::FilamentVector) + text += "[current_extruder]"; + + icon_name = ParamsInfo.at(param_type); +} + + +// ---------------------------------------------------------------------------- +// ParamsModel +// ---------------------------------------------------------------------------- + +ParamsModel::ParamsModel() +{ +} + +wxDataViewItem ParamsModel::AppendGroup(const wxString& group_name, + const std::string& icon_name) +{ + m_group_nodes.emplace_back(std::make_unique(group_name, icon_name)); + + wxDataViewItem parent(nullptr); + wxDataViewItem child((void*)m_group_nodes.back().get()); + + ItemAdded(parent, child); + m_ctrl->Expand(parent); + return child; +} + +wxDataViewItem ParamsModel::AppendSubGroup(wxDataViewItem parent, + const wxString& sub_group_name, + const std::string& icon_name) +{ + ParamsNode* parent_node = static_cast(parent.GetID()); + if (!parent_node) + return wxDataViewItem(0); + + parent_node->Append(std::make_unique(parent_node, sub_group_name, icon_name)); + const wxDataViewItem sub_group_item((void*)parent_node->GetChildren().back().get()); + + ItemAdded(parent, sub_group_item); + return sub_group_item; +} + +wxDataViewItem ParamsModel::AppendParam(wxDataViewItem parent, + ParamType param_type, + const std::string& param_key) +{ + ParamsNode* parent_node = static_cast(parent.GetID()); + if (!parent_node) + return wxDataViewItem(0); + + parent_node->Append(std::make_unique(parent_node, param_type, param_key)); + + const wxDataViewItem child_item((void*)parent_node->GetChildren().back().get()); + + ItemAdded(parent, child_item); + return child_item; +} + +wxString ParamsModel::GetParamName(wxDataViewItem item) +{ + if (item.IsOk()) { + ParamsNode* node = static_cast(item.GetID()); + if (node->IsParamNode()) + return node->text; + } + return wxEmptyString; +} + +std::string ParamsModel::GetParamKey(wxDataViewItem item) +{ + if (item.IsOk()) { + ParamsNode* node = static_cast(item.GetID()); + return node->param_key; + } + return std::string(); +} + +wxDataViewItem ParamsModel::Delete(const wxDataViewItem& item) +{ + auto ret_item = wxDataViewItem(nullptr); + ParamsNode* node = static_cast(item.GetID()); + if (!node) // happens if item.IsOk()==false + return ret_item; + + // first remove the node from the parent's array of children; + // NOTE: m_group_nodes is only a vector of _pointers_ + // thus removing the node from it doesn't result in freeing it + ParamsNodePtrArray& children = node->GetChildren(); + // Delete all children + while (!children.empty()) + Delete(wxDataViewItem(children.back().get())); + + auto node_parent = node->GetParent(); + + ParamsNodePtrArray& parents_children = node_parent ? node_parent->GetChildren() : m_group_nodes; + auto it = find_if(parents_children.begin(), parents_children.end(), + [node](std::unique_ptr& child) { return child.get() == node; }); + assert(it != parents_children.end()); + it = parents_children.erase(it); + + if (it != parents_children.end()) + ret_item = wxDataViewItem(it->get()); + + wxDataViewItem parent(node_parent); + // set m_container to FALSE if parent has no child + if (node_parent) { +#ifndef __WXGTK__ + if (node_parent->GetChildren().empty()) + node_parent->SetContainer(false); +#endif //__WXGTK__ + ret_item = parent; + } + + // notify control + ItemDeleted(parent, item); + return ret_item; +} + +void ParamsModel::Clear() +{ + while (!m_group_nodes.empty()) + Delete(wxDataViewItem(m_group_nodes.back().get())); + +} + +void ParamsModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const +{ + assert(item.IsOk()); + + ParamsNode* node = static_cast(item.GetID()); + if (col == (unsigned int)0) +#ifdef __linux__ + variant << wxDataViewIconText(node->text, get_bmp_bundle(node->icon_name)->GetIconFor(m_ctrl->GetParent())); +#else + variant << DataViewBitmapText(node->text, get_bmp_bundle(node->icon_name)->GetBitmapFor(m_ctrl->GetParent())); +#endif //__linux__ + else + wxLogError("DiffModel::GetValue: wrong column %d", col); +} + +bool ParamsModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) +{ + assert(item.IsOk()); + + ParamsNode* node = static_cast(item.GetID()); + if (col == (unsigned int)0) { +#ifdef __linux__ + wxDataViewIconText data; + data << variant; + node->icon = data.GetIcon(); +#else + DataViewBitmapText data; + data << variant; + node->icon = data.GetBitmap(); +#endif + node->text = data.GetText(); + return true; + } + + wxLogError("DiffModel::SetValue: wrong column"); + return false; +} + +wxDataViewItem ParamsModel::GetParent(const wxDataViewItem&item) const +{ + // the invisible root node has no parent + if (!item.IsOk()) + return wxDataViewItem(nullptr); + + ParamsNode* node = static_cast(item.GetID()); + + if (node->IsGroupNode()) + return wxDataViewItem(nullptr); + + return wxDataViewItem((void*)node->GetParent()); +} + +bool ParamsModel::IsContainer(const wxDataViewItem& item) const +{ + // the invisble root node can have children + if (!item.IsOk()) + return true; + + ParamsNode* node = static_cast(item.GetID()); + return node->IsContainer(); +} + +unsigned int ParamsModel::GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const +{ + ParamsNode* parent_node = (ParamsNode*)parent.GetID(); + + if (parent_node == nullptr) { + for (const auto& group : m_group_nodes) + array.Add(wxDataViewItem((void*)group.get())); + } + else { + const ParamsNodePtrArray& children = parent_node->GetChildren(); + for (const std::unique_ptr& child : children) + array.Add(wxDataViewItem((void*)child.get())); + } + + return array.Count(); +} + + +// ---------------------------------------------------------------------------- +// ParamsViewCtrl +// ---------------------------------------------------------------------------- + +ParamsViewCtrl::ParamsViewCtrl(wxWindow *parent, wxSize size) + : wxDataViewCtrl(parent, wxID_ANY, wxDefaultPosition, size, wxDV_SINGLE | wxDV_NO_HEADER// | wxDV_ROW_LINES +#ifdef _WIN32 + | wxBORDER_SIMPLE +#endif + ), + m_em_unit(em_unit(parent)) +{ + wxGetApp().UpdateDVCDarkUI(this); + + model = new ParamsModel(); + this->AssociateModel(model); + model->SetAssociatedControl(this); + +#ifdef __linux__ + wxDataViewIconTextRenderer* rd = new wxDataViewIconTextRenderer(); +#ifdef SUPPORTS_MARKUP + rd->EnableMarkup(true); +#endif + wxDataViewColumn* column = new wxDataViewColumn("", rd, 0, 20 * m_em_unit, wxALIGN_TOP, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_CELL_INERT); +#else + wxDataViewColumn* column = new wxDataViewColumn("", new BitmapTextRenderer(true, wxDATAVIEW_CELL_INERT), 0, 20 * m_em_unit, wxALIGN_TOP, wxDATAVIEW_COL_RESIZABLE); +#endif //__linux__ + this->AppendColumn(column); + this->SetExpanderColumn(column); +} + +wxDataViewItem ParamsViewCtrl::AppendGroup(const wxString& group_name, const std::string& icon_name) +{ + return model->AppendGroup(group_name, icon_name); +} + +wxDataViewItem ParamsViewCtrl::AppendSubGroup( wxDataViewItem parent, + const wxString& sub_group_name, + const std::string& icon_name) +{ + return model->AppendSubGroup(parent, sub_group_name, icon_name); +} + +wxDataViewItem ParamsViewCtrl::AppendParam( wxDataViewItem parent, + ParamType param_type, + const std::string& param_key) +{ + return model->AppendParam(parent, param_type, param_key); +} + +wxString ParamsViewCtrl::GetValue(wxDataViewItem item) +{ + return model->GetParamName(item); +} + +wxString ParamsViewCtrl::GetSelectedValue() +{ + return model->GetParamName(this->GetSelection()); +} + +std::string ParamsViewCtrl::GetSelectedParamKey() +{ + return model->GetParamKey(this->GetSelection()); +} + +void ParamsViewCtrl::CheckAndDeleteIfEmpty(wxDataViewItem item) +{ + wxDataViewItemArray children; + model->GetChildren(item, children); + if (children.IsEmpty()) + model->Delete(item); +} + +void ParamsViewCtrl::Clear() +{ + model->Clear(); +} + +void ParamsViewCtrl::Rescale(int em/* = 0*/) +{ +// model->Rescale(); + Refresh(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/EditGCodeDialog.hpp b/src/slic3r/GUI/EditGCodeDialog.hpp new file mode 100644 index 0000000..a0d1a8f --- /dev/null +++ b/src/slic3r/GUI/EditGCodeDialog.hpp @@ -0,0 +1,234 @@ +#ifndef slic3r_EditGCodeDialog_hpp_ +#define slic3r_EditGCodeDialog_hpp_ + +#include + +#include + +#include "GUI_Utils.hpp" +#include "wxExtensions.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PrintConfig.hpp" + +class wxListBox; +class wxTextCtrl; +class ScalableButton; +class wxStaticText; + +namespace Slic3r { + +namespace GUI { + +class ParamsViewCtrl; + +//------------------------------------------ +// EditGCodeDialog +//------------------------------------------ + +class EditGCodeDialog : public DPIDialog +{ + ParamsViewCtrl* m_params_list {nullptr}; + ScalableButton* m_add_btn {nullptr}; + wxTextCtrl* m_gcode_editor {nullptr}; + wxStaticText* m_param_label {nullptr}; + wxStaticText* m_param_description {nullptr}; + + ReadOnlySlicingStatesConfigDef cgp_ro_slicing_states_config_def; + ReadWriteSlicingStatesConfigDef cgp_rw_slicing_states_config_def; + OtherSlicingStatesConfigDef cgp_other_slicing_states_config_def; + PrintStatisticsConfigDef cgp_print_statistics_config_def; + ObjectsInfoConfigDef cgp_objects_info_config_def; + DimensionsConfigDef cgp_dimensions_config_def; + TimestampsConfigDef cgp_timestamps_config_def; + OtherPresetsConfigDef cgp_other_presets_config_def; + +public: + EditGCodeDialog(wxWindow*parent, const std::string&key, const std::string&value); + ~EditGCodeDialog(); + + std::string get_edited_gcode() const; + + void init_params_list(const std::string& custom_gcode_name); + wxDataViewItem add_presets_placeholders(); + + void add_selected_value_to_gcode(); + void bind_list_and_button(); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + void on_sys_color_changed() override; + + void selection_changed(wxDataViewEvent& evt); +}; + + + + +// ---------------------------------------------------------------------------- +// ParamsModelNode: a node inside ParamsModel +// ---------------------------------------------------------------------------- + +class ParamsNode; +using ParamsNodePtrArray = std::vector>; + +enum class ParamType { + Undef, + Scalar, + Vector, + FilamentVector, +}; + +// On all of 3 different platforms Bitmap+Text icon column looks different +// because of Markup text is missed or not implemented. +// As a temporary workaround, we will use: +// MSW - DataViewBitmapText (our custom renderer wxBitmap + wxString, supported Markup text) +// OSX - -//-, but Markup text is not implemented right now +// GTK - wxDataViewIconText (wxWidgets for GTK renderer wxIcon + wxString, supported Markup text) +class ParamsNode +{ + ParamsNode* m_parent{ nullptr }; + ParamsNodePtrArray m_children; + + ParamType m_param_type{ ParamType::Undef }; + + // TODO/FIXME: + // the GTK version of wxDVC (in particular wxDataViewCtrlInternal::ItemAdded) + // needs to know in advance if a node is or _will be_ a container. + // Thus implementing: + // bool IsContainer() const + // { return m_children.size()>0; } + // doesn't work with wxGTK when DiffModel::AddToClassical is called + // AND the classical node was removed (a new node temporary without children + // would be added to the control) + bool m_container{ true }; + +public: + +#ifdef __linux__ + wxIcon icon; +#else + wxBitmap icon; +#endif //__linux__ + std::string icon_name; + std::string param_key; + wxString text; + + // Group params(root) node + ParamsNode(const wxString& group_name, const std::string& icon_name); + + // sub SlicingState node + ParamsNode(ParamsNode* parent, + const wxString& sub_group_name, + const std::string& icon_name); + + // parametre node + ParamsNode( ParamsNode* parent, + ParamType param_type, + const std::string& param_key); + + bool IsContainer() const { return m_container; } + bool IsGroupNode() const { return m_parent == nullptr; } + bool IsParamNode() const { return m_param_type != ParamType::Undef; } + void SetContainer(bool is_container) { m_container = is_container; } + + ParamsNode* GetParent() { return m_parent; } + ParamsNodePtrArray& GetChildren() { return m_children; } + + void Append(std::unique_ptr child) { m_children.emplace_back(std::move(child)); } +}; + + +// ---------------------------------------------------------------------------- +// ParamsModel +// ---------------------------------------------------------------------------- + +class ParamsModel : public wxDataViewModel +{ + ParamsNodePtrArray m_group_nodes; + wxDataViewCtrl* m_ctrl{ nullptr }; + +public: + + ParamsModel(); + ~ParamsModel() override = default; + + void SetAssociatedControl(wxDataViewCtrl* ctrl) { m_ctrl = ctrl; } + + wxDataViewItem AppendGroup(const wxString& group_name, + const std::string& icon_name); + + wxDataViewItem AppendSubGroup(wxDataViewItem parent, + const wxString& sub_group_name, + const std::string&icon_name); + + wxDataViewItem AppendParam( wxDataViewItem parent, + ParamType param_type, + const std::string& param_key); + + wxDataViewItem Delete(const wxDataViewItem& item); + + wxString GetParamName(wxDataViewItem item); + std::string GetParamKey(wxDataViewItem item); + + void Clear(); + + wxDataViewItem GetParent(const wxDataViewItem& item) const override; + unsigned int GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const override; + + void GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const override; + bool SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) override; + + bool IsContainer(const wxDataViewItem& item) const override; + // Is the container just a header or an item with all columns + // In our case it is an item with all columns + bool HasContainerColumns(const wxDataViewItem& WXUNUSED(item)) const override { return true; } +}; + + +// ---------------------------------------------------------------------------- +// ParamsViewCtrl +// ---------------------------------------------------------------------------- + +class ParamsViewCtrl : public wxDataViewCtrl +{ + int m_em_unit; + +public: + ParamsViewCtrl(wxWindow* parent, wxSize size); + ~ParamsViewCtrl() override { + if (model) { + Clear(); + model->DecRef(); + } + } + + ParamsModel* model{ nullptr }; + + wxDataViewItem AppendGroup(const wxString& group_name, + const std::string& icon_name); + + wxDataViewItem AppendSubGroup(wxDataViewItem parent, + const wxString& sub_group_name, + const std::string&icon_name); + + wxDataViewItem AppendParam( wxDataViewItem parent, + ParamType param_type, + const std::string& param_key); + + wxString GetValue(wxDataViewItem item); + wxString GetSelectedValue(); + std::string GetSelectedParamKey(); + + void CheckAndDeleteIfEmpty(wxDataViewItem item); + + void Clear(); + void Rescale(int em = 0); + + void set_em_unit(int em) { m_em_unit = em; } +}; + + +} // namespace GUI +} // namespace Slic3r + +#endif diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp index b6e12c3..ed7f6cc 100644 --- a/src/slic3r/GUI/ExtraRenderers.cpp +++ b/src/slic3r/GUI/ExtraRenderers.cpp @@ -1,6 +1,7 @@ #include "ExtraRenderers.hpp" #include "wxExtensions.hpp" #include "GUI.hpp" +#include "GUI_App.hpp" #include "I18N.hpp" #include "BitmapComboBox.hpp" #include "Plater.hpp" @@ -150,7 +151,14 @@ bool BitmapTextRenderer::Render(wxRect rect, wxDC *dc, int state) // workaround for Windows DarkMode : Don't respect to the state & wxDATAVIEW_CELL_SELECTED to avoid update of the text color RenderText(m_value.GetText(), xoffset, rect, dc, state & wxDATAVIEW_CELL_SELECTED ? 0 :state); #else + { + wxDataViewCtrl* const view = GetView(); + if (GetAttr().HasFont()) + dc->SetFont(GetAttr().GetEffectiveFont(view->GetFont())); + else + dc->SetFont(view->GetFont()); RenderText(m_value.GetText(), xoffset, rect, dc, state); + } #endif return true; @@ -161,22 +169,22 @@ wxSize BitmapTextRenderer::GetSize() const if (!m_value.GetText().empty()) { wxSize size; -#if defined(SUPPORTS_MARKUP) && defined(wxHAS_GENERIC_DATAVIEWCTRL) - if (m_markupText) - { wxDataViewCtrl* const view = GetView(); wxClientDC dc(view); if (GetAttr().HasFont()) dc.SetFont(GetAttr().GetEffectiveFont(view->GetFont())); + else + dc.SetFont(view->GetFont()); +#if defined(SUPPORTS_MARKUP) && defined(wxHAS_GENERIC_DATAVIEWCTRL) + if (m_markupText) size = m_markupText->Measure(dc); + else +#endif // SUPPORTS_MARKUP && wxHAS_GENERIC_DATAVIEWCTRL + size = dc.GetTextExtent(m_value.GetText()); int lines = m_value.GetText().Freq('\n') + 1; size.SetHeight(size.GetHeight() * lines); - } - else -#endif // SUPPORTS_MARKUP && wxHAS_GENERIC_DATAVIEWCTRL - size = GetTextExtent(m_value.GetText()); if (m_value.GetBitmap().IsOk()) size.x += m_value.GetBitmap().GetWidth() + 4; @@ -319,6 +327,9 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR labelRect.GetTopLeft(), wxSize(labelRect.GetWidth(), -1), 0, nullptr , wxCB_READONLY); +#ifdef _WIN32 + Slic3r::GUI::wxGetApp().UpdateDarkUI(c_editor); +#endif int def_id = get_default_extruder_idx ? get_default_extruder_idx() : 0; c_editor->Append(_L("default"), def_id < 0 ? wxNullBitmap : *icons[def_id]); for (size_t i = 0; i < icons.size(); i++) @@ -345,7 +356,11 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR bool BitmapChoiceRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) { +#ifdef _WIN32 + Slic3r::GUI::BitmapComboBox* c = static_cast(ctrl); +#else wxBitmapComboBox* c = static_cast(ctrl); +#endif int selection = c->GetSelection(); if (selection < 0) return false; diff --git a/src/slic3r/GUI/ExtruderSequenceDialog.cpp b/src/slic3r/GUI/ExtruderSequenceDialog.cpp index e1c6a7c..a2c7129 100644 --- a/src/slic3r/GUI/ExtruderSequenceDialog.cpp +++ b/src/slic3r/GUI/ExtruderSequenceDialog.cpp @@ -18,6 +18,7 @@ #include "MainFrame.hpp" #include "BitmapComboBox.hpp" +#include "Widgets/CheckBox.hpp" namespace Slic3r { namespace GUI { @@ -161,7 +162,7 @@ ExtruderSequenceDialog::ExtruderSequenceDialog(const DoubleSlider::ExtrudersSequ intervals_box_sizer->Add(m_intervals_grid_sizer, 0, wxLEFT, em); option_sizer->Add(intervals_box_sizer, 0, wxEXPAND); - m_random_sequence = new wxCheckBox(this, wxID_ANY, _L("Random sequence")); + m_random_sequence = new ::CheckBox(this, _L("Random sequence")); m_random_sequence->SetValue(m_sequence.random_sequence); m_random_sequence->SetToolTip(_L("If enabled, random sequence of the selected extruders will be used.")); m_random_sequence->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& e) { @@ -169,7 +170,7 @@ ExtruderSequenceDialog::ExtruderSequenceDialog(const DoubleSlider::ExtrudersSequ m_color_repetition->Enable(m_sequence.random_sequence); }); - m_color_repetition = new wxCheckBox(this, wxID_ANY, _L("Allow next color repetition")); + m_color_repetition = new ::CheckBox(this, _L("Allow next color repetition")); m_color_repetition->SetValue(m_sequence.color_repetition); m_color_repetition->SetToolTip(_L("If enabled, a repetition of the next random color will be allowed.")); m_color_repetition->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& e) {m_sequence.color_repetition = e.IsChecked(); }); diff --git a/src/slic3r/GUI/ExtruderSequenceDialog.hpp b/src/slic3r/GUI/ExtruderSequenceDialog.hpp index da19f79..e47c438 100644 --- a/src/slic3r/GUI/ExtruderSequenceDialog.hpp +++ b/src/slic3r/GUI/ExtruderSequenceDialog.hpp @@ -6,7 +6,7 @@ class wxTextCtrl; class wxFlexGridSizer; -class wxCheckBox; +class CheckBox; namespace Slic3r { namespace GUI { @@ -23,8 +23,8 @@ class ExtruderSequenceDialog: public DPIDialog wxTextCtrl* m_interval_by_layers {nullptr}; wxTextCtrl* m_interval_by_mm {nullptr}; - wxCheckBox* m_random_sequence {nullptr}; - wxCheckBox* m_color_repetition{nullptr}; + CheckBox* m_random_sequence {nullptr}; + CheckBox* m_color_repetition{nullptr}; wxFlexGridSizer* m_intervals_grid_sizer {nullptr}; wxFlexGridSizer* m_extruders_grid_sizer {nullptr}; diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 19c1322..bbb1a13 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -8,6 +8,8 @@ #include "format.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/enum_bitmask.hpp" +#include "libslic3r/GCode/Thumbnails.hpp" #include #include @@ -20,13 +22,14 @@ #include "MsgDialog.hpp" #include "BitmapComboBox.hpp" +#include "Widgets/ComboBox.hpp" #ifdef __WXOSX__ #define wxOSX true #else #define wxOSX false #endif -namespace Slic3r { namespace GUI { +namespace Slic3r :: GUI { wxString double_to_string(double const value, const int max_precision /*= 4*/) { @@ -58,16 +61,22 @@ wxString double_to_string(double const value, const int max_precision /*= 4*/) return s; } -wxString get_thumbnails_string(const std::vector& values) +ThumbnailErrors validate_thumbnails_string(wxString& str, const wxString& def_ext = "PNG") { - wxString ret_str; - for (size_t i = 0; i < values.size(); ++ i) { - const Vec2d& el = values[i]; - ret_str += wxString::Format((i == 0) ? "%ix%i" : ", %ix%i", int(el[0]), int(el[1])); - } - return ret_str; + std::string input_string = into_u8(str); + + str.Clear(); + + auto [thumbnails_list, errors] = GCodeThumbnails::make_and_check_thumbnail_list(input_string); + if (!thumbnails_list.empty()) { + const auto& extentions = ConfigOptionEnum::get_enum_names(); + for (const auto& [format, size] : thumbnails_list) + str += format_wxstr("%1%x%2%/%3%, ", size.x(), size.y(), extentions[int(format)]); + str.resize(str.Len() - 2); } + return errors; +} Field::~Field() { @@ -88,7 +97,6 @@ Field::~Field() void Field::PostInitialize() { - auto color = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); switch (m_opt.type) { @@ -173,6 +181,11 @@ void Field::on_back_to_sys_value() m_back_to_sys_value(m_opt_id); } +void Field::on_edit_value() +{ + if (m_fn_edit_value) + m_fn_edit_value(m_opt_id); +} wxString Field::get_tooltip_text(const wxString& default_string) { if (m_opt.tooltip.empty()) @@ -355,66 +368,36 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true } } - m_value = into_u8(str); - break; } - - case coPoints: { - std::vector out_values; - str.Replace(" ", wxEmptyString, true); - if (!str.IsEmpty()) { - bool invalid_val = false; - bool out_of_range_val = false; - wxStringTokenizer thumbnails(str, ","); - while (thumbnails.HasMoreTokens()) { - wxString token = thumbnails.GetNextToken(); - double x, y; - wxStringTokenizer thumbnail(token, "x"); - if (thumbnail.HasMoreTokens()) { - wxString x_str = thumbnail.GetNextToken(); - if (x_str.ToDouble(&x) && thumbnail.HasMoreTokens()) { - wxString y_str = thumbnail.GetNextToken(); - if (y_str.ToDouble(&y) && !thumbnail.HasMoreTokens()) { - //Y18 - if (m_opt_id == "bed_exclude_area") { - if (0 <= x && x <= 1000 && 0 <= y && y <= 1000) { - out_values.push_back(Vec2d(x, y)); - continue; - } + if (m_opt.opt_key == "thumbnails") { + wxString str_out = str; + ThumbnailErrors errors = validate_thumbnails_string(str_out); + if (errors != enum_bitmask()) { + set_value(str_out, true); + wxString error_str; + if (errors.has(ThumbnailError::InvalidVal)) + error_str += format_wxstr(_L("Invalid input format. Expected vector of dimensions in the following format: \"%1%\""), "XxY/EXT, XxY/EXT, ..."); + if (errors.has(ThumbnailError::OutOfRange)) { + if (!error_str.empty()) + error_str += "\n\n"; + error_str += _L("Input value is out of range"); } - else{ - if (0 < x && x < 1000 && 0 < y && y < 1000) { - out_values.push_back(Vec2d(x, y)); - continue; - } - } - - out_of_range_val = true; - break; + if (errors.has(ThumbnailError::InvalidExt)) { + if (!error_str.empty()) + error_str += "\n\n"; + error_str += _L("Some extension in the input is invalid"); } + show_error(m_parent, error_str); } + else if (str_out != str) { + str = str_out; + set_value(str, true); } - invalid_val = true; - break; } - if (out_of_range_val) { - wxString text_value; - if (!m_value.empty()) - text_value = get_thumbnails_string(boost::any_cast>(m_value)); - set_value(text_value, true); - show_error(m_parent, _L("Input value is out of range")); - } - else if (invalid_val) { - wxString text_value; - if (!m_value.empty()) - text_value = get_thumbnails_string(boost::any_cast>(m_value)); - set_value(text_value, true); - show_error(m_parent, format_wxstr(_L("Invalid input format. Expected vector of dimensions in the following format: \"%1%\""),"XxY, XxY, ..." )); - } + m_value = into_u8(str); + break; } - m_value = out_values; - break; } default: break; @@ -494,18 +477,12 @@ void TextCtrl::BUILD() { text_value = vec->get_at(m_opt_idx); break; } - case coPoints: - text_value = get_thumbnails_string(m_opt.get_default_value()->values); - break; default: break; } long style = m_opt.multiline ? wxTE_MULTILINE : wxTE_PROCESS_ENTER; -#ifdef _WIN32 - style |= wxBORDER_SIMPLE; -#endif - auto temp = new wxTextCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size, style); + auto temp = new text_ctrl(m_parent, text_value, "", "", wxDefaultPosition, size, style); if (parent_is_custom_ctrl && m_opt.height < 0) opt_height = (double)temp->GetSize().GetHeight()/m_em_unit; temp->SetFont(m_opt.is_code ? @@ -517,9 +494,6 @@ void TextCtrl::BUILD() { // Only disable background refresh for single line input fields, as they are completely painted over by the edit control. // This does not apply to the multi-line edit field, where the last line and a narrow frame around the text is not cleared. temp->SetBackgroundStyle(wxBG_STYLE_PAINT); -#ifdef __WXOSX__ - temp->OSXDisableAllSmartSubstitutions(); -#endif // __WXOSX__ temp->SetToolTip(get_tooltip_text(text_value)); @@ -576,7 +550,7 @@ bool TextCtrl::value_was_changed() return true; boost::any val = m_value; - wxString ret_str = static_cast(window)->GetValue(); + wxString ret_str = static_cast(window)->GetValue(); // update m_value! // ret_str might be changed inside get_value_by_opt_type get_value_by_opt_type(ret_str); @@ -607,11 +581,11 @@ bool TextCtrl::value_was_changed() void TextCtrl::propagate_value() { - wxString val = dynamic_cast(window)->GetValue(); + wxString val = dynamic_cast(window)->GetValue(); if (m_opt.nullable && val != na_value()) m_last_meaningful_value = val; - if (!is_defined_input_value(window, m_opt.type) ) + if (!is_defined_input_value(window, m_opt.type) ) // on_kill_focus() cause a call of OptionsGroup::reload_config(), // Thus, do it only when it's really needed (when undefined value was input) on_kill_focus(); @@ -625,14 +599,14 @@ void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) const bool m_is_na_val = boost::any_cast(value) == na_value(); if (!m_is_na_val) m_last_meaningful_value = value; - dynamic_cast(window)->SetValue(m_is_na_val ? na_value() : boost::any_cast(value)); + dynamic_cast(window)->SetValue(m_is_na_val ? na_value() : boost::any_cast(value)); } else - dynamic_cast(window)->SetValue(boost::any_cast(value)); + dynamic_cast(window)->SetValue(boost::any_cast(value)); m_disable_change_event = false; if (!change_event) { - wxString ret_str = static_cast(window)->GetValue(); + wxString ret_str = static_cast(window)->GetValue(); /* Update m_value to correct work of next value_was_changed(). * But after checking of entered value, don't fix the "incorrect" value and don't show a warning message, * just clear m_value in this case. @@ -643,19 +617,19 @@ void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) void TextCtrl::set_last_meaningful_value() { - dynamic_cast(window)->SetValue(boost::any_cast(m_last_meaningful_value)); + dynamic_cast(window)->SetValue(boost::any_cast(m_last_meaningful_value)); propagate_value(); } void TextCtrl::set_na_value() { - dynamic_cast(window)->SetValue(na_value()); + dynamic_cast(window)->SetValue(na_value()); propagate_value(); } boost::any& TextCtrl::get_value() { - wxString ret_str = static_cast(window)->GetValue(); + wxString ret_str = static_cast(window)->GetValue(); // update m_value get_value_by_opt_type(ret_str); @@ -673,8 +647,11 @@ void TextCtrl::msw_rescale() size.SetHeight(lround(opt_height*m_em_unit)); if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit); - if (size != wxDefaultSize) - { + if (size != wxDefaultSize) { + if (::TextInput* text_input = dynamic_cast<::TextInput*>(window)) { + text_input->SetCtrlSize(size); + return; + } wxTextCtrl* field = dynamic_cast(window); if (parent_is_custom_ctrl) field->SetSize(size); @@ -684,8 +661,8 @@ void TextCtrl::msw_rescale() } -void TextCtrl::enable() { dynamic_cast(window)->Enable(); dynamic_cast(window)->SetEditable(true); } -void TextCtrl::disable() { dynamic_cast(window)->Disable(); dynamic_cast(window)->SetEditable(false); } +void TextCtrl::enable() { dynamic_cast(window)->Enable(); } +void TextCtrl::disable() { dynamic_cast(window)->Disable();} #ifdef __WXGTK__ void TextCtrl::change_field_value(wxEvent& event) @@ -696,10 +673,69 @@ void TextCtrl::change_field_value(wxEvent& event) }; #endif //__WXGTK__ +wxWindow* CheckBox::GetNewWin(wxWindow* parent, const wxString& label /*= wxEmptyString*/) +{ + if (wxGetApp().suppress_round_corners()) + return new ::CheckBox(parent, label); + + return new ::SwitchButton(parent, label); +} + +void CheckBox::SetValue(wxWindow* win, bool value) +{ + if (wxGetApp().suppress_round_corners()) { + if (::CheckBox* ch_b = dynamic_cast<::CheckBox*>(win)) + ch_b->SetValue(value); + } + else { + if (::SwitchButton* ch_b = dynamic_cast<::SwitchButton*>(win)) + ch_b->SetValue(value); + } +} + +bool CheckBox::GetValue(wxWindow* win) +{ + if (wxGetApp().suppress_round_corners()) + return dynamic_cast<::CheckBox*>(win)->GetValue(); + + return dynamic_cast<::SwitchButton*>(win)->GetValue(); +} + +void CheckBox::Rescale(wxWindow* win) +{ + if (wxGetApp().suppress_round_corners()) + dynamic_cast<::CheckBox*>(win)->Rescale(); + else + dynamic_cast<::SwitchButton*>(win)->Rescale(); +} + +void CheckBox::SysColorChanged(wxWindow* win) +{ + if (!wxGetApp().suppress_round_corners()) + dynamic_cast<::SwitchButton*>(win)->SysColorChange(); +} + +void CheckBox::SetValue(bool value) +{ + if (wxGetApp().suppress_round_corners()) + dynamic_cast<::CheckBox*>(window)->SetValue(value); + else + dynamic_cast<::SwitchButton*>(window)->SetValue(value); +} + +bool CheckBox::GetValue() +{ + if (wxGetApp().suppress_round_corners()) + return dynamic_cast<::CheckBox*>(window)->GetValue(); + + return dynamic_cast<::SwitchButton*>(window)->GetValue(); +} void CheckBox::BUILD() { auto size = wxSize(wxDefaultSize); - if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit); - if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit); + if (m_opt.height >= 0) + size.SetHeight(m_opt.height*m_em_unit); + if (m_opt.width >= 0) + size.SetWidth(m_opt.width*m_em_unit); bool check_value = m_opt.type == coBool ? m_opt.default_value->getBool() : m_opt.type == coBools ? @@ -709,21 +745,29 @@ void CheckBox::BUILD() { m_last_meaningful_value = static_cast(check_value); // Set Label as a string of at least one space simbol to correct system scaling of a CheckBox - auto temp = new wxCheckBox(m_parent, wxID_ANY, wxString(" "), wxDefaultPosition, size); - temp->SetFont(Slic3r::GUI::wxGetApp().normal_font()); - if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT); - temp->SetValue(check_value); - if (m_opt.readonly) temp->Disable(); + window = GetNewWin(m_parent); + wxGetApp().UpdateDarkUI(window); + window->SetFont(wxGetApp().normal_font()); + if (!wxOSX) + window->SetBackgroundStyle(wxBG_STYLE_PAINT); + if (m_opt.readonly) + window->Disable(); - temp->Bind(wxEVT_CHECKBOX, ([this](wxCommandEvent e) { + SetValue(check_value); + + window->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent e) { m_is_na_val = false; on_change_field(); - }), temp->GetId()); + }); - temp->SetToolTip(get_tooltip_text(check_value ? "true" : "false")); + window->SetToolTip(get_tooltip_text(check_value ? "true" : "false")); +} - // recast as a wxWindow to fit the calling convention - window = dynamic_cast(temp); +void CheckBox::set_value(const bool value, bool change_event/* = false*/) +{ + m_disable_change_event = !change_event; + SetValue(value); + m_disable_change_event = false; } void CheckBox::set_value(const boost::any& value, bool change_event) @@ -733,10 +777,10 @@ void CheckBox::set_value(const boost::any& value, bool change_event) m_is_na_val = boost::any_cast(value) == ConfigOptionBoolsNullable::nil_value(); if (!m_is_na_val) m_last_meaningful_value = value; - dynamic_cast(window)->SetValue(m_is_na_val ? false : boost::any_cast(value) != 0); + SetValue(m_is_na_val ? false : boost::any_cast(value) != 0); } else - dynamic_cast(window)->SetValue(boost::any_cast(value)); + SetValue(boost::any_cast(value)); m_disable_change_event = false; } @@ -744,7 +788,7 @@ void CheckBox::set_last_meaningful_value() { if (m_opt.nullable) { m_is_na_val = false; - dynamic_cast(window)->SetValue(boost::any_cast(m_last_meaningful_value) != 0); + SetValue(boost::any_cast(m_last_meaningful_value) != 0); on_change_field(); } } @@ -753,15 +797,13 @@ void CheckBox::set_na_value() { if (m_opt.nullable) { m_is_na_val = true; - dynamic_cast(window)->SetValue(false); on_change_field(); } } boost::any& CheckBox::get_value() { -// boost::any m_value; - bool value = dynamic_cast(window)->GetValue(); + bool value = GetValue(); if (m_opt.type == coBool) m_value = static_cast(value); else @@ -775,6 +817,22 @@ void CheckBox::msw_rescale() window->SetInitialSize(window->GetBestSize()); } +void CheckBox::sys_color_changed() +{ + Field::sys_color_changed(); + if (auto switch_btn = dynamic_cast<::SwitchButton*>(window)) + switch_btn->SysColorChange(); +} + +void CheckBox::enable() +{ + window->Enable(); +} + +void CheckBox::disable() +{ + window->Disable(); +} void SpinCtrl::BUILD() { auto size = wxSize(def_width() * m_em_unit, wxDefaultCoord); @@ -806,23 +864,11 @@ void SpinCtrl::BUILD() { if (default_value != UNDEF_VALUE) text_value = wxString::Format(_T("%i"), default_value); - const int min_val = m_opt.min == -FLT_MAX -#ifdef __WXOSX__ - // We will forcibly set the input value for SpinControl, since the value - // inserted from the keyboard is not updated under OSX. - // So, we can't set min control value bigger then 0. - // Otherwise, it couldn't be possible to input from keyboard value - // less then min_val. - || m_opt.min > 0 -#endif - ? (int)0 : (int)m_opt.min; + const int min_val = m_opt.min == -FLT_MAX ? (int)0 : (int)m_opt.min; const int max_val = m_opt.max < FLT_MAX ? (int)m_opt.max : INT_MAX; - auto temp = new wxSpinCtrl(m_parent, wxID_ANY, text_value, wxDefaultPosition, size, + auto temp = new ::SpinInput(m_parent, text_value, "", wxDefaultPosition, size, wxTE_PROCESS_ENTER | wxSP_ARROW_KEYS -#ifdef _WIN32 - | wxBORDER_SIMPLE -#endif , min_val, max_val, default_value); #ifdef __WXGTK3__ @@ -834,17 +880,11 @@ void SpinCtrl::BUILD() { if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT); wxGetApp().UpdateDarkUI(temp); - if (m_opt.height < 0 && parent_is_custom_ctrl) + if (m_opt.height < 0 && parent_is_custom_ctrl) { opt_height = (double)temp->GetSize().GetHeight() / m_em_unit; + } -// XXX: On OS X the wxSpinCtrl widget is made up of two subwidgets, unfortunatelly -// the kill focus event is not propagated to the encompassing widget, -// so we need to bind it on the inner text widget instead. (Ugh.) -#ifdef __WXOSX__ - temp->GetText()->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) -#else temp->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) -#endif { e.Skip(); if (bEnterPressed) { @@ -863,29 +903,17 @@ void SpinCtrl::BUILD() { propagate_value(); bEnterPressed = true; }), temp->GetId()); + temp->SetToolTip(get_tooltip_text(text_value)); - temp->Bind(wxEVT_TEXT, ([this, temp](wxCommandEvent e) - { -// # On OSX / Cocoa, wxSpinCtrl::GetValue() doesn't return the new value -// # when it was changed from the text control, so the on_change callback -// # gets the old one, and on_kill_focus resets the control to the old value. -// # As a workaround, we get the new value from $event->GetString and store -// # here temporarily so that we can return it from get_value() + temp->Bind(wxEVT_TEXT, [this, temp](wxCommandEvent e) { long value; - const bool parsed = e.GetString().ToLong(&value); - if (!parsed || value < INT_MIN || value > INT_MAX) + if (!e.GetString().ToLong(&value)) + return; + if (value < INT_MIN || value > INT_MAX) tmp_value = UNDEF_VALUE; else { tmp_value = std::min(std::max((int)value, temp->GetMin()), temp->GetMax()); -#ifdef __WXOSX__ - // Forcibly set the input value for SpinControl, since the value - // inserted from the keyboard or clipboard is not updated under OSX - temp->SetValue(tmp_value); - // But in SetValue() is executed m_text_ctrl->SelectAll(), so - // discard this selection and set insertion point to the end of string - temp->GetText()->SetInsertionPointEnd(); -#else // update value for the control only if it was changed in respect to the Min/max values if (tmp_value != (int)value) { temp->SetValue(tmp_value); @@ -894,11 +922,8 @@ void SpinCtrl::BUILD() { int pos = std::to_string(tmp_value).length(); temp->SetSelection(pos, pos); } -#endif } - }), temp->GetId()); - - temp->SetToolTip(get_tooltip_text(text_value)); + }, temp->GetId()); // recast as a wxWindow to fit the calling convention window = dynamic_cast(temp); @@ -912,35 +937,35 @@ void SpinCtrl::set_value(const boost::any& value, bool change_event/* = false*/) if (m_opt.nullable) { const bool m_is_na_val = tmp_value == ConfigOptionIntsNullable::nil_value(); if (m_is_na_val) - dynamic_cast(window)->SetValue(na_value(true)); + dynamic_cast<::SpinInput*>(window)->SetValue(na_value(true)); else { m_last_meaningful_value = value; - dynamic_cast(window)->SetValue(tmp_value); + dynamic_cast<::SpinInput*>(window)->SetValue(tmp_value); } } else - dynamic_cast(window)->SetValue(tmp_value); + dynamic_cast<::SpinInput*>(window)->SetValue(tmp_value); m_disable_change_event = false; } void SpinCtrl::set_last_meaningful_value() { const int val = boost::any_cast(m_last_meaningful_value); - dynamic_cast(window)->SetValue(val); + dynamic_cast<::SpinInput*>(window)->SetValue(val); tmp_value = val; propagate_value(); } void SpinCtrl::set_na_value() { - dynamic_cast(window)->SetValue(na_value(true)); + dynamic_cast<::SpinInput*>(window)->SetValue(na_value(true)); m_value = ConfigOptionIntsNullable::nil_value(); propagate_value(); } boost::any& SpinCtrl::get_value() { - wxSpinCtrl* spin = static_cast(window); + ::SpinInput* spin = static_cast<::SpinInput*>(window); if (spin->GetTextValue() == na_value(true)) return m_value; @@ -960,14 +985,6 @@ void SpinCtrl::propagate_value() if (tmp_value == UNDEF_VALUE) { on_kill_focus(); } else { -#ifdef __WXOSX__ - // check input value for minimum - if (m_opt.min > 0 && tmp_value < m_opt.min) { - wxSpinCtrl* spin = static_cast(window); - spin->SetValue(m_opt.min); - spin->GetText()->SetInsertionPointEnd(); - } -#endif on_change_field(); } } @@ -976,13 +993,16 @@ void SpinCtrl::msw_rescale() { Field::msw_rescale(); - wxSpinCtrl* field = dynamic_cast(window); + auto field = dynamic_cast<::SpinInput*>(window); if (parent_is_custom_ctrl) field->SetSize(wxSize(def_width() * m_em_unit, lround(opt_height * m_em_unit))); else field->SetMinSize(wxSize(def_width() * m_em_unit, int(1.9f*field->GetFont().GetPixelSize().y))); } +#if 1 +using choice_ctrl = ::ComboBox; +#else #ifdef __WXOSX__ static_assert(wxMAJOR_VERSION >= 3, "Use of wxBitmapComboBox on Settings Tabs requires wxWidgets 3.0 and newer"); using choice_ctrl = wxBitmapComboBox; @@ -993,6 +1013,7 @@ using choice_ctrl = BitmapComboBox; using choice_ctrl = wxComboBox; #endif #endif // __WXOSX__ +#endif void Choice::BUILD() { wxSize size(def_width_wider() * m_em_unit, wxDefaultCoord); @@ -1003,10 +1024,10 @@ void Choice::BUILD() { if (m_opt.gui_type != ConfigOptionDef::GUIType::undefined && m_opt.gui_type != ConfigOptionDef::GUIType::select_close) { m_is_editable = true; - temp = new choice_ctrl(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, wxTE_PROCESS_ENTER); + temp = new choice_ctrl(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, wxTE_PROCESS_ENTER | DD_NO_CHECK_ICON); } else { -#ifdef __WXOSX__ +#if 0 //#ifdef __WXOSX__ /* wxBitmapComboBox with wxCB_READONLY style return NULL for GetTextCtrl(), * so ToolTip doesn't shown. * Next workaround helps to solve this problem @@ -1015,7 +1036,7 @@ void Choice::BUILD() { temp->SetTextCtrlStyle(wxTE_READONLY); temp->Create(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr); #else - temp = new choice_ctrl(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, wxCB_READONLY); + temp = new choice_ctrl(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, wxCB_READONLY | DD_NO_CHECK_ICON); #endif //__WXOSX__ } @@ -1281,8 +1302,8 @@ void Choice::set_values(const std::vector& values) auto value = ww->GetValue(); ww->Clear(); ww->Append(""); - for (const auto &el : values) - ww->Append(wxString(el)); + for (const std::string& el : values) + ww->Append(from_u8(el)); ww->SetValue(value); m_disable_change_event = false; @@ -1517,21 +1538,19 @@ void PointCtrl::BUILD() wxString Y = val - int(val) == 0 ? wxString::Format(_T("%i"), int(val)) : wxNumberFormatter::ToString(val, 2, wxNumberFormatter::Style_None); long style = wxTE_PROCESS_ENTER; -#ifdef _WIN32 - style |= wxBORDER_SIMPLE; -#endif - x_textctrl = new wxTextCtrl(m_parent, wxID_ANY, X, wxDefaultPosition, field_size, style); - y_textctrl = new wxTextCtrl(m_parent, wxID_ANY, Y, wxDefaultPosition, field_size, style); + x_textctrl = new text_ctrl(m_parent, X, "", "", wxDefaultPosition, field_size, style); + y_textctrl = new text_ctrl(m_parent, Y, "", "", wxDefaultPosition, field_size, style); if (parent_is_custom_ctrl && m_opt.height < 0) opt_height = (double)x_textctrl->GetSize().GetHeight() / m_em_unit; x_textctrl->SetFont(Slic3r::GUI::wxGetApp().normal_font()); - x_textctrl->SetBackgroundStyle(wxBG_STYLE_PAINT); + if (!wxOSX) x_textctrl->SetBackgroundStyle(wxBG_STYLE_PAINT); y_textctrl->SetFont(Slic3r::GUI::wxGetApp().normal_font()); - y_textctrl->SetBackgroundStyle(wxBG_STYLE_PAINT); + if (!wxOSX) y_textctrl->SetBackgroundStyle(wxBG_STYLE_PAINT); - auto static_text_x = new wxStaticText(m_parent, wxID_ANY, "x : "); - auto static_text_y = new wxStaticText(m_parent, wxID_ANY, " y : "); + wxSize label_sz = wxSize(int(field_size.x / 2), field_size.y); + auto static_text_x = new wxStaticText(m_parent, wxID_ANY, "x : ", wxDefaultPosition, label_sz, wxALIGN_RIGHT); + auto static_text_y = new wxStaticText(m_parent, wxID_ANY, "y : ", wxDefaultPosition, label_sz, wxALIGN_RIGHT); static_text_x->SetFont(Slic3r::GUI::wxGetApp().normal_font()); static_text_x->SetBackgroundStyle(wxBG_STYLE_PAINT); static_text_y->SetFont(Slic3r::GUI::wxGetApp().normal_font()); @@ -1542,9 +1561,9 @@ void PointCtrl::BUILD() wxGetApp().UpdateDarkUI(static_text_x, false, true); wxGetApp().UpdateDarkUI(static_text_y, false, true); - temp->Add(static_text_x, 0, wxALIGN_CENTER_VERTICAL, 0); + temp->Add(static_text_x); temp->Add(x_textctrl); - temp->Add(static_text_y, 0, wxALIGN_CENTER_VERTICAL, 0); + temp->Add(static_text_y); temp->Add(y_textctrl); x_textctrl->Bind(wxEVT_TEXT_ENTER, ([this](wxCommandEvent e) { propagate_value(x_textctrl); }), x_textctrl->GetId()); @@ -1586,7 +1605,7 @@ void PointCtrl::sys_color_changed() #endif } -bool PointCtrl::value_was_changed(wxTextCtrl* win) +bool PointCtrl::value_was_changed(text_ctrl* win) { if (m_value.empty()) return true; @@ -1598,7 +1617,7 @@ bool PointCtrl::value_was_changed(wxTextCtrl* win) return boost::any_cast(m_value) != boost::any_cast(val); } -void PointCtrl::propagate_value(wxTextCtrl* win) +void PointCtrl::propagate_value(text_ctrl* win) { if (win->GetValue().empty()) on_kill_focus(); @@ -1717,7 +1736,7 @@ void SliderCtrl::BUILD() m_textctrl->SetFont(Slic3r::GUI::wxGetApp().normal_font()); m_textctrl->SetBackgroundStyle(wxBG_STYLE_PAINT); - temp->Add(m_slider, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL, 0); + temp->Add(m_slider, 1, wxEXPAND, 0); temp->Add(m_textctrl, 0, wxALIGN_CENTER_VERTICAL, 0); m_slider->Bind(wxEVT_SLIDER, ([this](wxCommandEvent e) { @@ -1760,5 +1779,4 @@ boost::any& SliderCtrl::get_value() } -} // GUI -} // Slic3r +} // Slic3r :: GUI diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 73af0ef..cb7d95f 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -21,6 +21,10 @@ #include "GUI.hpp" #include "wxExtensions.hpp" +#include "Widgets/CheckBox.hpp" +#include "Widgets/SwitchButton.hpp" +#include "Widgets/SpinInput.hpp" +#include "Widgets/TextInput.hpp" #ifdef __WXMSW__ #define wxMSW true @@ -37,7 +41,6 @@ using t_change = std::function; wxString double_to_string(double const value, const int max_precision = 4); -wxString get_thumbnails_string(const std::vector& values); class UndoValueUIManager { @@ -95,6 +98,29 @@ class UndoValueUIManager UndoValueUI m_undo_ui; + struct EditValueUI { + // Bitmap and Tooltip text for m_Edit_btn. The wxButton will be updated only if the new wxBitmap pointer differs from the currently rendered one. + const ScalableBitmap* bitmap{ nullptr }; + wxString tooltip { wxEmptyString }; + + bool set_bitmap(const ScalableBitmap* bmp) { + if (bitmap != bmp) { + bitmap = bmp; + return true; + } + return false; + } + + bool set_tooltip(const wxString& tip) { + if (tooltip != tip) { + tooltip = tip; + return true; + } + return false; + } + }; + + EditValueUI m_edit_ui; public: UndoValueUIManager() {} ~UndoValueUIManager() {} @@ -105,6 +131,8 @@ public: bool set_undo_tooltip(const wxString* tip) { return m_undo_ui.set_undo_tooltip(tip); } bool set_undo_to_sys_tooltip(const wxString* tip) { return m_undo_ui.set_undo_to_sys_tooltip(tip); } + bool set_edit_bitmap(const ScalableBitmap* bmp) { return m_edit_ui.set_bitmap(bmp); } + bool set_edit_tooltip(const wxString& tip) { return m_edit_ui.set_tooltip(tip); } // ui items used for revert line value bool has_undo_ui() const { return m_undo_ui.undo_bitmap != nullptr; } const wxBitmapBundle& undo_bitmap() const { return m_undo_ui.undo_bitmap->bmp(); } @@ -112,8 +140,15 @@ public: const wxBitmapBundle& undo_to_sys_bitmap() const { return m_undo_ui.undo_to_sys_bitmap->bmp(); } const wxString* undo_to_sys_tooltip() const { return m_undo_ui.undo_to_sys_tooltip; } const wxColour* label_color() const { return m_undo_ui.label_color; } + // Extentions + + // Search blinker const bool blink() const { return m_undo_ui.blink; } bool* get_blink_ptr() { return &m_undo_ui.blink; } + // Edit field button + bool has_edit_ui() const { return !m_edit_ui.tooltip.IsEmpty(); } + const wxBitmapBundle* edit_bitmap() const { return &m_edit_ui.bitmap->bmp(); } + const wxString* edit_tooltip() const { return &m_edit_ui.tooltip; } }; @@ -147,6 +182,8 @@ public: void on_back_to_initial_value(); /// Call the attached m_back_to_sys_value method. void on_back_to_sys_value(); + /// Call the attached m_fn_edit_value method. + void on_edit_value(); public: /// parent wx item, opportunity to refactor (probably not necessary - data duplication) @@ -162,6 +199,8 @@ public: t_back_to_init m_back_to_initial_value{ nullptr }; t_back_to_init m_back_to_sys_value{ nullptr }; + /// Callback function to edit field value + t_back_to_init m_fn_edit_value{ nullptr }; // This is used to avoid recursive invocation of the field change/update by wxWidgets. bool m_disable_change_event {false}; bool m_is_modified_value {false}; @@ -250,6 +289,7 @@ inline bool is_window_field(const t_field& obj) { return !is_bad_field(obj) && o /// Covenience function to determine whether this field is a valid sizer field. inline bool is_sizer_field(const t_field& obj) { return !is_bad_field(obj) && obj->getSizer() != nullptr; } +using text_ctrl = ::TextInput; //wxTextCtrl class TextCtrl : public Field { using Field::Field; #ifdef __WXGTK__ @@ -270,7 +310,7 @@ public: void set_value(const std::string& value, bool change_event = false) { m_disable_change_event = !change_event; - dynamic_cast(window)->SetValue(wxString(value)); + dynamic_cast(window)->SetValue(wxString(value)); m_disable_change_event = false; } void set_value(const boost::any& value, bool change_event = false) override; @@ -294,24 +334,29 @@ public: CheckBox(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {} ~CheckBox() {} + static wxWindow* GetNewWin(wxWindow* parent, const wxString& label = wxEmptyString); + static void SetValue(wxWindow* win, bool value); + static bool GetValue(wxWindow* win); + static void Rescale(wxWindow* win); + static void SysColorChanged(wxWindow* win); wxWindow* window{ nullptr }; void BUILD() override; - void set_value(const bool value, bool change_event = false) { - m_disable_change_event = !change_event; - dynamic_cast(window)->SetValue(value); - m_disable_change_event = false; - } + void set_value(const bool value, bool change_event = false); void set_value(const boost::any& value, bool change_event = false) override; void set_last_meaningful_value() override; void set_na_value() override; boost::any& get_value() override; void msw_rescale() override; + void sys_color_changed() override; - void enable() override { dynamic_cast(window)->Enable(); } - void disable() override { dynamic_cast(window)->Disable(); } + void enable() override; + void disable() override; wxWindow* getWindow() override { return window; } +private: + void SetValue(bool value); + bool GetValue(); }; class SpinCtrl : public Field { @@ -333,7 +378,14 @@ public: /* void set_value(const std::string& value, bool change_event = false) { m_disable_change_event = !change_event; - dynamic_cast(window)->SetValue(value); + dynamic_cast<::SpinInput*>(window)->SetValue(value); + m_disable_change_event = false; + } + void set_value(const boost::any& value, bool change_event = false) override { + m_disable_change_event = !change_event; + tmp_value = boost::any_cast(value); + m_value = value; + dynamic_cast<::SpinInput*>(window)->SetValue(tmp_value); m_disable_change_event = false; } */ @@ -343,10 +395,16 @@ public: boost::any& get_value() override; +/* + boost::any& get_value() override { + int value = static_cast<::SpinInput*>(window)->GetValue(); + return m_value = value; + } +*/ void msw_rescale() override; - void enable() override { dynamic_cast(window)->Enable(); } - void disable() override { dynamic_cast(window)->Disable(); } + void enable() override { dynamic_cast<::SpinInput*>(window)->Enable(); } + void disable() override { dynamic_cast<::SpinInput*>(window)->Disable(); } wxWindow* getWindow() override { return window; } }; @@ -422,13 +480,13 @@ public: ~PointCtrl(); wxSizer* sizer{ nullptr }; - wxTextCtrl* x_textctrl{ nullptr }; - wxTextCtrl* y_textctrl{ nullptr }; + text_ctrl* x_textctrl{ nullptr }; + text_ctrl* y_textctrl{ nullptr }; void BUILD() override; - bool value_was_changed(wxTextCtrl* win); + bool value_was_changed(text_ctrl* win); // Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER - void propagate_value(wxTextCtrl* win); + void propagate_value(text_ctrl* win); void set_value(const Vec2d& value, bool change_event = false); void set_value(const boost::any& value, bool change_event = false) override; boost::any& get_value() override; diff --git a/src/slic3r/GUI/FirmwareDialog.cpp b/src/slic3r/GUI/FirmwareDialog.cpp index 129cd10..2674fe1 100644 --- a/src/slic3r/GUI/FirmwareDialog.cpp +++ b/src/slic3r/GUI/FirmwareDialog.cpp @@ -813,11 +813,13 @@ FirmwareDialog::FirmwareDialog(wxWindow *parent) : p->hex_picker = new wxFilePickerCtrl(panel, wxID_ANY, wxEmptyString, /*wxFileSelectorPromptStr*/_L("Select a file"), "Hex files (*.hex)|*.hex|All files|*.*"); p->hex_picker->GetPickerCtrl()->SetLabelText(_(L("Browse"))); + GUI::wxGetApp().SetWindowVariantForButton(static_cast(p->hex_picker->GetPickerCtrl())); auto *label_port_picker = new wxStaticText(panel, wxID_ANY, _(L("Serial port:"))); p->port_picker = new wxComboBox(panel, wxID_ANY); p->txt_port_autodetect = new wxStaticText(panel, wxID_ANY, _(L("Autodetected"))); p->btn_rescan = new wxButton(panel, wxID_ANY, _(L("Rescan"))); + GUI::wxGetApp().SetWindowVariantForButton(p->btn_rescan); auto *port_sizer = new wxBoxSizer(wxHORIZONTAL); port_sizer->Add(p->port_picker, 1, wxEXPAND | wxRIGHT, SPACING); port_sizer->Add(p->btn_rescan, 0); @@ -841,7 +843,7 @@ FirmwareDialog::FirmwareDialog(wxWindow *parent) : grid->Add(port_sizer, 0, wxEXPAND); grid->Add(label_progress, 0, wxALIGN_CENTER_VERTICAL); - grid->Add(p->progressbar, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL); + grid->Add(p->progressbar, 1, wxEXPAND); grid->Add(label_status, 0, wxALIGN_CENTER_VERTICAL); grid->Add(p->txt_status, 0, wxEXPAND); @@ -860,7 +862,9 @@ FirmwareDialog::FirmwareDialog(wxWindow *parent) : vsizer->Add(p->spoiler, 1, wxEXPAND | wxBOTTOM, SPACING); p->btn_close = new wxButton(panel, wxID_CLOSE, _(L("Close"))); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac + GUI::wxGetApp().SetWindowVariantForButton(p->btn_close); p->btn_flash = new wxButton(panel, wxID_ANY, p->btn_flash_label_ready); + GUI::wxGetApp().SetWindowVariantForButton(p->btn_flash); p->btn_flash->Disable(); auto *bsizer = new wxBoxSizer(wxHORIZONTAL); bsizer->Add(p->btn_close); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 297ecf5..f1ac89a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -301,7 +301,7 @@ void GCodeViewer::SequentialView::Marker::init() void GCodeViewer::SequentialView::Marker::set_world_position(const Vec3f& position) { m_world_position = position; - m_world_transform = (Geometry::translation_transform((position + m_z_offset * Vec3f::UnitZ()).cast()) * + m_world_transform = (Geometry::translation_transform((position + m_model_z_offset * Vec3f::UnitZ()).cast()) * Geometry::translation_transform(m_model.get_bounding_box().size().z() * Vec3d::UnitZ()) * Geometry::rotation_transform({ M_PI, 0.0, 0.0 })).cast(); } @@ -395,39 +395,15 @@ void GCodeViewer::SequentialView::Marker::render(EViewType &view_type) ImGui::PopStyleVar(); } -void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const std::string& filename, const std::vector& lines_ends) -{ - assert(! m_file.is_open()); - if (m_file.is_open()) - return; - - m_filename = filename; - m_lines_ends = lines_ends; - - m_selected_line_id = 0; - m_last_lines_size = 0; - - try +void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const GCodeProcessorResult& gcode_result) { - m_file.open(boost::filesystem::path(m_filename)); - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Unable to map file " << m_filename << ". Cannot show G-code window."; - reset(); - } + m_filename = gcode_result.filename; + m_is_binary_file = gcode_result.is_binary_file; + m_lines_ends = gcode_result.lines_ends; } -void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, uint64_t curr_line_id) const +void GCodeViewer::SequentialView::GCodeWindow::add_gcode_line_to_lines_cache(const std::string& src) { - auto update_lines = [this](uint64_t start_id, uint64_t end_id) { - std::vector ret; - ret.reserve(end_id - start_id + 1); - for (uint64_t id = start_id; id <= end_id; ++id) { - // read line from file - const size_t start = id == 1 ? 0 : m_lines_ends[id - 2]; - const size_t len = m_lines_ends[id - 1] - start; - std::string gline(m_file.data() + start, len); std::string command; std::string parameters; @@ -435,7 +411,7 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u // extract comment std::vector tokens; - boost::split(tokens, gline, boost::is_any_of(";"), boost::token_compress_on); + boost::split(tokens, src, boost::is_any_of(";"), boost::token_compress_on); command = tokens.front(); if (tokens.size() > 1) comment = ";" + tokens.back(); @@ -450,9 +426,124 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u } } } - ret.push_back({ command, parameters, comment }); + m_lines_cache.push_back({ command, parameters, comment }); +} + +void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, size_t curr_line_id) +{ + auto update_lines_ascii = [this]() { + m_lines_cache.clear(); + m_lines_cache.reserve(m_cache_range.size()); + const std::vector& lines_ends = m_lines_ends.front(); + FILE* file = boost::nowide::fopen(m_filename.c_str(), "rb"); + if (file != nullptr) { + for (size_t id = *m_cache_range.min; id <= *m_cache_range.max; ++id) { + assert(id > 0); + // read line from file + const size_t begin = id == 1 ? 0 : lines_ends[id - 2]; + const size_t len = lines_ends[id - 1] - begin; + std::string gline(len, '\0'); + fseek(file, begin, SEEK_SET); + const size_t rsize = fread((void*)gline.data(), 1, len, file); + if (ferror(file) || rsize != len) { + m_lines_cache.clear(); + break; + } + + add_gcode_line_to_lines_cache(gline); + } + fclose(file); + } + }; + + auto update_lines_binary = [this]() { + m_lines_cache.clear(); + m_lines_cache.reserve(m_cache_range.size()); + + size_t cumulative_lines_count = 0; + std::vector cumulative_lines_counts; + cumulative_lines_counts.reserve(m_lines_ends.size()); + for (size_t i = 0; i < m_lines_ends.size(); ++i) { + cumulative_lines_count += m_lines_ends[i].size(); + cumulative_lines_counts.emplace_back(cumulative_lines_count); + } + + size_t first_block_id = 0; + for (size_t i = 0; i < cumulative_lines_counts.size(); ++i) { + if (*m_cache_range.min <= cumulative_lines_counts[i]) { + first_block_id = i; + break; + } + } + size_t last_block_id = 0; + for (size_t i = 0; i < cumulative_lines_counts.size(); ++i) { + if (*m_cache_range.max <= cumulative_lines_counts[i]) { + last_block_id = i; + break; + } + } + assert(last_block_id >= first_block_id); + + FilePtr file(boost::nowide::fopen(m_filename.c_str(), "rb")); + if (file.f != nullptr) { + fseek(file.f, 0, SEEK_END); + const long file_size = ftell(file.f); + rewind(file.f); + + // read file header + using namespace bgcode::core; + using namespace bgcode::binarize; + FileHeader file_header; + EResult res = read_header(*file.f, file_header, nullptr); + if (res == EResult::Success) { + // search first GCode block + BlockHeader block_header; + res = read_next_block_header(*file.f, file_header, block_header, EBlockType::GCode, nullptr, 0); + if (res == EResult::Success) { + for (size_t i = 0; i < first_block_id; ++i) { + skip_block(*file.f, file_header, block_header); + res = read_next_block_header(*file.f, file_header, block_header, nullptr, 0); + if (res != EResult::Success || block_header.type != (uint16_t)EBlockType::GCode) { + m_lines_cache.clear(); + return; + } + } + + for (size_t i = first_block_id; i <= last_block_id; ++i) { + GCodeBlock block; + res = block.read_data(*file.f, file_header, block_header); + if (res != EResult::Success) { + m_lines_cache.clear(); + return; + } + + const size_t ref_id = (i == 0) ? 0 : i - 1; + const size_t first_line_id = (i == 0) ? *m_cache_range.min : + (*m_cache_range.min > cumulative_lines_counts[ref_id]) ? *m_cache_range.min - cumulative_lines_counts[ref_id] : 1; + const size_t last_line_id = (*m_cache_range.max <= cumulative_lines_counts[i]) ? + (i == 0) ? *m_cache_range.max : *m_cache_range.max - cumulative_lines_counts[ref_id] : m_lines_ends[i].size(); + assert(last_line_id >= first_line_id); + + for (size_t j = first_line_id; j <= last_line_id; ++j) { + const size_t begin = (j == 1) ? 0 : m_lines_ends[i][j - 2]; + const size_t end = m_lines_ends[i][j - 1]; + std::string gline; + gline.insert(gline.end(), block.raw_data.begin() + begin, block.raw_data.begin() + end); + add_gcode_line_to_lines_cache(gline); + } + + if (ftell(file.f) == file_size) + break; + + res = read_next_block_header(*file.f, file_header, block_header, nullptr, 0); + if (res != EResult::Success || block_header.type != (uint16_t)EBlockType::GCode) { + m_lines_cache.clear(); + return; + } + } + } + } } - return ret; }; //B18 static const ImVec4 LINE_NUMBER_COLOR = ImGuiWrapper::COL_BLUE_LIGHT; @@ -471,37 +562,45 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u // number of visible lines const float text_height = ImGui::CalcTextSize("0").y; const ImGuiStyle& style = ImGui::GetStyle(); - const uint64_t lines_count = static_cast((wnd_height - 2.0f * style.WindowPadding.y + style.ItemSpacing.y) / (text_height + style.ItemSpacing.y)); + const size_t visible_lines_count = static_cast((wnd_height - 2.0f * style.WindowPadding.y + style.ItemSpacing.y) / (text_height + style.ItemSpacing.y)); - if (lines_count == 0) + if (visible_lines_count == 0) return; + if (m_lines_ends.empty() || m_lines_ends.front().empty()) + return; + + auto resize_range = [&](Range& range, size_t lines_count) { + const size_t half_lines_count = lines_count / 2; + range.min = (curr_line_id > half_lines_count) ? curr_line_id - half_lines_count : 1; + range.max = *range.min + lines_count - 1; + size_t lines_ends_count = 0; + for (const auto& le : m_lines_ends) { + lines_ends_count += le.size(); + } + if (*range.max >= lines_ends_count) { + range.max = lines_ends_count - 1; + range.min = *range.max - lines_count + 1; + } + }; // visible range - const uint64_t half_lines_count = lines_count / 2; - uint64_t start_id = (curr_line_id >= half_lines_count) ? curr_line_id - half_lines_count : 0; - uint64_t end_id = start_id + lines_count - 1; - if (end_id >= static_cast(m_lines_ends.size())) { - end_id = static_cast(m_lines_ends.size()) - 1; - start_id = end_id - lines_count + 1; + Range visible_range; + resize_range(visible_range, visible_lines_count); + + // update cache if needed + if (m_cache_range.empty() || !m_cache_range.contains(visible_range)) { + resize_range(m_cache_range, 4 * visible_range.size()); + if (m_is_binary_file) + update_lines_binary(); + else + update_lines_ascii(); } - // updates list of lines to show, if needed - if (m_selected_line_id != curr_line_id || m_last_lines_size != end_id - start_id + 1) { - try - { - *const_cast*>(&m_lines) = update_lines(start_id, end_id); - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) << "Error while loading from file " << m_filename << ". Cannot show G-code window."; + if (m_lines_cache.empty()) return; - } - *const_cast(&m_selected_line_id) = curr_line_id; - *const_cast(&m_last_lines_size) = m_lines.size(); - } // line number's column width - const float id_width = ImGui::CalcTextSize(std::to_string(end_id).c_str()).x; + const float id_width = ImGui::CalcTextSize(std::to_string(*visible_range.max).c_str()).x; ImGuiWrapper& imgui = *wxGetApp().imgui(); @@ -521,14 +620,10 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u current_length += out_text.length(); ImGui::SameLine(0.0f, spacing); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text(out_text); - ImGui::PopStyleColor(); + imgui.text_colored(color, out_text); if (reduced) { ImGui::SameLine(0.0f, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Text, ELLIPSIS_COLOR); - imgui.text("..."); - ImGui::PopStyleColor(); + imgui.text_colored(ELLIPSIS_COLOR, "..."); } return reduced; @@ -537,16 +632,17 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u imgui.set_next_window_pos(0.0f, top, ImGuiCond_Always, 0.0f, 0.0f); imgui.set_next_window_size(0.0f, wnd_height, ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::SetNextWindowBgAlpha(0.8f); + ImGui::SetNextWindowBgAlpha(0.6f); imgui.begin(std::string("G-code"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove); // center the text in the window by pushing down the first line - const float f_lines_count = static_cast(lines_count); + const float f_lines_count = static_cast(visible_lines_count); ImGui::SetCursorPosY(0.5f * (wnd_height - f_lines_count * text_height - (f_lines_count - 1.0f) * style.ItemSpacing.y)); // render text lines - for (uint64_t id = start_id; id <= end_id; ++id) { - const Line& line = m_lines[id - start_id]; + size_t max_line_length = 0; + for (size_t id = *visible_range.min; id <= *visible_range.max; ++id) { + const Line& line = m_lines_cache[id - *m_cache_range.min]; // rect around the current selected line if (id == curr_line_id) { @@ -574,16 +670,16 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, u if (!stop_adding && !line.comment.empty()) // render comment stop_adding = add_item_to_line(line.comment, COMMENT_COLOR, line.command.empty() ? -1.0f : 0.0f, line_length); + max_line_length = std::max(max_line_length, line_length); } imgui.end(); ImGui::PopStyleVar(); -} - -void GCodeViewer::SequentialView::GCodeWindow::stop_mapping_file() -{ - if (m_file.is_open()) - m_file.close(); + // request an extra frame if window's width changed + if (m_max_line_length != max_line_length) { + m_max_line_length = max_line_length; + imgui.set_requires_extra_frame(); + } } //B43 void GCodeViewer::SequentialView::render(float legend_height, EViewType &view_type) @@ -768,12 +864,13 @@ void GCodeViewer::load(const GCodeProcessorResult& gcode_result, const Print& pr //B43 m_gcode_result = &gcode_result; - m_sequential_view.gcode_window.load_gcode(gcode_result.filename, gcode_result.lines_ends); + m_sequential_view.gcode_window.load_gcode(gcode_result); if (wxGetApp().is_gcode_viewer()) m_custom_gcode_per_print_z = gcode_result.custom_gcode_per_print_z; m_max_print_height = gcode_result.max_print_height; + m_z_offset = gcode_result.z_offset; load_toolpaths(gcode_result); load_wipetower_shell(print); @@ -933,6 +1030,7 @@ void GCodeViewer::reset() m_paths_bounding_box.reset(); m_max_bounding_box.reset(); m_max_print_height = 0.0f; + m_z_offset = 0.0f; m_tool_colors = std::vector(); m_extruders_count = 0; m_extruder_ids = std::vector(); @@ -975,6 +1073,7 @@ void GCodeViewer::render() if (m_sequential_view.current.last != m_sequential_view.endpoints.last) { m_sequential_view.marker.set_world_position(m_sequential_view.current_position); m_sequential_view.marker.set_world_offset(m_sequential_view.current_offset); + m_sequential_view.marker.set_z_offset(m_z_offset); //B43 m_sequential_view.render(legend_height, m_view_type); } @@ -2211,20 +2310,21 @@ void GCodeViewer::load_toolpaths(const GCodeProcessorResult& gcode_result) if (move.type == EMoveType::Seam) ++seams_count; - size_t move_id = i - seams_count; + const size_t move_id = i - seams_count; if (move.type == EMoveType::Extrude) { - if (!move.internal_only) { - // layers zs + // layers zs/ranges const double* const last_z = m_layers.empty() ? nullptr : &m_layers.get_zs().back(); const double z = static_cast(move.position.z()); - if (last_z == nullptr || z < *last_z - EPSILON || *last_z + EPSILON < z) { + if (move.extrusion_role != GCodeExtrusionRole::Custom && + (last_z == nullptr || z < *last_z - EPSILON || *last_z + EPSILON < z)) { + // start a new layer const size_t start_it = (m_layers.empty() && first_travel_s_id != 0) ? first_travel_s_id : last_travel_s_id; m_layers.append(z, { start_it, move_id }); } - else + else if (!m_layers.empty() && !move.internal_only) + // update last layer m_layers.get_ranges().back().last = move_id; - } // extruder ids m_extruder_ids.emplace_back(move.extruder_id); // roles @@ -2287,9 +2387,18 @@ void GCodeViewer::load_shells(const Print& print) return; // adds objects' volumes - int object_id = 0; for (const PrintObject* obj : print.objects()) { const ModelObject* model_obj = obj->model_object(); + int object_id = -1; + const ModelObjectPtrs model_objects = wxGetApp().plater()->model().objects; + for (int i = 0; i < static_cast(model_objects.size()); ++i) { + if (model_obj->id() == model_objects[i]->id()) { + object_id = i; + break; + } + } + if (object_id == -1) + continue; std::vector instance_ids(model_obj->instances.size()); for (int i = 0; i < (int)model_obj->instances.size(); ++i) { @@ -2309,7 +2418,6 @@ void GCodeViewer::load_shells(const Print& print) } } - ++object_id; } wxGetApp().plater()->get_current_canvas3D()->check_volumes_outside_state(m_shells.volumes); @@ -2411,6 +2519,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool case EViewType::Temperature: { color = m_extrusions.ranges.temperature.get_color_at(path.temperature); break; } case EViewType::LayerTimeLinear: case EViewType::LayerTimeLogarithmic: { + if (!m_layers_times.empty() && m_layers.size() == m_layers_times.front().size()) { const Path::Sub_Path& sub_path = path.sub_paths.front(); double z = static_cast(sub_path.first.position.z()); const std::vector& zs = m_layers.get_zs(); @@ -2425,6 +2534,7 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool } } } + } break; } case EViewType::VolumetricRate: { color = m_extrusions.ranges.volumetric_rate.get_color_at(path.volumetric_rate); break; } @@ -2489,8 +2599,11 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool const size_t min_s_id = m_layers.get_range_at(min_id).first; const size_t max_s_id = m_layers.get_range_at(max_id).last; - return (min_s_id <= path.sub_paths.front().first.s_id && path.sub_paths.front().first.s_id <= max_s_id) || - (min_s_id <= path.sub_paths.back().last.s_id && path.sub_paths.back().last.s_id <= max_s_id); + const bool top_layer_shown = max_id == m_layers.size() - 1; + + return (min_s_id <= path.sub_paths.front().first.s_id && path.sub_paths.front().first.s_id <= max_s_id) || // the leading vertex is contained + (min_s_id <= path.sub_paths.back().last.s_id && path.sub_paths.back().last.s_id <= max_s_id) || // the trailing vertex is contained + (top_layer_shown && max_s_id < path.sub_paths.front().first.s_id); // the leading vertex is above the top layer and the top layer is shown }; #if ENABLE_GCODE_VIEWER_STATISTICS @@ -2539,11 +2652,9 @@ void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool for (size_t i = 0; i < buffer.paths.size(); ++i) { const Path& path = buffer.paths[i]; if (path.type == EMoveType::Travel) { - if (path.sub_paths.front().first.s_id > m_layers_z_range[0]) { if (!is_travel_in_layers_range(i, m_layers_z_range[0], m_layers_z_range[1])) continue; } - } else if (!is_in_layers_range(path, m_layers_z_range[0], m_layers_z_range[1])) continue; @@ -3329,7 +3440,7 @@ void GCodeViewer::render_legend(float& legend_height) imgui.set_next_window_pos(0.0f, 0.0f, ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::SetNextWindowBgAlpha(0.8f); + ImGui::SetNextWindowBgAlpha(0.6f); const float max_height = 0.75f * static_cast(cnv_size.get_height()); const float child_height = 0.3333f * max_height; ImGui::SetNextWindowSizeConstraints({ 0.0f, 0.0f }, { -1.0f, max_height }); @@ -3697,17 +3808,25 @@ void GCodeViewer::render_legend(float& legend_height) ImGui::PushStyleColor(ImGuiCol_FrameBg, { 0.1f, 0.1f, 0.1f, 0.8f }); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, { 0.2f, 0.2f, 0.2f, 0.8f }); - imgui.combo(std::string(), { _u8L("Feature type"), - _u8L("Height (mm)"), - _u8L("Width (mm)"), - _u8L("Speed (mm/s)"), - _u8L("Fan speed (%)"), - _u8L("Temperature (°C)"), - _u8L("Volumetric flow rate (mm³/s)"), - _u8L("Layer time (linear)"), - _u8L("Layer time (logarithmic)"), - _u8L("Tool"), - _u8L("Color Print") }, view_type, ImGuiComboFlags_HeightLargest, 0.0f, -1.0f); + std::vector view_options; + std::vector view_options_id; + if (!m_layers_times.empty() && m_layers.size() == m_layers_times.front().size()) { + view_options = { _u8L("Feature type"), _u8L("Height (mm)"), _u8L("Width (mm)"), _u8L("Speed (mm/s)"), _u8L("Fan speed (%)"), + _u8L("Temperature (°C)"), _u8L("Volumetric flow rate (mm³/s)"), _u8L("Layer time (linear)"), _u8L("Layer time (logarithmic)"), + _u8L("Tool"), _u8L("Color Print") }; + view_options_id = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + } + else { + view_options = { _u8L("Feature type"), _u8L("Height (mm)"), _u8L("Width (mm)"), _u8L("Speed (mm/s)"), _u8L("Fan speed (%)"), + _u8L("Temperature (°C)"), _u8L("Volumetric flow rate (mm³/s)"), _u8L("Tool"), _u8L("Color Print") }; + view_options_id = { 0, 1, 2, 3, 4, 5, 6, 9, 10 }; + if (view_type == 7 || view_type == 8) + view_type = 0; + } + auto view_type_it = std::find(view_options_id.begin(), view_options_id.end(), view_type); + int view_type_id = (view_type_it == view_options_id.end()) ? 0 : std::distance(view_options_id.begin(), view_type_it); + if (imgui.combo(std::string(), view_options, view_type_id, ImGuiComboFlags_HeightLargest, 0.0f, -1.0f)) + view_type = view_options_id[view_type_id]; ImGui::PopStyleColor(2); if (old_view_type != view_type) { diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index ea2a43c..80c345a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -654,11 +654,14 @@ public: GLModel m_model; Vec3f m_world_position; Transform3f m_world_transform; - // for seams, the position of the marker is on the last endpoint of the toolpath containing it - // the offset is used to show the correct value of tool position in the "ToolPosition" window - // see implementation of render() method + // For seams, the position of the marker is on the last endpoint of the toolpath containing it. + // This offset is used to show the correct value of tool position in the "ToolPosition" window. + // See implementation of render() method Vec3f m_world_offset; - float m_z_offset{ 0.5f }; + // z offset of the print + float m_z_offset{ 0.0f }; + // z offset of the model + float m_model_z_offset{ 0.5f }; bool m_visible{ true }; //B43 GCodeProcessorResult::MoveVertex m_curr_move; @@ -670,6 +673,7 @@ public: void set_world_position(const Vec3f& position); void set_world_offset(const Vec3f& offset) { m_world_offset = offset; } + void set_z_offset(float z_offset) { m_z_offset = z_offset; } bool is_visible() const { return m_visible; } void set_visible(bool visible) { m_visible = visible; } @@ -686,32 +690,42 @@ public: std::string parameters; std::string comment; }; + struct Range + { + std::optional min; + std::optional max; + bool empty() const { + return !min.has_value() || !max.has_value(); + } + bool contains(const Range& other) const { + return !this->empty() && !other.empty() && *this->min <= *other.min && *this->max >= other.max; + } + size_t size() const { + return empty() ? 0 : *this->max - *this->min + 1; + } + }; bool m_visible{ true }; - uint64_t m_selected_line_id{ 0 }; - size_t m_last_lines_size{ 0 }; std::string m_filename; - boost::iostreams::mapped_file_source m_file; + bool m_is_binary_file{ false }; // map for accessing data in file by line number - std::vector m_lines_ends; - // current visible lines - std::vector m_lines; + std::vector> m_lines_ends; + std::vector m_lines_cache; + Range m_cache_range; + size_t m_max_line_length{ 0 }; public: - GCodeWindow() = default; - ~GCodeWindow() { stop_mapping_file(); } - void load_gcode(const std::string& filename, const std::vector& lines_ends); + void load_gcode(const GCodeProcessorResult& gcode_result); void reset() { - stop_mapping_file(); m_lines_ends.clear(); - m_lines.clear(); + m_lines_cache.clear(); m_filename.clear(); } void toggle_visibility() { m_visible = !m_visible; } + void render(float top, float bottom, size_t curr_line_id); - void render(float top, float bottom, uint64_t curr_line_id) const; - - void stop_mapping_file(); + private: + void add_gcode_line_to_lines_cache(const std::string& src); }; struct Endpoints @@ -767,6 +781,7 @@ private: const GCodeProcessorResult *m_gcode_result; float m_max_print_height{ 0.0f }; + float m_z_offset{ 0.0f }; std::vector m_tool_colors; Layers m_layers; std::array m_layers_z_range; @@ -827,10 +842,13 @@ public: const BoundingBoxf3& get_max_bounding_box() const { BoundingBoxf3& max_bounding_box = const_cast(m_max_bounding_box); if (!max_bounding_box.defined) { + if (m_shells_bounding_box.defined) max_bounding_box = m_shells_bounding_box; + if (m_paths_bounding_box.defined) { max_bounding_box.merge(m_paths_bounding_box); max_bounding_box.merge(m_paths_bounding_box.max + m_sequential_view.marker.get_bounding_box().size().z() * Vec3d::UnitZ()); } + } return m_max_bounding_box; } const std::vector& get_layers_zs() const { return m_layers.get_zs(); } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 6ff0279..929b681 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -70,6 +70,7 @@ #include "DoubleSlider.hpp" #include +#include static constexpr const float TRACKBALLSIZE = 0.8f; @@ -348,7 +349,7 @@ std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) con } } if (h > 0.0f) - ret = std::to_string(h); + ret = format("%.3f", h); } } return ret; @@ -983,7 +984,7 @@ void GLCanvas3D::SequentialPrintClearance::render() glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - if (!m_evaluating) + if (!m_evaluating && !m_dragging) m_fill.render(); #if ENABLE_GL_CORE_PROFILE @@ -1031,6 +1032,7 @@ wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MIRRORED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent); @@ -1533,6 +1535,8 @@ bool GLCanvas3D::check_volumes_outside_state(GLVolumeCollection& volumes, ModelI contained_min_one |= !volume->is_outside; } } + else if (volume->is_modifier) + volume->is_outside = false; } for (unsigned int vol_idx = 0; vol_idx < volumes.volumes.size(); ++vol_idx) { @@ -1665,7 +1669,12 @@ void GLCanvas3D::set_config(const DynamicPrintConfig* config) double objdst = min_object_distance(*config); double min_obj_dst = slot == ArrangeSettingsDb_AppCfg::slotFFFSeqPrint ? objdst : 0.; m_arrange_settings_db.set_distance_from_obj_range(slot, min_obj_dst, 100.); + if (std::abs(m_arrange_settings_db.get_defaults(slot).d_obj - objdst) > EPSILON) { m_arrange_settings_db.get_defaults(slot).d_obj = objdst; + // Defaults have changed, so let's sync with the app config and fill + // in the missing values with the new defaults. + m_arrange_settings_db.sync(); + } } } @@ -2013,6 +2022,10 @@ void GLCanvas3D::render() #endif // ENABLE_SLA_VIEW_DEBUG_WINDOW } +#if ENABLE_BINARIZED_GCODE_DEBUG_WINDOW + if (wxGetApp().plater()->is_view3D_shown() && current_printer_technology() != ptSLA && fff_print()->config().gcode_binary) + show_binary_gcode_debug_window(); +#endif // ENABLE_BINARIZED_GCODE_DEBUG_WINDOW std::string tooltip; // Negative coordinate means out of the window, likely because the window was deactivated. @@ -2371,6 +2384,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re volume->extruder_id = extruder_id; volume->is_modifier = !mvs->model_volume->is_model_part(); + volume->shader_outside_printer_detection_enabled = mvs->model_volume->is_model_part(); volume->set_color(color_from_model_volume(*mvs->model_volume)); // force update of render_color alpha channel volume->set_render_color(volume->color.is_transparent()); @@ -2649,7 +2663,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re for (GLVolume* volume : m_volumes.volumes) if (volume->object_idx() < (int)m_model->objects.size() && m_model->objects[volume->object_idx()]->instances[volume->instance_idx()]->is_printable()) { - if (volume->is_modifier && m_model->objects[volume->object_idx()]->volumes[volume->volume_idx()]->is_modifier()) + if (volume->is_active && volume->is_modifier && m_model->objects[volume->object_idx()]->volumes[volume->volume_idx()]->is_modifier()) volume->is_active = printer_technology != ptSLA; } @@ -3622,20 +3636,17 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) //#if defined(__WXMSW__) || defined(__linux__) // // On Windows and Linux needs focus in order to catch key events - // Set focus in order to remove it from object list if (m_canvas != nullptr) { + // Set focus in order to remove it from sidebar but not from TextControl (in ManipulationPanel f.e.) + if (!m_canvas->HasFocus()) { + wxTopLevelWindow* tlw = find_toplevel_parent(m_canvas); // Only set focus, if the top level window of this canvas is active - // and ObjectList has a focus - auto p = dynamic_cast(evt.GetEventObject()); - while (p->GetParent()) - p = p->GetParent(); -#ifdef __WIN32__ - wxWindow* const obj_list = wxGetApp().obj_list()->GetMainWindow(); -#else - wxWindow* const obj_list = wxGetApp().obj_list(); -#endif - if (obj_list == p->FindFocus()) + if (tlw->IsActive()) { + auto* text_ctrl = dynamic_cast(tlw->FindFocus()); + if (text_ctrl == nullptr) m_canvas->SetFocus(); + } + } m_mouse.position = pos.cast(); m_tooltip_enabled = false; // 1) forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while @@ -3738,6 +3749,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!evt.CmdDown()) m_mouse.drag.start_position_3D = m_mouse.scene_position; m_sequential_print_clearance_first_displacement = true; + m_sequential_print_clearance.start_dragging(); } } } @@ -3862,6 +3874,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) else if (m_mouse.drag.move_volume_idx != -1 && m_mouse.dragging) { do_move(L("Move Object")); wxGetApp().obj_manipul()->set_dirty(); + m_sequential_print_clearance.stop_dragging(); // Let the plater know that the dragging finished, so a delayed refresh // of the scene with the background processing data should be performed. post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); @@ -3937,7 +3950,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) type == GLGizmosManager::EType::Move || type == GLGizmosManager::EType::Rotate || type == GLGizmosManager::EType::Scale || - type == GLGizmosManager::EType::Emboss) ) { + type == GLGizmosManager::EType::Emboss|| + type == GLGizmosManager::EType::Svg) ) { for (int hover_volume_id : m_hover_volume_idxs) { const GLVolume &hover_gl_volume = *m_volumes.volumes[hover_volume_id]; int object_idx = hover_gl_volume.object_idx(); @@ -3946,12 +3960,19 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) int hover_volume_idx = hover_gl_volume.volume_idx(); if (hover_volume_idx < 0 || static_cast(hover_volume_idx) >= hover_object->volumes.size()) continue; const ModelVolume* hover_volume = hover_object->volumes[hover_volume_idx]; - if (!hover_volume->text_configuration.has_value()) continue; + if (hover_volume->text_configuration.has_value()) { m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); if (type != GLGizmosManager::EType::Emboss) m_gizmos.open_gizmo(GLGizmosManager::EType::Emboss); wxGetApp().obj_list()->update_selections(); return; + } else if (hover_volume->emboss_shape.has_value()) { + m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); + if (type != GLGizmosManager::EType::Svg) + m_gizmos.open_gizmo(GLGizmosManager::EType::Svg); + wxGetApp().obj_list()->update_selections(); + return; + } } } @@ -4325,7 +4346,7 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type) for (int id : obj_idx_for_update_info_items) wxGetApp().obj_list()->update_info_items(static_cast(id)); - post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MIRRORED)); m_dirty = true; } @@ -4890,9 +4911,8 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const camera.apply_projection(volumes_box, near_z, far_z); - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; + const ModelObjectPtrs &model_objects = GUI::wxGetApp().model().objects; + std::vector extruders_colors = get_extruders_colors(); if (thumbnail_params.transparent_background) //Y18 @@ -4901,15 +4921,34 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); - shader->start_using(); - shader->set_uniform("emission_factor", 0.0f); + glsafe(::glCullFace(GL_BACK)); const Transform3d& projection_matrix = camera.get_projection_matrix(); + const int extruders_count = wxGetApp().extruders_edited_cnt(); for (GLVolume* vol : visible_volumes) { - //B3 - //vol->model.set_color((vol->printable && !vol->is_outside) ? vol->color : ColorRGBA::GRAY()); - vol->model.set_color({ 0.2f, 0.6f, 1.0f, 1.0f }); + const int obj_idx = vol->object_idx(); + const int vol_idx = vol->volume_idx(); + const bool render_as_painted = (obj_idx >= 0 && vol_idx >= 0) ? + !model_objects[obj_idx]->volumes[vol_idx]->mmu_segmentation_facets.empty() : false; + GLShaderProgram* shader = wxGetApp().get_shader(render_as_painted ? "mm_gouraud" : "gouraud_light"); + if (shader == nullptr) + continue; + + shader->start_using(); + const std::array clp_data = { 0.0f, 0.0f, 1.0f, FLT_MAX }; + const std::array z_range = { -FLT_MAX, FLT_MAX }; + const bool is_left_handed = vol->is_left_handed(); + if (render_as_painted) { + shader->set_uniform("volume_world_matrix", vol->world_matrix()); + shader->set_uniform("volume_mirrored", is_left_handed); + shader->set_uniform("clipping_plane", clp_data); + shader->set_uniform("z_range", z_range); + } + else { + shader->set_uniform("emission_factor", 0.0f); + vol->model.set_color((vol->printable && !vol->is_outside) ? vol->color : ColorRGBA::GRAY()); + } // the volume may have been deactivated by an active gizmo const bool is_active = vol->is_active; vol->is_active = true; @@ -4918,12 +4957,27 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const shader->set_uniform("projection_matrix", projection_matrix); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); + if (is_left_handed) + glsafe(::glFrontFace(GL_CW)); + + if (render_as_painted) { + const ModelVolume& model_volume = *model_objects[obj_idx]->volumes[vol_idx]; + const size_t extruder_idx = get_extruder_color_idx(model_volume, extruders_count); + TriangleSelectorMmGui ts(model_volume.mesh(), extruders_colors, extruders_colors[extruder_idx]); + ts.deserialize(model_volume.mmu_segmentation_facets.get_data(), true); + ts.request_update_render_data(); + + ts.render(nullptr, model_matrix); + } + else vol->render(); - vol->is_active = is_active; - } + if (is_left_handed) + glsafe(::glFrontFace(GL_CCW)); shader->stop_using(); + vol->is_active = is_active; + } glsafe(::glDisable(GL_DEPTH_TEST)); if (thumbnail_params.show_bed) @@ -5525,7 +5579,7 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h) auto *imgui = wxGetApp().imgui(); imgui->set_display_size(static_cast(w), static_cast(h)); - const float font_size = 1.7f * wxGetApp().em_unit(); + const float font_size = 1.5f * wxGetApp().em_unit(); #if ENABLE_RETINA_GL imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor()); #else @@ -6252,7 +6306,7 @@ void GLCanvas3D::_check_and_update_toolbar_icon_scale() #if ENABLE_RETINA_GL new_scale /= m_retina_helper->get_scale_factor(); #endif - if (fabs(new_scale - scale) > 0.01) // scale is changed by 1% and more + if (fabs(new_scale - scale) > 0.015) // scale is changed by 1.5% and more wxGetApp().set_auto_toolbar_icon_scale(new_scale); } @@ -7807,6 +7861,95 @@ void GLCanvas3D::GizmoHighlighter::blink() invalidate(); } +#if ENABLE_BINARIZED_GCODE_DEBUG_WINDOW +void GLCanvas3D::show_binary_gcode_debug_window() +{ + bgcode::binarize::BinarizerConfig& binarizer_config = GCodeProcessor::get_binarizer_config(); + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + imgui.begin(std::string("Binary GCode"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + using namespace bgcode::core; + if (ImGui::BeginTable("BinaryGCodeConfig", 2)) { + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "File metadata compression"); + ImGui::TableSetColumnIndex(1); + std::vector options = { "None", "Deflate", "heatshrink 11,4", "heatshrink 12,4" }; + int option_id = (int)binarizer_config.compression.file_metadata; + if (imgui.combo(std::string("##file_metadata_compression"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.compression.file_metadata = (ECompressionType)option_id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Printer metadata compression"); + ImGui::TableSetColumnIndex(1); + option_id = (int)binarizer_config.compression.printer_metadata; + if (imgui.combo(std::string("##printer_metadata_compression"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.compression.printer_metadata = (ECompressionType)option_id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Print metadata compression"); + ImGui::TableSetColumnIndex(1); + option_id = (int)binarizer_config.compression.print_metadata; + if (imgui.combo(std::string("##print_metadata_compression"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.compression.print_metadata = (ECompressionType)option_id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Slicer metadata compression"); + ImGui::TableSetColumnIndex(1); + option_id = (int)binarizer_config.compression.slicer_metadata; + if (imgui.combo(std::string("##slicer_metadata_compression"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.compression.slicer_metadata = (ECompressionType)option_id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "GCode compression"); + ImGui::TableSetColumnIndex(1); + option_id = (int)binarizer_config.compression.gcode; + if (imgui.combo(std::string("##gcode_compression"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.compression.gcode = (ECompressionType)option_id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "GCode encoding"); + ImGui::TableSetColumnIndex(1); + options = { "None", "MeatPack", "MeatPack Comments" }; + option_id = (int)binarizer_config.gcode_encoding; + if (imgui.combo(std::string("##gcode_encoding"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.gcode_encoding = (EGCodeEncodingType)option_id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Metadata encoding"); + ImGui::TableSetColumnIndex(1); + options = { "INI" }; + option_id = (int)binarizer_config.metadata_encoding; + if (imgui.combo(std::string("##metadata_encoding"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.metadata_encoding = (EMetadataEncodingType)option_id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Checksum type"); + ImGui::TableSetColumnIndex(1); + options = { "None", "CRC32" }; + option_id = (int)binarizer_config.checksum; + if (imgui.combo(std::string("##4"), options, option_id, ImGuiComboFlags_HeightLargest, 0.0f, 175.0f)) + binarizer_config.checksum = (EChecksumType)option_id; + + ImGui::EndTable(); + + ImGui::Separator(); + imgui.text("!!! WARNING !!!"); + imgui.text("Changing values does NOT invalidate the current slice"); + } + + imgui.end(); +} +#endif // ENABLE_BINARIZED_GCODE_DEBUG_WINDOW const ModelVolume *get_model_volume(const GLVolume &v, const Model &model) { const ModelVolume * ret = nullptr; @@ -7820,10 +7963,10 @@ const ModelVolume *get_model_volume(const GLVolume &v, const Model &model) return ret; } -const ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects) +ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects) { for (const ModelObject *obj : objects) - for (const ModelVolume *vol : obj->volumes) + for (ModelVolume *vol : obj->volumes) if (vol->id() == volume_id) return vol; return nullptr; diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index f86dda3..88ee311 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -162,6 +162,7 @@ wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); +wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MIRRORED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent); wxDECLARE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); //Y5 @@ -616,6 +617,7 @@ private: // list of transforms used to render the contours std::vector> m_instances; bool m_evaluating{ false }; + bool m_dragging{ false }; std::vector> m_hulls_2d_cache; @@ -625,6 +627,9 @@ private: void render(); bool empty() const { return m_contours.empty(); } + void start_dragging() { m_dragging = true; } + bool is_dragging() const { return m_dragging; } + void stop_dragging() { m_dragging = false; } friend class GLCanvas3D; }; @@ -963,11 +968,18 @@ public: void reset_sequential_print_clearance() { m_sequential_print_clearance.m_evaluating = false; + if (m_sequential_print_clearance.is_dragging()) + m_sequential_print_clearance_first_displacement = true; + else m_sequential_print_clearance.set_contours(ContoursList(), false); + set_as_dirty(); + request_extra_frame(); } void set_sequential_print_clearance_contours(const ContoursList& contours, bool generate_fill) { m_sequential_print_clearance.set_contours(contours, generate_fill); + set_as_dirty(); + request_extra_frame(); } bool is_sequential_print_clearance_empty() const { @@ -979,7 +991,11 @@ public: } void update_sequential_clearance(bool force_contours_generation); - void set_sequential_clearance_as_evaluating() { m_sequential_print_clearance.m_evaluating = true; } + void set_sequential_clearance_as_evaluating() { + m_sequential_print_clearance.m_evaluating = true; + set_as_dirty(); + request_extra_frame(); + } const Print* fff_print() const; const SLAPrint* sla_print() const; @@ -1100,10 +1116,13 @@ private: bool _deactivate_arrange_menu(); float get_overlay_window_width() { return LayersEditing::get_overlay_window_width(); } +#if ENABLE_BINARIZED_GCODE_DEBUG_WINDOW + void show_binary_gcode_debug_window(); +#endif // ENABLE_BINARIZED_GCODE_DEBUG_WINDOW }; const ModelVolume *get_model_volume(const GLVolume &v, const Model &model); -const ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects); +ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects); ModelVolume *get_model_volume(const GLVolume &v, const ModelObjectPtrs &objects); ModelVolume *get_model_volume(const GLVolume &v, const ModelObject &object); diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index a42d73d..f88b380 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -203,7 +203,7 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt } break; case coPoints:{ - if (opt_key == "bed_shape" || opt_key == "bed_exclude_area" || opt_key == "thumbnails") { + if (opt_key == "bed_shape") { config.option(opt_key)->values = boost::any_cast>(value); break; } @@ -338,7 +338,7 @@ void show_substitutions_info(const PresetsConfigSubstitutions& presets_config_su }; for (const PresetConfigSubstitutions& substitution : presets_config_substitutions) { - changes += "\n\n" + format_wxstr("%1% : %2%", preset_type_name(substitution.preset_type), bold_string(substitution.preset_name)); + changes += "\n\n" + format_wxstr("%1% : %2%", preset_type_name(substitution.preset_type), bold_string(from_u8(substitution.preset_name))); if (!substitution.preset_file.empty()) changes += format_wxstr(" (%1%)", substitution.preset_file); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index f015795..272015d 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -87,6 +87,8 @@ #include "DesktopIntegrationDialog.hpp" #include "SendSystemInfoDialog.hpp" #include "Downloader.hpp" +#include "PhysicalPrinterDialog.hpp" +#include "WifiConfigDialog.hpp" #include "BitmapCache.hpp" #include "Notebook.hpp" @@ -483,8 +485,8 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = { /* FT_STEP */ { "STEP files"sv, { ".stp"sv, ".step"sv } }, /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, - /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, - /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, + /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".bgcode"sv, ".bgc"sv, ".g"sv, ".ngc"sv } }, + /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv, ".svg"sv } }, /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, /* FT_FONTS */ { "Font files"sv, { ".ttc"sv, ".ttf"sv } }, /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, @@ -786,15 +788,10 @@ void GUI_App::post_init() this->mainframe->load_config_file(this->init_params->load_configs.back()); // If loading a 3MF file, the config is loaded from the last one. if (!this->init_params->input_files.empty()) { -#if 1 // #ysFIXME_delete_after_test_of wxArrayString fns; for (const std::string& name : this->init_params->input_files) fns.Add(from_u8(name)); if (plater()->load_files(fns) && this->init_params->input_files.size() == 1) { -#else - const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); - if (!res.empty() && this->init_params->input_files.size() == 1) { -#endif // Update application titlebar when opening a project file const std::string& filename = this->init_params->input_files.front(); if (boost::algorithm::iends_with(filename, ".amf") || @@ -825,20 +822,21 @@ void GUI_App::post_init() // This is ugly but I honestly found no better way to do it. // Neither wxShowEvent nor wxWindowCreateEvent work reliably. if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. +#if 0 // This code was moved to EVT_CONFIG_UPDATER_SYNC_DONE bind - after preset_updater finishes synchronization. if (! this->check_updates(false)) // Configuration is not compatible and reconfigure was refused by the user. Application is closing. return; +#endif CallAfter([this] { // preset_updater->sync downloads profile updates on background so it must begin after config wizard finished. bool cw_showed = this->config_wizard_startup(); - this->preset_updater->sync(preset_bundle); - //B20 - /*if (! cw_showed) { + this->preset_updater->sync(preset_bundle, this); + if (! cw_showed) { // The CallAfter is needed as well, without it, GL extensions did not show. // Also, we only want to show this when the wizard does not, so the new user // sees something else than "we want something" on the first start. show_send_system_info_dialog_if_needed(); - }*/ + } // app version check is asynchronous and triggers blocking dialog window, better call it last this->app_version_check(false); }); @@ -1093,8 +1091,19 @@ bool GUI_App::OnInit() } } +static int get_app_font_pt_size(const AppConfig* app_config) +{ + if (!app_config->has("font_pt_size")) + return -1; + const int font_pt_size = atoi(app_config->get("font_pt_size").c_str()); + const int max_font_pt_size = wxGetApp().get_max_font_pt_size(); + + return (font_pt_size > max_font_pt_size) ? max_font_pt_size : font_pt_size; +} bool GUI_App::on_init_inner() { + // TODO: remove this when all asserts are gone. + wxDisableAsserts(); // Set initialization of image handlers before any UI actions - See GH issue #7469 wxInitAllImageHandlers(); @@ -1244,7 +1253,6 @@ bool GUI_App::on_init_inner() if (! older_data_dir_path.empty()) { preset_bundle->import_newer_configs(older_data_dir_path); - //app_config->save(); // It looks like redundant call of save. ysFIXME delete after testing } if (is_editor()) { @@ -1291,11 +1299,16 @@ bool GUI_App::on_init_inner() show_error(nullptr, evt.GetString()); }); + Bind(EVT_CONFIG_UPDATER_SYNC_DONE, [this](const wxCommandEvent& evt) { + this->check_updates(false); + }); } else { #ifdef __WXMSW__ if (app_config->get_bool("associate_gcode")) associate_gcode_files(); + if (app_config->get_bool("associate_bgcode")) + associate_bgcode_files(); #endif // __WXMSW__ } @@ -1328,7 +1341,7 @@ bool GUI_App::on_init_inner() if (!delayed_error_load_presets.empty()) show_error(nullptr, delayed_error_load_presets); - mainframe = new MainFrame(app_config->has("font_size") ? atoi(app_config->get("font_size").c_str()) : -1); + mainframe = new MainFrame(get_app_font_pt_size(app_config)); // hide settings tabs after first Layout if (is_editor()) mainframe->select_tab(size_t(0)); @@ -1631,6 +1644,7 @@ void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) #ifdef _WIN32 UpdateDarkUI(dvc, highlited ? dark_mode() : false); #ifdef _MSW_DARK_MODE + if (!dvc->HasFlag(wxDV_NO_HEADER)) dvc->RefreshHeaderDarkMode(&m_normal_font); #endif //_MSW_DARK_MODE if (dvc->HasFlag(wxDV_ROW_LINES)) @@ -1653,6 +1667,29 @@ void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) #endif } +void GUI_App::SetWindowVariantForButton(wxButton* btn) +{ +#ifdef __APPLE__ + // This is a limit imposed by OSX. The way the native button widget is drawn only allows it to be stretched horizontally, + // and the vertical size is fixed. (see https://stackoverflow.com/questions/29083891/wxpython-button-size-being-ignored-on-osx) + // But standard height is possible to change using SetWindowVariant method (see https://docs.wxwidgets.org/3.0/window_8h.html#a879bccd2c987fedf06030a8abcbba8ac) + if (m_normal_font.GetPointSize() > 15) { + btn->SetWindowVariant(wxWINDOW_VARIANT_LARGE); + btn->SetFont(m_normal_font); + } +#endif +} + +int GUI_App::get_max_font_pt_size() +{ + const unsigned disp_count = wxDisplay::GetCount(); + for (unsigned i = 0; i < disp_count; i++) { + const wxRect display_rect = wxDisplay(i).GetGeometry(); + if (display_rect.width >= 2560 && display_rect.height >= 1440) + return 20; + } + return 15; +} void GUI_App::init_fonts() { m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); @@ -1759,9 +1796,26 @@ bool GUI_App::tabs_as_menu() const return app_config->get_bool("tabs_as_menu"); // || dark_mode(); } -wxSize GUI_App::get_min_size() const +bool GUI_App::suppress_round_corners() const { - return wxSize(76*m_em_unit, 49 * m_em_unit); + return true;// app_config->get("suppress_round_corners") == "1"; +} + +wxSize GUI_App::get_min_size(wxWindow* display_win) const +{ + wxSize min_size(76*m_em_unit, 49 * m_em_unit); + + const wxDisplay display = wxDisplay(display_win); + wxRect display_rect = display.GetGeometry(); + display_rect.width *= 0.75; + display_rect.height *= 0.75; + + if (min_size.x > display_rect.GetWidth()) + min_size.x = display_rect.GetWidth(); + if (min_size.y > display_rect.GetHeight()) + min_size.y = display_rect.GetHeight(); + + return min_size; } float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const @@ -1836,7 +1890,7 @@ void GUI_App::recreate_GUI(const wxString& msg_name) dlg.Update(10, _L("Recreating") + dots); MainFrame *old_main_frame = mainframe; - mainframe = new MainFrame(app_config->has("font_size") ? atoi(app_config->get("font_size").c_str()) : -1); + mainframe = new MainFrame(get_app_font_pt_size(app_config)); if (is_editor()) // hide settings tabs after first Layout mainframe->select_tab(size_t(0)); @@ -2026,7 +2080,7 @@ void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const { input_file.Clear(); wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), + _L("Choose one file (GCODE/GCO/G/BGCODE/BGC/NGC):"), app_config->get_last_dir(), "", file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); @@ -2127,6 +2181,9 @@ int GUI_App::GetSingleChoiceIndex(const wxString& message, #ifdef _WIN32 wxSingleChoiceDialog dialog(nullptr, message, caption, choices); wxGetApp().UpdateDlgDarkUI(&dialog); + auto children = dialog.GetChildren(); + for (auto child : children) + child->SetFont(normal_font()); dialog.SetSelection(initialSelection); return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; @@ -2550,6 +2607,19 @@ void GUI_App::add_config_menu(wxMenuBar *menu) case ConfigMenuFlashFirmware: FirmwareDialog::run(mainframe); break; + case ConfigMenuWifiConfigFile: + { + open_wifi_config_dialog(true); + /* + std::string file_path; + WifiConfigDialog dialog(mainframe, file_path, removable_drive_manager()); + if (dialog.ShowModal() == wxID_OK) + { + plater_->get_notification_manager()->push_exporting_finished_notification(file_path, boost::filesystem::path(file_path).parent_path().string(), true); + } + */ + } + break; default: break; } @@ -2594,6 +2664,8 @@ void GUI_App::open_preferences(const std::string& highlight_option /*= std::stri else { if (app_config->get_bool("associate_gcode")) associate_gcode_files(); + if (app_config->get_bool("associate_bgcode")) + associate_bgcode_files(); } #endif // _WIN32 @@ -2949,7 +3021,8 @@ ObjectSettings* GUI_App::obj_settings() ObjectList* GUI_App::obj_list() { - return sidebar().obj_list(); + // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) + return plater_ ? sidebar().obj_list() : nullptr; } ObjectLayers* GUI_App::obj_layers() @@ -3387,6 +3460,12 @@ void GUI_App::associate_gcode_files() { associate_file_type(L".gcode", L"QIDISlicer.GCodeViewer.1", L"QIDISlicerGCodeViewer", true); } + +void GUI_App::associate_bgcode_files() +{ + associate_file_type(L".bgcode", L"QIDISlicer.GCodeViewer.1", L"QIDISlicerGCodeViewer", true); +} + #endif // __WXMSW__ void GUI_App::on_version_read(wxCommandEvent& evt) @@ -3503,5 +3582,37 @@ void GUI_App::start_download(std::string url) m_downloader->start_download(url); } +void GUI_App::open_wifi_config_dialog(bool forced, const wxString& drive_path/* = {}*/) +{ + if(m_wifi_config_dialog_shown) + return; + + bool dialog_was_declined = app_config->get_bool("wifi_config_dialog_declined"); + + if (!forced && dialog_was_declined) { + + // dialog was already declined this run, show only notification + notification_manager()->push_notification(NotificationType::WifiConfigFileDetected + , NotificationManager::NotificationLevel::ImportantNotificationLevel + // TRN Text of notification when Slicer starts and usb stick with printer settings ini file is present + , _u8L("Printer configuration file detected on removable media.") + // TRN Text of hypertext of notification when Slicer starts and usb stick with printer settings ini file is present + , _u8L("Write Wi-Fi credentials."), [drive_path](wxEvtHandler* evt_hndlr) { + wxGetApp().open_wifi_config_dialog(true, drive_path); + return true; }); + return; + } + + m_wifi_config_dialog_shown = true; + std::string file_path; + WifiConfigDialog dialog(mainframe, file_path, removable_drive_manager(), drive_path); + if (dialog.ShowModal() == wxID_OK) { + plater_->get_notification_manager()->push_exporting_finished_notification(file_path, boost::filesystem::path(file_path).parent_path().string(), true); + app_config->set("wifi_config_dialog_declined", "0"); + } else { + app_config->set("wifi_config_dialog_declined", "1"); + } + m_wifi_config_dialog_shown = false; +} } // GUI } //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 8ff20a7..2d23f98 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -96,6 +96,7 @@ enum ConfigMenuIDs { ConfigMenuLanguage, ConfigMenuFlashFirmware, ConfigMenuCnt, + ConfigMenuWifiConfigFile }; class Tab; @@ -212,6 +213,7 @@ public: void UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited = false); // update color mode for panel including all static texts controls void UpdateAllStaticTextDarkUI(wxWindow* parent); + void SetWindowVariantForButton(wxButton* btn); void init_fonts(); void update_fonts(const MainFrame *main_frame = nullptr); void set_label_clr_modified(const wxColour& clr); @@ -251,7 +253,9 @@ public: const wxFont& link_font() { return m_link_font; } int em_unit() const { return m_em_unit; } bool tabs_as_menu() const; - wxSize get_min_size() const; + bool suppress_round_corners() const; + wxSize get_min_size(wxWindow* display_win) const; + int get_max_font_pt_size(); float toolbar_icon_scale(const bool is_limited = false) const; void set_auto_toolbar_icon_scale(float scale) const; void check_printer_presets(); @@ -374,12 +378,15 @@ public: //Y void associate_step_files(); void associate_gcode_files(); + void associate_bgcode_files(); #endif // __WXMSW__ // URL download - QIDISlicer gets system call to open qidislicer:// URL which should contain address of download void start_download(std::string url); + void open_wifi_config_dialog(bool forced, const wxString& drive_path = {}); + bool get_wifi_config_dialog_shown() const { return m_wifi_config_dialog_shown; } private: bool on_init_inner(); void init_app_config(); @@ -403,6 +410,8 @@ private: bool m_datadir_redefined { false }; + bool m_wifi_config_dialog_shown { false }; + bool m_wifi_config_dialog_was_declined { false }; }; DECLARE_APP(GUI_App) diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index fd3faac..fc22752 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -14,6 +14,7 @@ #include "Selection.hpp" #include "format.hpp" #include "Gizmos/GLGizmoEmboss.hpp" +#include "Gizmos/GLGizmoSVG.hpp" #include #include "slic3r/Utils/FixModelByWin10.hpp" @@ -170,6 +171,12 @@ static const constexpr std::array, 3> TEXT {L("Add negative text"), "add_text_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME {L("Add text modifier"), "add_text_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER }}; +// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important +static const constexpr std::array, 3> SVG_VOLUME_ICONS{{ + {L("Add SVG part"), "svg_part"}, // ~ModelVolumeType::MODEL_PART + {L("Add negative SVG"), "svg_negative"}, // ~ModelVolumeType::NEGATIVE_VOLUME + {L("Add SVG modifier"), "svg_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER +}}; static Plater* plater() { @@ -441,7 +448,7 @@ std::vector MenuFactory::get_volume_bitmaps() { std::vector volume_bmps; volume_bmps.reserve(ADD_VOLUME_MENU_ITEMS.size()); - for (auto item : ADD_VOLUME_MENU_ITEMS) + for (const auto& item : ADD_VOLUME_MENU_ITEMS) volume_bmps.push_back(get_bmp_bundle(item.second)); return volume_bmps; } @@ -450,7 +457,16 @@ std::vector MenuFactory::get_text_volume_bitmaps() { std::vector volume_bmps; volume_bmps.reserve(TEXT_VOLUME_ICONS.size()); - for (auto item : TEXT_VOLUME_ICONS) + for (const auto& item : TEXT_VOLUME_ICONS) + volume_bmps.push_back(get_bmp_bundle(item.second)); + return volume_bmps; +} + +std::vector MenuFactory::get_svg_volume_bitmaps() +{ + std::vector volume_bmps; + volume_bmps.reserve(SVG_VOLUME_ICONS.size()); + for (const auto &item : SVG_VOLUME_ICONS) volume_bmps.push_back(get_bmp_bundle(item.second)); return volume_bmps; } @@ -487,6 +503,7 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty } append_menu_item_add_text(sub_menu, type); + append_menu_item_add_svg(sub_menu, type); if (mode >= comAdvanced) { sub_menu->AppendSeparator(); @@ -497,15 +514,11 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty return sub_menu; } -void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item/* = true*/) -{ - auto add_text = [type](wxCommandEvent& evt) { - GLCanvas3D* canvas = plater()->canvas3D(); - GLGizmosManager& mng = canvas->get_gizmos_manager(); - GLGizmoBase* gizmo = mng.get_gizmo(GLGizmosManager::Emboss); - GLGizmoEmboss* emboss = dynamic_cast(gizmo); - assert(emboss != nullptr); - if (emboss == nullptr) return; +static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) { + auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { + const GLCanvas3D *canvas = plater()->canvas3D(); + const GLGizmosManager &mng = canvas->get_gizmos_manager(); + GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type); ModelVolumeType volume_type = type; // no selected object means create new object @@ -513,26 +526,44 @@ void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, volume_type = ModelVolumeType::MODEL_PART; auto screen_position = canvas->get_popup_menu_position(); + if (gizmo_type == GLGizmosManager::Emboss) { + auto emboss = dynamic_cast(gizmo_base); + assert(emboss != nullptr); + if (emboss == nullptr) return; if (screen_position.has_value()) { emboss->create_volume(volume_type, *screen_position); } else { emboss->create_volume(volume_type); } + } else if (gizmo_type == GLGizmosManager::Svg) { + auto svg = dynamic_cast(gizmo_base); + assert(svg != nullptr); + if (svg == nullptr) return; + if (screen_position.has_value()) { + svg->create_volume(volume_type, *screen_position); + } else { + svg->create_volume(volume_type); + } + } }; - if ( type == ModelVolumeType::MODEL_PART - || type == ModelVolumeType::NEGATIVE_VOLUME - || type == ModelVolumeType::PARAMETER_MODIFIER - || type == ModelVolumeType::INVALID // cannot use gizmo without selected object + if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER || + type == ModelVolumeType::INVALID // cannot use gizmo without selected object ) { - wxString item_name = is_submenu_item ? "" : _(ADD_VOLUME_MENU_ITEMS[int(type)].first) + ": "; - item_name += _L("Text"); + wxString item_name = wxString(is_submenu_item ? "" : _(ADD_VOLUME_MENU_ITEMS[int(type)].first) + ": ") + name; menu->AppendSeparator(); const std::string icon_name = is_submenu_item ? "" : ADD_VOLUME_MENU_ITEMS[int(type)].second; - append_menu_item(menu, wxID_ANY, item_name, "", add_text, icon_name, menu); + append_menu_item(menu, wxID_ANY, item_name, "", add_, icon_name, menu); } } +void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item/* = true*/){ + append_menu_itemm_add_(_L("Text"), GLGizmosManager::Emboss, menu, type, is_submenu_item); +} + +void MenuFactory::append_menu_item_add_svg(wxMenu *menu, ModelVolumeType type, bool is_submenu_item /* = true*/){ + append_menu_itemm_add_(_L("SVG"), GLGizmosManager::Svg, menu, type, is_submenu_item); +} void MenuFactory::append_menu_items_add_volume(MenuType menu_type) { wxMenu* menu = menu_type == mtObjectFFF ? &m_object_menu : menu_type == mtObjectSLA ? &m_sla_object_menu : nullptr; @@ -781,13 +812,13 @@ void MenuFactory::append_menu_items_osx(wxMenu* menu) menu->AppendSeparator(); } -wxMenuItem* MenuFactory::append_menu_item_fix_through_netfabb(wxMenu* menu) +wxMenuItem* MenuFactory::append_menu_item_fix_through_winsdk(wxMenu* menu) { if (!is_windows10()) return nullptr; - wxMenuItem* menu_item = append_menu_item(menu, wxID_ANY, _L("Fix through the Netfabb"), "", - [](wxCommandEvent&) { obj_list()->fix_through_netfabb(); }, "", menu, - []() {return plater()->can_fix_through_netfabb(); }, m_parent); + wxMenuItem* menu_item = append_menu_item(menu, wxID_ANY, _L("Fix by Windows repair algorithm"), "", + [](wxCommandEvent&) { obj_list()->fix_through_winsdk(); }, "", menu, + []() {return plater()->can_fix_through_winsdk(); }, m_parent); return menu_item; } @@ -985,15 +1016,18 @@ void MenuFactory::append_menu_item_edit_text(wxMenu *menu) wxString name = _L("Edit text"); auto can_edit_text = []() { - if (plater() != nullptr) { - const Selection& sel = plater()->get_selection(); - if (sel.volumes_count() == 1) { - const GLVolume* gl_vol = sel.get_first_volume(); - const ModelVolume* vol = plater()->model().objects[gl_vol->object_idx()]->volumes[gl_vol->volume_idx()]; - return vol->text_configuration.has_value(); - } - } + if (plater() == nullptr) + return false; + const Selection& selection = plater()->get_selection(); + if (selection.volumes_count() != 1) + return false; + const GLVolume* gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) + return false; + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume == nullptr) return false; + return volume->is_text(); }; if (menu != &m_text_part_menu) { @@ -1005,7 +1039,7 @@ void MenuFactory::append_menu_item_edit_text(wxMenu *menu) } wxString description = _L("Ability to change text, font, size, ..."); - std::string icon = ""; + std::string icon = "cog"; auto open_emboss = [](const wxCommandEvent &) { GLGizmosManager &mng = plater()->canvas3D()->get_gizmos_manager(); if (mng.get_current_type() == GLGizmosManager::Emboss) @@ -1015,6 +1049,42 @@ void MenuFactory::append_menu_item_edit_text(wxMenu *menu) append_menu_item(menu, wxID_ANY, name, description, open_emboss, icon, nullptr, can_edit_text, m_parent); } +void MenuFactory::append_menu_item_edit_svg(wxMenu *menu) +{ + wxString name = _L("Edit SVG"); + auto can_edit_svg = []() { + if (plater() == nullptr) + return false; + const Selection& selection = plater()->get_selection(); + if (selection.volumes_count() != 1) + return false; + const GLVolume* gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) + return false; + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume == nullptr) + return false; + return volume->is_svg(); + }; + + if (menu != &m_svg_part_menu) { + const int menu_item_id = menu->FindItem(name); + if (menu_item_id != wxNOT_FOUND) + menu->Destroy(menu_item_id); + if (!can_edit_svg()) + return; + } + + wxString description = _L("Change SVG source file, projection, size, ..."); + std::string icon = "cog"; + auto open_svg = [](const wxCommandEvent &) { + GLGizmosManager &mng = plater()->canvas3D()->get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); // close() and reopen - move to be visible + mng.open_gizmo(GLGizmosManager::Svg); + }; + append_menu_item(menu, wxID_ANY, name, description, open_svg, icon, nullptr, can_edit_svg, m_parent); +} MenuFactory::MenuFactory() { for (int i = 0; i < mtCount; i++) { @@ -1067,7 +1137,7 @@ void MenuFactory::create_common_object_menu(wxMenu* menu) // "Scale to print volume" makes a sense just for whole object append_menu_item_scale_selection_to_fit_print_volume(menu); - append_menu_item_fix_through_netfabb(menu); + append_menu_item_fix_through_winsdk(menu); append_menu_item_simplify(menu); append_menu_items_mirror(menu); @@ -1116,7 +1186,7 @@ void MenuFactory::create_part_menu() append_menu_item_reload_from_disk(menu); append_menu_item_replace_with_stl(menu); append_menu_item_export_stl(menu); - append_menu_item_fix_through_netfabb(menu); + append_menu_item_fix_through_winsdk(menu); append_menu_item_simplify(menu); append_menu_item(menu, wxID_ANY, _L("Split"), _L("Split the selected object into individual parts"), @@ -1130,9 +1200,21 @@ void MenuFactory::create_text_part_menu() { wxMenu* menu = &m_text_part_menu; - append_menu_item_delete(menu); append_menu_item_edit_text(menu); - append_menu_item_fix_through_netfabb(menu); + append_menu_item_delete(menu); + append_menu_item_fix_through_winsdk(menu); + append_menu_item_simplify(menu); + + append_immutable_part_menu_items(menu); +} + +void MenuFactory::create_svg_part_menu() +{ + wxMenu* menu = &m_svg_part_menu; + + append_menu_item_edit_svg(menu); + append_menu_item_delete(menu); + append_menu_item_fix_through_winsdk(menu); append_menu_item_simplify(menu); append_immutable_part_menu_items(menu); @@ -1155,6 +1237,7 @@ void MenuFactory::init(wxWindow* parent) create_common_object_menu(&m_sla_object_menu); create_part_menu(); create_text_part_menu(); + create_svg_part_menu(); create_instance_menu(); } @@ -1177,6 +1260,7 @@ wxMenu* MenuFactory::object_menu() update_menu_items_instance_manipulation(mtObjectFFF); append_menu_item_invalidate_cut_info(&m_object_menu); append_menu_item_edit_text(&m_object_menu); + append_menu_item_edit_svg(&m_object_menu); return &m_object_menu; } @@ -1188,6 +1272,7 @@ wxMenu* MenuFactory::sla_object_menu() update_menu_items_instance_manipulation(mtObjectSLA); append_menu_item_invalidate_cut_info(&m_sla_object_menu); append_menu_item_edit_text(&m_sla_object_menu); + append_menu_item_edit_svg(&m_object_menu); return &m_sla_object_menu; } @@ -1208,6 +1293,11 @@ wxMenu* MenuFactory::text_part_menu() return &m_text_part_menu; } +wxMenu *MenuFactory::svg_part_menu() +{ + append_mutable_part_menu_items(&m_svg_part_menu); + return &m_svg_part_menu; +} wxMenu* MenuFactory::instance_menu() { return &m_instance_menu; @@ -1236,7 +1326,7 @@ wxMenu* MenuFactory::multi_selection_menu() wxMenu* menu = new MenuWithSeparators(); - append_menu_item_fix_through_netfabb(menu); + append_menu_item_fix_through_winsdk(menu); append_menu_item_reload_from_disk(menu); append_menu_items_convert_unit(menu); if (obj_list()->can_merge_to_multipart_object()) diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index 515311d..a0c8997 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -35,6 +35,7 @@ class MenuFactory public: static std::vector get_volume_bitmaps(); static std::vector get_text_volume_bitmaps(); + static std::vector get_svg_volume_bitmaps(); MenuFactory(); ~MenuFactory() = default; @@ -52,6 +53,7 @@ public: wxMenu* sla_object_menu(); wxMenu* part_menu(); wxMenu* text_part_menu(); + wxMenu* svg_part_menu(); wxMenu* instance_menu(); wxMenu* layer_menu(); wxMenu* multi_selection_menu(); @@ -68,6 +70,7 @@ private: MenuWithSeparators m_object_menu; MenuWithSeparators m_part_menu; MenuWithSeparators m_text_part_menu; + MenuWithSeparators m_svg_part_menu; MenuWithSeparators m_sla_object_menu; MenuWithSeparators m_default_menu; MenuWithSeparators m_instance_menu; @@ -83,10 +86,12 @@ private: void append_mutable_part_menu_items(wxMenu* menu); void create_part_menu(); void create_text_part_menu(); + void create_svg_part_menu(); void create_instance_menu(); wxMenu* append_submenu_add_generic(wxMenu* menu, ModelVolumeType type); void append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item = true); + void append_menu_item_add_svg(wxMenu *menu, ModelVolumeType type, bool is_submenu_item = true); void append_menu_items_add_volume(MenuType type); wxMenuItem* append_menu_item_layers_editing(wxMenu* menu); wxMenuItem* append_menu_item_settings(wxMenu* menu); @@ -95,7 +100,7 @@ private: wxMenuItem* append_menu_item_printable(wxMenu* menu); void append_menu_item_invalidate_cut_info(wxMenu *menu); void append_menu_items_osx(wxMenu* menu); - wxMenuItem* append_menu_item_fix_through_netfabb(wxMenu* menu); + wxMenuItem* append_menu_item_fix_through_winsdk(wxMenu* menu); wxMenuItem* append_menu_item_simplify(wxMenu* menu); void append_menu_item_export_stl(wxMenu* menu); void append_menu_item_reload_from_disk(wxMenu* menu); @@ -108,6 +113,7 @@ private: // void append_menu_item_merge_to_single_object(wxMenu *menu); void append_menu_items_mirror(wxMenu *menu); void append_menu_item_edit_text(wxMenu *menu); + void append_menu_item_edit_svg(wxMenu *menu); void append_menu_items_instance_manipulation(wxMenu *menu); void update_menu_items_instance_manipulation(MenuType type); void append_menu_items_split(wxMenu *menu); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 2be574f..a7994a1 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -477,7 +477,7 @@ MeshErrorsInfo ObjectList::get_mesh_errors_info(const int obj_idx, const int vol *sidebar_info = stats.manifold() ? auto_repaired_info : (remaining_info + (stats.repaired() ? ("\n" + auto_repaired_info) : "")); if (is_windows10() && !sidebar_info) - tooltip += "\n" + _L("Right button click the icon to fix STL through Netfabb"); + tooltip += "\n" + _L("Right button click the icon to fix STL by Windows repair algorithm"); return { tooltip, get_warning_icon_name(stats) }; } @@ -996,7 +996,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me { if (is_windows10() && m_objects_model->HasWarningIcon(item) && mouse_pos.x > 2 * wxGetApp().em_unit() && mouse_pos.x < 4 * wxGetApp().em_unit()) - fix_through_netfabb(); + fix_through_winsdk(); else if (evt_context_menu) show_context_menu(evt_context_menu); // show context menu for "Name" column too } @@ -1034,7 +1034,11 @@ void ObjectList::show_context_menu(const bool evt_context_menu) get_selected_item_indexes(obj_idx, vol_idx, item); if (obj_idx < 0 || vol_idx < 0) return; - menu = object(obj_idx)->volumes[vol_idx]->text_configuration.has_value() ? plater->text_part_menu() : plater->part_menu(); + const ModelVolume *volume = object(obj_idx)->volumes[vol_idx]; + + menu = volume->is_text() ? plater->text_part_menu() : + volume->is_svg() ? plater->svg_part_menu() : + plater->part_menu(); } else menu = type & itInstance ? plater->instance_menu() : @@ -1512,12 +1516,7 @@ void ObjectList::load_subobject(ModelVolumeType type, bool from_galery/* = false take_snapshot((type == ModelVolumeType::MODEL_PART) ? _L("Load Part") : _L("Load Modifier")); std::vector volumes; - // ! ysFIXME - delete commented code after testing and rename "load_modifier" to something common - /* - if (type == ModelVolumeType::MODEL_PART) - load_part(*(*m_objects)[obj_idx], volumes, type, from_galery); - else*/ - load_modifier(input_files, *(*m_objects)[obj_idx], volumes, type, from_galery); + load_from_files(input_files, *(*m_objects)[obj_idx], volumes, type, from_galery); if (volumes.empty()) return; @@ -1537,71 +1536,9 @@ void ObjectList::load_subobject(ModelVolumeType type, bool from_galery/* = false selection_changed(); } -/* -void ObjectList::load_part(ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery = false) + +void ObjectList::load_from_files(const wxArrayString& input_files, ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery) { - if (type != ModelVolumeType::MODEL_PART) - return; - - wxWindow* parent = wxGetApp().tab_panel()->GetPage(0); - - wxArrayString input_files; - - if (from_galery) { - GalleryDialog dlg(this); - if (dlg.ShowModal() == wxID_CLOSE) - return; - dlg.get_input_files(input_files); - if (input_files.IsEmpty()) - return; - } - else - wxGetApp().import_model(parent, input_files); - - wxProgressDialog dlg(_L("Loading") + dots, "", 100, wxGetApp().mainframe wxPD_AUTO_HIDE); - wxBusyCursor busy; - - for (size_t i = 0; i < input_files.size(); ++i) { - std::string input_file = input_files.Item(i).ToUTF8().data(); - - dlg.Update(static_cast(100.0f * static_cast(i) / static_cast(input_files.size())), - _L("Loading file") + ": " + from_path(boost::filesystem::path(input_file).filename())); - dlg.Fit(); - - Model model; - try { - model = Model::read_from_file(input_file); - } - catch (std::exception &e) { - auto msg = _L("Error!") + " " + input_file + " : " + e.what() + "."; - show_error(parent, msg); - exit(1); - } - - for (auto object : model.objects) { - Vec3d delta = Vec3d::Zero(); - if (model_object.origin_translation != Vec3d::Zero()) { - object->center_around_origin(); - delta = model_object.origin_translation - object->origin_translation; - } - for (auto volume : object->volumes) { - volume->translate(delta); - auto new_volume = model_object.add_volume(*volume, type); - new_volume->name = boost::filesystem::path(input_file).filename().string(); - // set a default extruder value, since user can't add it manually - new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); - - added_volumes.push_back(new_volume); - } - } - } -} -*/ -void ObjectList::load_modifier(const wxArrayString& input_files, ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery) -{ - // ! ysFIXME - delete commented code after testing and rename "load_modifier" to something common - //if (type == ModelVolumeType::MODEL_PART) - // return; wxWindow* parent = wxGetApp().tab_panel()->GetPage(0); @@ -1841,12 +1778,7 @@ void ObjectList::load_shape_object_from_gallery(const wxArrayString& input_files wxGetApp().mainframe->update_title(); } -void ObjectList::load_mesh_object( - const TriangleMesh & mesh, - const std::string & name, - bool center, - const TextConfiguration *text_config /* = nullptr*/, - const Transform3d * transformation /* = nullptr*/) +void ObjectList::load_mesh_object(const TriangleMesh &mesh, const std::string &name, bool center) { PlaterAfterLoadAutoArrange plater_after_load_auto_arrange; // Add mesh to model as a new object @@ -1856,7 +1788,6 @@ void ObjectList::load_mesh_object( check_model_ids_validity(model); #endif /* _DEBUG */ - std::vector object_idxs; ModelObject* new_object = model.add_object(); new_object->name = name; new_object->add_instance(); // each object should have at list one instance @@ -1864,31 +1795,22 @@ void ObjectList::load_mesh_object( ModelVolume* new_volume = new_object->add_volume(mesh); new_object->sort_volumes(wxGetApp().app_config->get_bool("order_volumes")); new_volume->name = name; - if (text_config) - new_volume->text_configuration = *text_config; // set a default extruder value, since user can't add it manually new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); new_object->invalidate_bounding_box(); - if (transformation) { - assert(!center); - Slic3r::Geometry::Transformation tr(*transformation); - new_object->instances[0]->set_transformation(tr); - } else { auto bb = mesh.bounding_box(); new_object->translate(-bb.center()); new_object->instances[0]->set_offset( center ? to_3d(wxGetApp().plater()->build_volume().bounding_volume2d().center(), -new_object->origin_translation.z()) : bb.center()); - } new_object->ensure_on_bed(); - object_idxs.push_back(model.objects.size() - 1); #ifdef _DEBUG check_model_ids_validity(model); #endif /* _DEBUG */ - paste_objects_into_list(object_idxs); + paste_objects_into_list({model.objects.size() - 1}); #ifdef _DEBUG check_model_ids_validity(model); @@ -2544,7 +2466,9 @@ bool ObjectList::has_selected_cut_object() const for (wxDataViewItem item : sels) { const int obj_idx = m_objects_model->GetObjectIdByItem(item); - if (obj_idx >= 0 && object(obj_idx)->is_cut()) + // ys_FIXME: The obj_idx= 0 && obj_idx < int(m_objects->size()) && object(obj_idx)->is_cut()) return true; } @@ -3061,6 +2985,7 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st volume_idx, volume->type(), volume->is_text(), + volume->is_svg(), get_warning_icon_name(volume->mesh().stats()), extruder2str(volume->config.has("extruder") ? volume->config.extruder() : 0)); add_settings_item(vol_item, &volume->config.get()); @@ -4337,7 +4262,8 @@ void ObjectList::change_part_type() types.emplace_back(ModelVolumeType::PARAMETER_MODIFIER); } - if (!volume->text_configuration.has_value()) { + // is not embossed(SVG or Text) + if (!volume->emboss_shape.has_value()) { for (const wxString& name : { _L("Support Blocker"), _L("Support Enforcer") }) names.Add(name); for (const ModelVolumeType type_id : { ModelVolumeType::SUPPORT_BLOCKER, ModelVolumeType::SUPPORT_ENFORCER }) @@ -4629,7 +4555,7 @@ void ObjectList::rename_item() update_name_in_model(item); } -void ObjectList::fix_through_netfabb() +void ObjectList::fix_through_winsdk() { // Do not fix anything when a gizmo is open. There might be issues with updates // and what is worse, the snapshot time would refer to the internal stack. @@ -4649,21 +4575,21 @@ void ObjectList::fix_through_netfabb() // clear selections from the non-broken models if any exists // and than fill names of models to repairing if (vol_idxs.empty()) { -#if !FIX_THROUGH_NETFABB_ALWAYS +#if !FIX_THROUGH_WINSDK_ALWAYS for (int i = int(obj_idxs.size())-1; i >= 0; --i) if (object(obj_idxs[i])->get_repaired_errors_count() == 0) obj_idxs.erase(obj_idxs.begin()+i); -#endif // FIX_THROUGH_NETFABB_ALWAYS +#endif // FIX_THROUGH_WINSDK_ALWAYS for (int obj_idx : obj_idxs) model_names.push_back(object(obj_idx)->name); } else { ModelObject* obj = object(obj_idxs.front()); -#if !FIX_THROUGH_NETFABB_ALWAYS +#if !FIX_THROUGH_WINSDK_ALWAYS for (int i = int(vol_idxs.size()) - 1; i >= 0; --i) if (obj->get_repaired_errors_count(vol_idxs[i]) == 0) vol_idxs.erase(vol_idxs.begin() + i); -#endif // FIX_THROUGH_NETFABB_ALWAYS +#endif // FIX_THROUGH_WINSDK_ALWAYS for (int vol_idx : vol_idxs) model_names.push_back(obj->volumes[vol_idx]->name); } @@ -4707,19 +4633,19 @@ void ObjectList::fix_through_netfabb() return true; }; - Plater::TakeSnapshot snapshot(plater, _L("Fix through NetFabb")); + Plater::TakeSnapshot snapshot(plater, _L("Fix by Windows repair algorithm")); // Open a progress dialog. - wxProgressDialog progress_dlg(_L("Fixing through NetFabb"), "", 100, find_toplevel_parent(plater), + wxProgressDialog progress_dlg(_L("Fixing by Windows repair algorithm"), "", 100, find_toplevel_parent(plater), wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT); int model_idx{ 0 }; if (vol_idxs.empty()) { int vol_idx{ -1 }; for (int obj_idx : obj_idxs) { -#if !FIX_THROUGH_NETFABB_ALWAYS +#if !FIX_THROUGH_WINSDK_ALWAYS if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0) continue; -#endif // FIX_THROUGH_NETFABB_ALWAYS +#endif // FIX_THROUGH_WINSDK_ALWAYS if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models)) break; model_idx++; @@ -4752,7 +4678,7 @@ void ObjectList::fix_through_netfabb() } if (msg.IsEmpty()) msg = _L("Repairing was canceled"); - plater->get_notification_manager()->push_notification(NotificationType::NetfabbFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, boost::nowide::narrow(msg)); + plater->get_notification_manager()->push_notification(NotificationType::RepairFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, boost::nowide::narrow(msg)); } void ObjectList::simplify() diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index d2965c7..e86d16f 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -27,7 +27,6 @@ class ModelConfig; class ModelObject; class ModelVolume; class TriangleMesh; -struct TextConfiguration; enum class ModelVolumeType : int; // FIXME: broken build on mac os because of this is missing: @@ -38,7 +37,7 @@ typedef std::pair t_layer_height_range; typedef std::map t_layer_config_ranges; // Manifold mesh may contain self-intersections, so we want to always allow fixing the mesh. -#define FIX_THROUGH_NETFABB_ALWAYS 1 +#define FIX_THROUGH_WINSDK_ALWAYS 1 namespace GUI { @@ -251,15 +250,12 @@ public: bool is_instance_or_object_selected(); bool is_selected_object_cut(); void load_subobject(ModelVolumeType type, bool from_galery = false); - // ! ysFIXME - delete commented code after testing and rename "load_modifier" to something common - //void load_part(ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery = false); - void load_modifier(const wxArrayString& input_files, ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery = false); + void load_from_files(const wxArrayString& input_files, ModelObject& model_object, std::vector& added_volumes, ModelVolumeType type, bool from_galery = false); void load_generic_subobject(const std::string& type_name, const ModelVolumeType type); void load_shape_object(const std::string &type_name); void load_shape_object_from_gallery(); void load_shape_object_from_gallery(const wxArrayString& input_files); - void load_mesh_object(const TriangleMesh &mesh, const std::string &name, bool center = true, - const TextConfiguration* text_config = nullptr, const Transform3d* transformation = nullptr); + void load_mesh_object(const TriangleMesh &mesh, const std::string &name, bool center = true); bool del_object(const int obj_idx); bool del_subobject_item(wxDataViewItem& item); void del_settings_from_config(const wxDataViewItem& parent_item); @@ -385,7 +381,7 @@ public: void instances_to_separated_objects(const int obj_idx); void split_instances(); void rename_item(); - void fix_through_netfabb(); + void fix_through_winsdk(); void simplify(); void update_item_error_icon(const int obj_idx, int vol_idx) const ; diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 8cd146d..20b36fc 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -21,6 +21,7 @@ #include #include "slic3r/Utils/FixModelByWin10.hpp" +#include "Widgets/CheckBox.hpp" // For special mirroring in manipulation gizmo #include "Gizmos/GLGizmosManager.hpp" #include "Gizmos/GLGizmoEmboss.hpp" @@ -73,7 +74,7 @@ static choice_ctrl* create_word_local_combo(wxWindow *parent) wxSize size(15 * wxGetApp().em_unit(), -1); choice_ctrl* temp = nullptr; -#ifdef __WXOSX__ +#if 0//def __WXOSX__ /* wxBitmapComboBox with wxCB_READONLY style return NULL for GetTextCtrl(), * so ToolTip doesn't shown. * Next workaround helps to solve this problem @@ -82,7 +83,7 @@ static choice_ctrl* create_word_local_combo(wxWindow *parent) temp->SetTextCtrlStyle(wxTE_READONLY); temp->Create(parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr); #else - temp = new choice_ctrl(parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, wxCB_READONLY | wxBORDER_SIMPLE); + temp = new choice_ctrl(parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, wxCB_READONLY | DD_NO_CHECK_ICON); #endif //__WXOSX__ temp->SetFont(Slic3r::GUI::wxGetApp().normal_font()); @@ -133,7 +134,7 @@ void msw_rescale_word_local_combo(choice_ctrl* combo) static void set_font_and_background_style(wxWindow* win, const wxFont& font) { win->SetFont(font); - win->SetBackgroundStyle(wxBG_STYLE_PAINT); + if (!wxOSX) win->SetBackgroundStyle(wxBG_STYLE_PAINT); } static const wxString axes_color_text[] = { "#990000", "#009900", "#000099" }; @@ -158,19 +159,19 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : // Add "Name" label with warning icon auto sizer = new wxBoxSizer(wxHORIZONTAL); - m_fix_throught_netfab_bitmap = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); + m_fix_by_winsdk_bitmap = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); if (is_windows10()) - m_fix_throught_netfab_bitmap->Bind(wxEVT_CONTEXT_MENU, [this](wxCommandEvent& e) + m_fix_by_winsdk_bitmap->Bind(wxEVT_CONTEXT_MENU, [this](wxCommandEvent& e) { // if object/sub-object has no errors - if (m_fix_throught_netfab_bitmap->GetBitmap().GetRefData() == wxNullBitmap.GetRefData()) + if (m_fix_by_winsdk_bitmap->GetBitmap().GetRefData() == wxNullBitmap.GetRefData()) return; - wxGetApp().obj_list()->fix_through_netfabb(); + wxGetApp().obj_list()->fix_through_winsdk(); update_warning_icon_state(wxGetApp().obj_list()->get_mesh_errors_info()); }); - sizer->Add(m_fix_throught_netfab_bitmap); + sizer->Add(m_fix_by_winsdk_bitmap); auto name_label = new wxStaticText(m_parent, wxID_ANY, _L("Name")+":"); set_font_and_background_style(name_label, wxGetApp().normal_font()); @@ -491,16 +492,16 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : }); m_main_grid_sizer->Add(m_reset_skew_button); - m_check_inch = new wxCheckBox(parent, wxID_ANY, _L("Inches")); + m_check_inch = CheckBox::GetNewWin(parent, _L("Inches")); m_check_inch->SetFont(wxGetApp().normal_font()); - m_check_inch->SetValue(m_imperial_units); + CheckBox::SetValue(m_check_inch, m_imperial_units); m_check_inch->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { - wxGetApp().app_config->set("use_inches", m_check_inch->GetValue() ? "1" : "0"); + wxGetApp().app_config->set("use_inches", CheckBox::GetValue(m_check_inch) ? "1" : "0"); wxGetApp().sidebar().update_ui_from_settings(); }); - m_main_grid_sizer->Add(m_check_inch, 1, wxEXPAND); + m_main_grid_sizer->Add(m_check_inch); m_og->activate(); m_og->sizer->Clear(true); @@ -529,7 +530,7 @@ void ObjectManipulation::Show(const bool show) const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); bool show_world_local_combo = wxGetApp().get_mode() != comSimple && (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()); if (selection.is_single_volume_or_modifier() && m_word_local_combo->GetCount() < 3) { -#ifdef __linux__ +#if 0//def __linux__ m_word_local_combo->Insert(coordinate_type_str(ECoordinatesType::Local), 2); #else m_word_local_combo->Insert(coordinate_type_str(ECoordinatesType::Local), wxNullBitmap, 2); @@ -611,7 +612,7 @@ void ObjectManipulation::update_ui_from_settings() update(3/*meSize*/, m_new_size); } } - m_check_inch->SetValue(m_imperial_units); + CheckBox::SetValue(m_check_inch, m_imperial_units); if (m_use_colors != wxGetApp().app_config->get_bool("color_mapinulation_panel")) { m_use_colors = wxGetApp().app_config->get_bool("color_mapinulation_panel"); @@ -891,9 +892,9 @@ void ObjectManipulation::update_warning_icon_state(const MeshErrorsInfo& warning !warning_icon_name.empty()) m_manifold_warning_bmp = ScalableBitmap(m_parent, warning_icon_name); const wxString& tooltip = warning.tooltip; - m_fix_throught_netfab_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp()); - m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0,0) : m_manifold_warning_bmp.GetSize()); - m_fix_throught_netfab_bitmap->SetToolTip(tooltip); + m_fix_by_winsdk_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp()); + m_fix_by_winsdk_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0,0) : m_manifold_warning_bmp.GetSize()); + m_fix_by_winsdk_bitmap->SetToolTip(tooltip); } wxString ObjectManipulation::coordinate_type_str(ECoordinatesType type) @@ -1168,9 +1169,9 @@ void ObjectManipulation::msw_rescale() msw_rescale_word_local_combo(m_word_local_combo); m_word_local_combo_sizer->SetMinSize(wxSize(-1, m_word_local_combo->GetBestHeight(-1))); - const wxString& tooltip = m_fix_throught_netfab_bitmap->GetToolTipText(); - m_fix_throught_netfab_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp()); - m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0, 0) : m_manifold_warning_bmp.GetSize()); + const wxString& tooltip = m_fix_by_winsdk_bitmap->GetToolTipText(); + m_fix_by_winsdk_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp()); + m_fix_by_winsdk_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0, 0) : m_manifold_warning_bmp.GetSize()); // rescale label-heights // Text trick to grid sizer layout: @@ -1196,6 +1197,7 @@ void ObjectManipulation::sys_color_changed() wxGetApp().UpdateDarkUI(m_word_local_combo); wxGetApp().UpdateDarkUI(m_check_inch); #endif + CheckBox::SysColorChanged(m_check_inch); for (ManipulationEditor* editor : m_editors) editor->sys_color_changed(this); @@ -1224,19 +1226,12 @@ static const char axes[] = { 'x', 'y', 'z' }; ManipulationEditor::ManipulationEditor(ObjectManipulation* parent, const std::string& opt_key, int axis) : - wxTextCtrl(parent->parent(), wxID_ANY, wxEmptyString, wxDefaultPosition, - wxSize((wxOSX ? 5 : 6)*int(wxGetApp().em_unit()), wxDefaultCoord), wxTE_PROCESS_ENTER -#ifdef _WIN32 - | wxBORDER_SIMPLE -#endif - ), + ::TextInput(parent->parent(), "", "", "", wxDefaultPosition, + wxSize(int(5.8 * wxGetApp().em_unit()), wxDefaultCoord), wxTE_PROCESS_ENTER), m_opt_key(opt_key), m_axis(axis) { set_font_and_background_style(this, wxGetApp().normal_font()); -#ifdef __WXOSX__ - this->OSXDisableAllSmartSubstitutions(); -#endif // __WXOSX__ if (parent->use_colors()) { this->SetBackgroundColour(wxColour(axes_color_back[axis])); this->SetForegroundColour(*wxBLACK); @@ -1266,14 +1261,14 @@ ManipulationEditor::ManipulationEditor(ObjectManipulation* parent, e.Skip(); }, this->GetId()); - this->Bind(wxEVT_SET_FOCUS, [this, parent](wxFocusEvent& e) + this->GetTextCtrl()->Bind(wxEVT_SET_FOCUS, [this, parent](wxFocusEvent& e) { parent->set_focused_editor(this); // needed to show the visual hints in 3D scene wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(m_full_opt_name, true); e.Skip(); - }, this->GetId()); + }); this->Bind(wxEVT_CHAR, ([this](wxKeyEvent& event) { @@ -1292,8 +1287,7 @@ ManipulationEditor::ManipulationEditor(ObjectManipulation* parent, void ManipulationEditor::msw_rescale() { - const int em = wxGetApp().em_unit(); - SetMinSize(wxSize(5 * em, wxDefaultCoord)); + SetCtrlSize(wxSize(int(5.8 * wxGetApp().em_unit()), wxDefaultCoord)); } void ManipulationEditor::sys_color_changed(ObjectManipulation* parent) diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.hpp b/src/slic3r/GUI/GUI_ObjectManipulation.hpp index 6b5dfe5..43b07f4 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.hpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.hpp @@ -9,6 +9,8 @@ #include "libslic3r/Point.hpp" #include +#include "Widgets/ComboBox.hpp" +#include "Widgets/TextInput.hpp" #ifdef __WXOSX__ class wxBitmapComboBox; #else @@ -17,7 +19,6 @@ class wxComboBox; class wxStaticText; class LockButton; class wxStaticBitmap; -class wxCheckBox; namespace Slic3r { namespace GUI { @@ -26,6 +27,9 @@ namespace GUI { class BitmapComboBox; #endif +#if 1 + using choice_ctrl = ::ComboBox; +#else #ifdef __WXOSX__ static_assert(wxMAJOR_VERSION >= 3, "Use of wxBitmapComboBox on Manipulation panel requires wxWidgets 3.0 and newer"); using choice_ctrl = wxBitmapComboBox; @@ -36,11 +40,12 @@ class BitmapComboBox; using choice_ctrl = wxComboBox; #endif #endif // __WXOSX__ +#endif class Selection; class ObjectManipulation; -class ManipulationEditor : public wxTextCtrl +class ManipulationEditor : public ::TextInput { std::string m_opt_key; int m_axis; @@ -124,7 +129,7 @@ private: ScalableButton* m_reset_skew_button{ nullptr }; ScalableButton* m_drop_to_bed_button{ nullptr }; - wxCheckBox* m_check_inch {nullptr}; + wxWindow* m_check_inch {nullptr}; std::array m_mirror_buttons; @@ -148,7 +153,7 @@ private: choice_ctrl* m_word_local_combo { nullptr }; ScalableBitmap m_manifold_warning_bmp; - wxStaticBitmap* m_fix_throught_netfab_bitmap{ nullptr }; + wxStaticBitmap* m_fix_by_winsdk_bitmap{ nullptr }; wxStaticBitmap* m_mirror_warning_bitmap{ nullptr }; // Currently focused editor (nullptr if none) diff --git a/src/slic3r/GUI/GUI_Utils.cpp b/src/slic3r/GUI/GUI_Utils.cpp index fad4703..1c35e6b 100644 --- a/src/slic3r/GUI/GUI_Utils.cpp +++ b/src/slic3r/GUI/GUI_Utils.cpp @@ -1,5 +1,6 @@ #include "GUI_Utils.hpp" #include "GUI_App.hpp" +#include "format.hpp" #include #include @@ -300,8 +301,10 @@ TaskTimer::~TaskTimer() { std::chrono::milliseconds stop_timer = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()); - auto process_duration = std::chrono::milliseconds(stop_timer - start_timer).count(); - std::string out = (boost::format("\n!!! %1% duration = %2% ms \n\n") % task_name % process_duration).str(); + const auto timer_delta = stop_timer - start_timer; + const auto process_duration_ms = std::chrono::milliseconds(timer_delta).count(); + const auto process_duration_s = std::chrono::duration_cast>(timer_delta).count(); + std::string out = format("\n! \"%1%\" duration = %2% s (%3% ms) \n", task_name, process_duration_s, process_duration_ms); printf("%s", out.c_str()); #ifdef __WXMSW__ std::wstring stemp = std::wstring(out.begin(), out.end()); diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index 624e7e1..0022d97 100644 --- a/src/slic3r/GUI/GUI_Utils.hpp +++ b/src/slic3r/GUI/GUI_Utils.hpp @@ -92,13 +92,13 @@ public: if (font_point_size > 0) m_normal_font.SetPointSize(font_point_size); + else if (parent) + m_normal_font.SetPointSize(parent->GetFont().GetPointSize()); /* Because of default window font is a primary display font, * We should set correct font for window before getting em_unit value. */ -#ifndef __WXOSX__ // Don't call SetFont under OSX to avoid name cutting in ObjectList this->SetFont(m_normal_font); -#endif this->CenterOnParent(); #ifdef _WIN32 update_dark_ui(this); @@ -107,7 +107,13 @@ public: // Linux specific issue : get_dpi_for_window(this) still doesn't responce to the Display's scale in new wxWidgets(3.1.3). // So, calculate the m_em_unit value from the font size, as before #if !defined(__WXGTK__) - m_em_unit = std::max(10, 10.0f * m_scale_factor); +#ifdef _WIN32 + const double font_to_em_koef = 10./9.;// Default font point size on Windows is 9 pt +#else // ifdef __WXOSX__ + const double font_to_em_koef = 10./11.;// Default font point size on OSX is 11 pt +#endif + m_em_unit_from_font_size = int(font_to_em_koef * m_normal_font.GetPointSize()); + m_em_unit = std::max(10, int(m_scale_factor * m_em_unit_from_font_size)); #else // initialize default width_unit according to the width of the one symbol ("m") of the currently active font of this window. m_em_unit = std::max(10, this->GetTextExtent("m").x - 1); @@ -174,7 +180,6 @@ public: float prev_scale_factor() const { return m_prev_scale_factor; } int em_unit() const { return m_em_unit; } -// int font_size() const { return m_font_size; } const wxFont& normal_font() const { return m_normal_font; } void enable_force_rescale() { m_force_rescale = true; } @@ -193,7 +198,7 @@ protected: private: float m_scale_factor; int m_em_unit; -// int m_font_size; + int m_em_unit_from_font_size {10}; wxFont m_normal_font; float m_prev_scale_factor; @@ -202,13 +207,6 @@ private: int m_new_font_point_size; -// void recalc_font() -// { -// wxClientDC dc(this); -// const auto metrics = dc.GetFontMetrics(); -// m_font_size = metrics.height; -// m_em_unit = metrics.averageWidth; -// } // check if new scale is differ from previous bool is_new_scale_factor() const { return fabs(m_scale_factor - m_prev_scale_factor) > 0.001; } @@ -250,7 +248,7 @@ private: m_normal_font = this->GetFont(); // update em_unit value for new window font - m_em_unit = std::max(10, 10.0f * m_scale_factor); + m_em_unit = std::max(10, int(m_scale_factor * m_em_unit_from_font_size)); // rescale missed controls sizes and images on_dpi_changed(suggested_rect); diff --git a/src/slic3r/GUI/GalleryDialog.cpp b/src/slic3r/GUI/GalleryDialog.cpp index 5ac51d5..91b0b24 100644 --- a/src/slic3r/GUI/GalleryDialog.cpp +++ b/src/slic3r/GUI/GalleryDialog.cpp @@ -94,10 +94,12 @@ GalleryDialog::GalleryDialog(wxWindow* parent) : #endif wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK | wxCLOSE); - m_ok_btn = static_cast(FindWindowById(wxID_OK, this)); + wxGetApp().SetWindowVariantForButton(buttons->GetCancelButton()); + m_ok_btn = buttons->GetAffirmativeButton(); + wxGetApp().SetWindowVariantForButton(m_ok_btn); m_ok_btn->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(!m_selected_items.empty()); }); - static_cast(FindWindowById(wxID_CLOSE, this))->Bind(wxEVT_BUTTON, [this](wxCommandEvent&){ this->EndModal(wxID_CLOSE); }); + buttons->GetCancelButton()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&){ this->EndModal(wxID_CLOSE); }); this->SetEscapeId(wxID_CLOSE); auto add_btn = [this, buttons]( size_t pos, int& ID, wxString title, wxString tooltip, void (GalleryDialog::* method)(wxEvent&), @@ -105,6 +107,7 @@ GalleryDialog::GalleryDialog(wxWindow* parent) : ID = NewControlId(); wxButton* btn = new wxButton(this, ID, title); btn->SetToolTip(tooltip); + wxGetApp().SetWindowVariantForButton(btn); btn->Bind(wxEVT_UPDATE_UI, [enable_fn](wxUpdateUIEvent& evt) { evt.Enable(enable_fn()); }); buttons->Insert(pos, btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, BORDER_W); this->Bind(wxEVT_BUTTON, method, this, ID); @@ -410,8 +413,7 @@ void GalleryDialog::load_label_icon_list() int img_cnt = m_image_list->GetImageCount(); for (int i = 0; i < img_cnt; i++) { m_list_ctrl->InsertItem(i, from_u8(list_items[i].name), i); - if (list_items[i].is_system) - m_list_ctrl->SetItemFont(i, wxGetApp().bold_font()); + m_list_ctrl->SetItemData(i, list_items[i].is_system ? 1 : 0); } } @@ -510,7 +512,7 @@ void GalleryDialog::change_thumbnail() void GalleryDialog::select(wxListEvent& event) { int idx = event.GetIndex(); - Item item { into_u8(m_list_ctrl->GetItemText(idx)), m_list_ctrl->GetItemFont(idx).GetWeight() == wxFONTWEIGHT_BOLD }; + Item item { into_u8(m_list_ctrl->GetItemText(idx)), m_list_ctrl->GetItemData(idx) == static_cast(1)}; m_selected_items.push_back(item); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 21795ae..2a44c7b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -228,6 +228,7 @@ GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, {"Shape" , _u8L("Shape")}, {"Depth" , _u8L("Depth")}, {"Size" , _u8L("Size")}, + {"Rotation" , _u8L("Rotation")}, {"Groove" , _u8L("Groove")}, {"Width" , _u8L("Width")}, {"Flap Angle" , _u8L("Flap Angle")}, @@ -559,10 +560,10 @@ bool GLGizmoCut3D::render_double_input(const std::string& label, double& value_i bool GLGizmoCut3D::render_slider_double_input(const std::string& label, float& value_in, float& tolerance_in, float min_val/* = -0.1f*/, float max_tolerance/* = -0.1f*/) { - constexpr float UndefMinVal = -0.1f; + static constexpr const float UndefMinVal = -0.1f; const float f_mm_to_in = static_cast(ObjectManipulation::mm_to_in); - auto render_slider = [this, UndefMinVal, f_mm_to_in] + auto render_slider = [this, f_mm_to_in] (const std::string& label, float& val, float def_val, float max_val, const wxString& tooltip) { float min_val = val < 0.f ? UndefMinVal : def_val; float value = val; @@ -2326,6 +2327,10 @@ void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors) connectors[idx].radius_tolerance = 0.5f * m_connector_size_tolerance; }); + if (render_angle_input(m_labels_map["Rotation"], m_connector_angle, 0.f, 0.f, 180.f)) + apply_selected_connectors([this, &connectors](size_t idx) { + connectors[idx].z_angle = m_connector_angle; + }); if (m_connector_type == CutConnectorType::Snap) { render_snap_specific_input(_u8L("Bulge"), _L("Bulge proportion related to radius"), m_snap_bulge_proportion, 0.15f, 5.f, 100.f * m_snap_space_proportion); render_snap_specific_input(_u8L("Space"), _L("Space proportion related to radius"), m_snap_space_proportion, 0.3f, 10.f, 50.f); @@ -2535,7 +2540,7 @@ void GLGizmoCut3D::render_groove_float_input(const std::string& label, float& in } } -void GLGizmoCut3D::render_groove_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) +bool GLGizmoCut3D::render_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) { bool is_changed{ false }; @@ -2548,13 +2553,15 @@ void GLGizmoCut3D::render_groove_angle_input(const std::string& label, float& in const float old_val = val; const std::string format = "%.0f " + _u8L("°"); - m_imgui->slider_float(("##groove_" + label).c_str(), &val, min_val, max_val, format.c_str(), 1.f, true, from_u8(label)); + m_imgui->slider_float(("##angle_" + label).c_str(), &val, min_val, max_val, format.c_str(), 1.f, true, from_u8(label)); m_is_slider_editing_done |= m_imgui->get_last_slider_status().deactivated_after_edit; if (!is_approx(old_val, val)) { if (m_imgui->get_last_slider_status().can_take_snapshot) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), format_wxstr("%1%: %2%", _L("Groove change"), label), UndoRedo::SnapshotType::GizmoAction); + // TRN: This is an entry in the Undo/Redo stack. The whole line will be 'Edited: (name of whatever was edited)'. + Plater::TakeSnapshot snapshot(wxGetApp().plater(), format_wxstr("%1%: %2%", _L("Edited"), label), UndoRedo::SnapshotType::GizmoAction); m_imgui->get_last_slider_status().invalidate_snapshot(); + if (m_mode == size_t(CutMode::cutTongueAndGroove)) m_groove_editing = true; } in_val = deg2rad(val); @@ -2565,14 +2572,19 @@ void GLGizmoCut3D::render_groove_angle_input(const std::string& label, float& in m_imgui->disabled_begin(is_approx(in_val, init_val)); const std::string act_name = _u8L("Reset"); - if (render_reset_button(("##groove_" + label + act_name).c_str(), act_name)) { + if (render_reset_button(("##angle_" + label + act_name).c_str(), act_name)) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), format_wxstr("%1%: %2%", act_name, label), UndoRedo::SnapshotType::GizmoAction); in_val = init_val; is_changed = true; } m_imgui->disabled_end(); - if (is_changed) { + return is_changed; +} + +void GLGizmoCut3D::render_groove_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) +{ + if (render_angle_input(label, in_val, init_val, min_val, max_val)) { update_plane_model(); reset_cut_by_contours(); } @@ -2790,6 +2802,8 @@ void GLGizmoCut3D::validate_connector_settings() m_connector_size = 2.5f; if (m_connector_size_tolerance < 0.f) m_connector_size_tolerance = 0.f; + if (m_connector_angle < 0.f || m_connector_angle > float(PI) ) + m_connector_angle = 0.f; if (m_connector_type == CutConnectorType::Undef) m_connector_type = CutConnectorType::Plug; @@ -2809,6 +2823,7 @@ void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) float depth_ratio_tolerance { UndefFloat }; float radius { UndefFloat }; float radius_tolerance { UndefFloat }; + float angle { UndefFloat }; CutConnectorType type { CutConnectorType::Undef }; CutConnectorStyle style { CutConnectorStyle::Undef }; CutConnectorShape shape { CutConnectorShape::Undef }; @@ -2822,6 +2837,7 @@ void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) depth_ratio_tolerance = connector.height_tolerance; radius = connector.radius; radius_tolerance = connector.radius_tolerance; + angle = connector.z_angle; type = connector.attribs.type; style = connector.attribs.style; shape = connector.attribs.shape; @@ -2839,6 +2855,8 @@ void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) radius = UndefFloat; if (!is_approx(radius_tolerance, connector.radius_tolerance)) radius_tolerance = UndefFloat; + if (!is_approx(angle, connector.z_angle)) + angle = UndefFloat; if (type != connector.attribs.type) type = CutConnectorType::Undef; @@ -2854,6 +2872,7 @@ void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) m_connector_size = 2.f * radius; m_connector_size_tolerance = 2.f * radius_tolerance; m_connector_type = type; + m_connector_angle = angle; m_connector_style = int(style); m_connector_shape_id = int(shape); } @@ -3020,7 +3039,7 @@ void GLGizmoCut3D::toggle_model_objects_visibility() { bool has_active_volume = false; std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); - for (const auto raycaster : *raycasters) + for (const std::shared_ptr &raycaster : *raycasters) if (raycaster->is_active()) { has_active_volume = true; break; @@ -3106,6 +3125,7 @@ void GLGizmoCut3D::render_connectors() pos += 0.05 * m_clp_normal; const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(pos) * m_rotation_m * + rotation_transform(-connector.z_angle * Vec3d::UnitZ()) * scale_transform(Vec3f(connector.radius, connector.radius, height).cast()); render_model(m_shapes[connector.attribs].model, render_color, view_model_matrix); @@ -3232,6 +3252,31 @@ void update_object_cut_id(CutObjectBase& cut_id, ModelObjectCutAttributes attrib } } +static void check_objects_after_cut(const ModelObjectPtrs& objects) +{ + std::vector err_objects_names; + for (const ModelObject* object : objects) { + std::vector connectors_names; + connectors_names.reserve(object->volumes.size()); + for (const ModelVolume* vol : object->volumes) + if (vol->cut_info.is_connector) + connectors_names.push_back(vol->name); + const size_t connectors_count = connectors_names.size(); + sort_remove_duplicates(connectors_names); + if (connectors_count != connectors_names.size()) + err_objects_names.push_back(object->name); + } + if (err_objects_names.empty()) + return; + + wxString names = from_u8(err_objects_names[0]); + for (size_t i = 1; i < err_objects_names.size(); i++) + names += ", " + from_u8(err_objects_names[i]); + WarningDialog(wxGetApp().plater(), format_wxstr("Objects(%1%) have duplicated connectors. " + "Some connectors may be missing in slicing result.\n" + "Please report to PrusaSlicer team in which scenario this issue happened.\n" + "Thank you.", names)).ShowModal(); +} void synchronize_model_after_cut(Model& model, const CutObjectBase& cut_id) { for (ModelObject* obj : model.objects) @@ -3296,6 +3341,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) const ModelObjectPtrs& new_objects = cut_by_contour ? cut.perform_by_contour(m_part_selection.get_cut_parts(), dowels_count): cut_with_groove ? cut.perform_with_groove(m_groove, m_rotation_m) : cut.perform_with_plane(); + check_objects_after_cut(new_objects); // save cut_id to post update synchronization const CutObjectBase cut_id = cut_mo->cut_id; @@ -3508,6 +3554,7 @@ bool GLGizmoCut3D::add_connector(CutConnectors& connectors, const Vec2d& mouse_p connectors.emplace_back(pos, m_rotation_m, m_connector_size * 0.5f, m_connector_depth_ratio, m_connector_size_tolerance * 0.5f, m_connector_depth_ratio_tolerance, + m_connector_angle, CutConnectorAttributes( CutConnectorType(m_connector_type), CutConnectorStyle(m_connector_style), CutConnectorShape(m_connector_shape_id))); @@ -3732,6 +3779,7 @@ void GLGizmoCut3D::apply_cut_connectors(ModelObject* mo, const std::string& conn // Transform the new modifier to be aligned inside the instance new_volume->set_transformation(translation_transform(connector.pos) * connector.rotation_m * + rotation_transform(-connector.z_angle * Vec3d::UnitZ()) * scale_transform(Vec3f(connector.radius, connector.radius, connector.height).cast())); new_volume->cut_info = { connector.attribs.type, connector.radius_tolerance, connector.height_tolerance }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp index 1fa0386..675eb06 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp @@ -135,6 +135,7 @@ class GLGizmoCut3D : public GLGizmoBase float m_connector_depth_ratio{ 3.f }; float m_connector_size{ 2.5f }; + float m_connector_angle{ 0.f }; float m_connector_depth_ratio_tolerance{ 0.1f }; float m_connector_size_tolerance{ 0.f }; @@ -303,6 +304,7 @@ protected: void render_color_marker(float size, const ImU32& color); void render_groove_float_input(const std::string &label, float &in_val, const float &init_val, float &in_tolerance); void render_groove_angle_input(const std::string &label, float &in_val, const float &init_val, float min_val, float max_val); + bool render_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val); void render_snap_specific_input(const std::string& label, const wxString& tooltip, float& in_val, const float& init_val, const float min_val, const float max_val); void render_cut_plane_input_window(CutConnectors &connectors); void init_input_window_data(CutConnectors &connectors); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index beecd80..cb31567 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -16,12 +16,9 @@ #include "slic3r/Utils/WxFontUtils.hpp" #include "slic3r/Utils/UndoRedo.hpp" -// TODO: remove include -#include "libslic3r/SVG.hpp" // debug store -#include "libslic3r/Geometry.hpp" // covex hull 2d +#include "libslic3r/Geometry.hpp" // to range pi pi #include "libslic3r/Timer.hpp" -#include "libslic3r/NSVGUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/ClipperUtils.hpp" // union_ex @@ -30,15 +27,24 @@ #include "libslic3r/BuildVolume.hpp" #include "imgui/imgui_stdlib.h" // using std::string for inputs -#include "nanosvg/nanosvg.h" // load SVG file #include #include #include #include #include // detection of change DPI +#include #include +#include // serialize deserialize facenames +#include +#include + +// cache font list by cereal +#include +#include +#include +#include #include #include // measure enumeration of fonts @@ -57,7 +63,6 @@ #define SHOW_FINE_POSITION // draw convex hull around volume #define DRAW_PLACE_TO_ADD_TEXT // Interactive draw of window position #define ALLOW_OPEN_NEAR_VOLUME -#define EXECUTE_PROCESS_ON_MAIN_THREAD // debug execution on main thread #endif // ALLOW_DEBUG_MODE //#define USE_PIXEL_SIZE_IN_WX_FONT @@ -67,8 +72,7 @@ using namespace Slic3r::Emboss; using namespace Slic3r::GUI; using namespace Slic3r::GUI::Emboss; -namespace priv { -template struct MinMax { T min; T max;}; +namespace { template struct Limit { // Limitation for view slider range in GUI MinMax gui; @@ -79,7 +83,7 @@ template struct Limit { // Variable keep limits for variables static const struct Limits { - MinMax emboss{0.01f, 1e4f}; // in mm + MinMax emboss{0.01, 1e4}; // in mm MinMax size_in_mm{0.1f, 1000.f}; // in mm Limit boldness{{-200.f, 200.f}, {-2e4f, 2e4f}}; // in font points Limit skew{{-1.f, 1.f}, {-100.f, 100.f}}; // ration without unit @@ -88,59 +92,9 @@ static const struct Limits // distance text object from surface MinMax angle{-180.f, 180.f}; // in degrees - template - static bool apply(std::optional &val, const MinMax &limit) { - if (val.has_value()) - return apply(*val, limit); - return false; - } - template - static bool apply(T &val, const MinMax &limit) - { - if (val > limit.max) { - val = limit.max; - return true; - } - if (val < limit.min) { - val = limit.min; - return true; - } - return false; - } } limits; -// Define where is up vector on model -constexpr double up_limit = 0.9; - -// Normalize radian angle from -PI to PI -template void to_range_pi_pi(T& angle) -{ - if (angle > PI || angle < -PI) { - int count = static_cast(std::round(angle / (2 * PI))); - angle -= static_cast(count * 2 * PI); - } -} -} // namespace priv -using namespace priv; - -GLGizmoEmboss::GLGizmoEmboss(GLCanvas3D &parent) - : GLGizmoBase(parent, M_ICON_FILENAME, -2) - , m_volume(nullptr) - , m_is_unknown_font(false) - , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) - , m_style_manager(m_imgui->get_glyph_ranges(), create_default_styles) - , m_job_cancel(nullptr) -{ - m_rotate_gizmo.set_group_id(0); - m_rotate_gizmo.set_force_local_coordinate(true); - // TODO: add suggestion to use https://fontawesome.com/ - // (copy & paste) unicode symbols from web - // paste HEX unicode into notepad move cursor after unicode press [alt] + [x] -} - -// Private namespace with helper function for create volume -namespace priv { /// /// Prepare data for emboss @@ -152,68 +106,35 @@ namespace priv { /// Define type of volume - side of surface(in / out) /// Cancel for previous job /// Base data for emboss text -static DataBase create_emboss_data_base(const std::string &text, +std::unique_ptr create_emboss_data_base( + const std::string& text, StyleManager &style_manager, TextLinesModel &text_lines, const Selection &selection, ModelVolumeType type, std::shared_ptr> &cancel); +CreateVolumeParams create_input(GLCanvas3D &canvas, const StyleManager::Style &style, RaycastManager &raycaster, ModelVolumeType volume_type); /// -/// Start job for add new volume to object with given transformation +/// Move window for edit emboss text near to embossed object +/// NOTE: embossed object must be selected /// -/// Define where to add -/// Volume transformation -/// Define text -/// Type of volume -static void start_create_volume_job(const ModelObject *object, - const Transform3d volume_trmat, - DataBase &emboss_data, - ModelVolumeType volume_type); +ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size); -/// -/// Start job for add new volume on surface of object defined by screen coor -/// -/// Define params of text -/// Emboss / engrave -/// Mouse position which define position -/// Volume to find surface for create -/// Ability to ray cast to model -/// Per glyph transformation -/// Line height need font file/param> -/// Contain already used scene RayCasters -/// True when start creation, False when there is no hit surface by screen coor -static bool start_create_volume_on_surface_job(DataBase &emboss_data, - ModelVolumeType volume_type, - const Vec2d &screen_coor, - const GLVolume *gl_volume, - RaycastManager &raycaster, - TextLinesModel &text_lines, - /*const */ StyleManager &style_manager, - GLCanvas3D &canvas); +struct TextDataBase : public DataBase +{ + TextDataBase(DataBase &&parent, const FontFileWithCache &font_file, + TextConfiguration &&text_configuration, const EmbossProjection& projection); + // Create shape from text + font configuration + EmbossShape &create_shape() override; + void write(ModelVolume &volume) const override; -/// -/// Find volume in selected object with closest convex hull to screen center. -/// Return -/// -/// Define where to search for closest -/// Canvas center(dependent on camera settings) -/// Actual objects -/// OUT: coordinate of controid of closest volume -/// OUT: closest volume -static void find_closest_volume(const Selection &selection, - const Vec2d &screen_center, - const Camera &camera, - const ModelObjectPtrs &objects, - Vec2d *closest_center, - const GLVolume **closest_volume); - -/// -/// Start job for add object with text into scene -/// -/// Define params of text -/// Screen coordinat, where to create new object laying on bed -static void start_create_object_job(DataBase &emboss_data, const Vec2d &coor); +private: + // Keep pointer on Data of font (glyph shapes) + FontFileWithCache m_font_file; + // font item is not used for create object + TextConfiguration m_text_configuration; +}; // Loaded icons enum // Have to match order of files in function GLGizmoEmboss::init_icons() @@ -248,173 +169,174 @@ enum class IconState : unsigned { activable = 0, hovered /*1*/, disabled /*2*/ } // selector for icon by enum const IconManager::Icon &get_icon(const IconManager::VIcons& icons, IconType type, IconState state); // short call of Slic3r::GUI::button -static bool draw_button(const IconManager::VIcons& icons, IconType type, bool disable = false); +bool draw_button(const IconManager::VIcons& icons, IconType type, bool disable = false); -/// -/// Apply camera direction for emboss direction -/// -/// Define view vector -/// Containe Selected Model to modify -/// Keep same up vector -/// True when apply change otherwise false -static bool apply_camera_dir(const Camera &camera, GLCanvas3D &canvas, bool keep_up); +struct FaceName +{ + wxString wx_name; + std::string name_truncated = ""; + size_t texture_index = 0; + // State for generation of texture + // when start generate create share pointers + std::shared_ptr> cancel = nullptr; + // R/W only on main thread - finalize of job + std::shared_ptr is_created = nullptr; +}; -} // namespace priv +// Implementation of forwarded struct +// Keep sorted list of loadable face names +struct Facenames +{ + // flag to keep need of enumeration fonts from OS + // false .. wants new enumeration check by Hash + // true .. already enumerated(During opened combo box) + bool is_init = false; -namespace { + bool has_truncated_names = false; + + // data of can_load() faces + std::vector faces = {}; + // Sorter set of Non valid face names in OS + std::vector bad = {}; + + // Configuration of font encoding + static const wxFontEncoding encoding = wxFontEncoding::wxFONTENCODING_SYSTEM; + + // Identify if preview texture exists + GLuint texture_id = 0; + + // protection for open too much font files together + // Gtk:ERROR:../../../../gtk/gtkiconhelper.c:494:ensure_surface_for_gicon: assertion failed (error == NULL): Failed to load + // /usr/share/icons/Yaru/48x48/status/image-missing.png: Error opening file /usr/share/icons/Yaru/48x48/status/image-missing.png: Too + // many open files (g-io-error-quark, 31) This variable must exist until no CreateFontImageJob is running + unsigned int count_opened_font_files = 0; + + // Configuration for texture height + const int count_cached_textures = 32; + + // index for new generated texture index(must be lower than count_cached_textures) + size_t texture_index = 0; + + // hash created from enumerated font from OS + // check when new font was installed + size_t hash = 0; + + // filtration pattern + std::string search = ""; + std::vector hide; // result of filtration +}; + +bool store(const Facenames &facenames); +bool load(Facenames &facenames); +void init_face_names(Facenames &facenames); +void init_truncated_names(Facenames &face_names, float max_width); + +// This configs holds GUI layout size given by translated texts. +// etc. When language changes, GUI is recreated and this class constructed again, +// so the change takes effect. (info by GLGizmoFdmSupports.hpp) +struct GuiCfg +{ + // Detect invalid config values when change monitor DPI + double screen_scale; + float main_toolbar_height; + + // Zero means it is calculated in init function + ImVec2 minimal_window_size = ImVec2(0, 0); + ImVec2 minimal_window_size_with_advance = ImVec2(0, 0); + ImVec2 minimal_window_size_with_collections = ImVec2(0, 0); + float height_of_volume_type_selector = 0.f; + float input_width = 0.f; + float delete_pos_x = 0.f; + float max_style_name_width = 0.f; + unsigned int icon_width = 0; + + // maximal width and height of style image + Vec2i max_style_image_size = Vec2i(0, 0); + + float indent = 0.f; + float input_offset = 0.f; + float advanced_input_offset = 0.f; + float lock_offset = 0.f; + + ImVec2 text_size; + + // maximal size of face name image + Vec2i face_name_size = Vec2i(0, 0); + float face_name_texture_offset_x = 0.f; + + // maximal texture generate jobs running at once + unsigned int max_count_opened_font_files = 10; + + // Only translations needed for calc GUI size + struct Translations + { + std::string font; + std::string height; + std::string depth; + + // advanced + std::string use_surface; + std::string per_glyph; + std::string alignment; + std::string char_gap; + std::string line_gap; + std::string boldness; + std::string skew_ration; + std::string from_surface; + std::string rotation; + std::string keep_up; + std::string collection; + }; + Translations translations; +}; +GuiCfg create_gui_configuration(); + +void draw_font_preview(FaceName &face, const std::string &text, Facenames &faces, const GuiCfg &cfg, bool is_visible); // for existing volume which is selected(could init different(to volume text) lines count when edit text) void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* const*/ StyleManager &style_manager, unsigned count_lines=0); -// before text volume is created -void init_new_text_line(TextLinesModel &text_lines, const Transform3d& new_text_tr, const ModelObject& mo, /* const*/ StyleManager &style_manager); +} // namespace priv + +// use private definition +struct GLGizmoEmboss::Facenames: public ::Facenames{}; +struct GLGizmoEmboss::GuiCfg: public ::GuiCfg{}; + +GLGizmoEmboss::GLGizmoEmboss(GLCanvas3D &parent) + : GLGizmoBase(parent, M_ICON_FILENAME, -2) + , m_gui_cfg(nullptr) + , m_style_manager(m_imgui->get_glyph_ranges(), create_default_styles) + , m_face_names(std::make_unique()) + , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) +{ + m_rotate_gizmo.set_group_id(0); + m_rotate_gizmo.set_force_local_coordinate(true); + // to use https://fontawesome.com/ (copy & paste) unicode symbols from web + // paste HEX unicode into notepad move cursor after unicode press [alt] + [x] } -//B34 -void GLGizmoEmboss::create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos, std::string str) +bool GLGizmoEmboss::create_volume(ModelVolumeType volume_type, const Vec2d& mouse_pos) { if (!init_create(volume_type)) - return; + return false; - // select position by camera position and view direction - const Selection &selection = m_parent.get_selection(); - int object_idx = selection.get_object_idx(); - - Size s = m_parent.get_canvas_size(); - - Vec2d screen_center(s.get_width() / 2., s.get_height() / 2.); - DataBase emboss_data = priv::create_emboss_data_base(str, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, - m_job_cancel); - - const ModelObjectPtrs &objects = selection.get_model()->objects; - bool is_simple_mode = wxGetApp().get_mode() == comSimple; - // No selected object so create new object - if (selection.is_empty() || object_idx < 0 || static_cast(object_idx) >= objects.size() || is_simple_mode) { - // create Object on center of screen - // when ray throw center of screen not hit bed it create object on center of bed - priv::start_create_object_job(emboss_data, screen_center); - return; - } - - // create volume inside of selected object - Vec2d coor; - const GLVolume *vol = nullptr; - const Camera & camera = wxGetApp().plater()->get_camera(); - priv::find_closest_volume(selection, screen_center, camera, objects, &coor, &vol); - - // there is no point on surface so no use of surface will be applied - FontProp &prop = emboss_data.text_configuration.style.prop; - if (prop.use_surface) - prop.use_surface = false; - - - Plater *plater = wxGetApp().plater(); - // Transformation is inspired add generic volumes in ObjectList::load_generic_subobject - const ModelObject *obj = objects[vol->object_idx()]; - BoundingBoxf3 instance_bb = obj->instance_bounding_box(vol->instance_idx()); - - size_t vol_id = obj->volumes[vol->volume_idx()]->id().id; - - auto cond = RaycastManager::AllowVolumes({vol_id}); - - RaycastManager::Meshes meshes = create_meshes(m_parent, cond); - m_raycast_manager.actualize(*obj, &cond, &meshes); - std::optional hit = ray_from_camera(m_raycast_manager, coor, camera, &cond); - - DynamicPrintConfig *print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; - double pa_first_layer_height = print_config->get_abs_value("first_layer_height"); - double pa_layer_height = print_config->get_abs_value("layer_height"); - Transform3d surface_trmat = create_transformation_onto_surface(Vec3d(mouse_pos.x(), mouse_pos.y(), pa_first_layer_height), - hit->normal, - priv::up_limit); - emboss_data.text_configuration.style.prop.emboss = pa_layer_height; - emboss_data.text_configuration.style.prop.size_in_mm = 7; - - const FontProp &font_prop = emboss_data.text_configuration.style.prop; - apply_transformation(font_prop, surface_trmat); - Transform3d instance = vol->get_instance_transformation().get_matrix(); - Transform3d volume_trmat = instance.inverse() * surface_trmat; - start_create_volume_job(obj, volume_trmat, emboss_data, volume_type); - -} - - -void GLGizmoEmboss::create_volume(ModelVolumeType volume_type, const Vec2d& mouse_pos) -{ - if (!init_create(volume_type)) - return; - - const GLVolume *gl_volume = get_first_hovered_gl_volume(m_parent); - DataBase emboss_data = priv::create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); - bool is_simple_mode = wxGetApp().get_mode() == comSimple; - if (gl_volume != nullptr && !is_simple_mode) { - // Try to cast ray into scene and find object for add volume - if (!priv::start_create_volume_on_surface_job(emboss_data, volume_type, mouse_pos, gl_volume, m_raycast_manager, m_text_lines, m_style_manager, m_parent)) { - // When model is broken. It could appear that hit miss the object. - // So add part near by in simmilar manner as right panel do - create_volume(volume_type); - } - } else { - // object is not under mouse position soo create object on plater - priv::start_create_object_job(emboss_data, mouse_pos); - } + // NOTE: change style manager - be carefull with order changes + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); + CreateVolumeParams input = create_input(m_parent, m_style_manager.get_style(), m_raycast_manager, volume_type); + return start_create_volume(input, std::move(base), mouse_pos); } // Designed for create volume without information of mouse in scene -void GLGizmoEmboss::create_volume(ModelVolumeType volume_type) +bool GLGizmoEmboss::create_volume(ModelVolumeType volume_type) { if (!init_create(volume_type)) - return; + return false; - // select position by camera position and view direction - const Selection &selection = m_parent.get_selection(); - int object_idx = selection.get_object_idx(); - - Size s = m_parent.get_canvas_size(); - Vec2d screen_center(s.get_width() / 2., s.get_height() / 2.); - DataBase emboss_data = priv::create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); - const ModelObjectPtrs &objects = selection.get_model()->objects; - bool is_simple_mode = wxGetApp().get_mode() == comSimple; - // No selected object so create new object - if (selection.is_empty() || object_idx < 0 || static_cast(object_idx) >= objects.size() || is_simple_mode) { - // create Object on center of screen - // when ray throw center of screen not hit bed it create object on center of bed - priv::start_create_object_job(emboss_data, screen_center); - return; + // NOTE: change style manager - be carefull with order changes + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); + CreateVolumeParams input = create_input(m_parent, m_style_manager.get_style(), m_raycast_manager, volume_type); + return start_create_volume_without_position(input, std::move(base)); } - // create volume inside of selected object - Vec2d coor; - const GLVolume *vol = nullptr; - const Camera &camera = wxGetApp().plater()->get_camera(); - priv::find_closest_volume(selection, screen_center, camera, objects, &coor, &vol); - if (vol == nullptr) { - priv::start_create_object_job(emboss_data, screen_center); - } else if (!priv::start_create_volume_on_surface_job(emboss_data, volume_type, coor, vol, m_raycast_manager, m_text_lines, m_style_manager, m_parent)) { - // in centroid of convex hull is not hit with object - // soo create transfomation on border of object - - // there is no point on surface so no use of surface will be applied - FontProp &prop = emboss_data.text_configuration.style.prop; - if (prop.use_surface) - prop.use_surface = false; - - // Transformation is inspired add generic volumes in ObjectList::load_generic_subobject - const ModelObject *obj = objects[vol->object_idx()]; - BoundingBoxf3 instance_bb = obj->instance_bounding_box(vol->instance_idx()); - // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. - Transform3d tr = vol->get_instance_transformation().get_matrix_no_offset().inverse(); - Vec3d offset_tr(0, // center of instance - Can't suggest width of text before it will be created - - instance_bb.size().y() / 2 - prop.size_in_mm / 2, // under - prop.emboss / 2 - instance_bb.size().z() / 2 // lay on bed - ); - Transform3d volume_trmat = tr * Eigen::Translation3d(offset_tr); - if (prop.per_glyph) { - init_new_text_line(m_text_lines, volume_trmat, *obj, m_style_manager); - emboss_data.text_lines = m_text_lines.get_lines(); - } - priv::start_create_volume_job(obj, volume_trmat, emboss_data, volume_type); - } -} void GLGizmoEmboss::on_shortcut_key() { set_volume_by_selection(); @@ -423,7 +345,8 @@ void GLGizmoEmboss::on_shortcut_key() { // NOTE: After finish job for creation emboss Text volume, // GLGizmoEmboss will be opened create_volume(ModelVolumeType::MODEL_PART); - } else { + } + else { // shortcut is pressed when text is selected soo start edit it. auto &mng = m_parent.get_gizmos_manager(); if (mng.get_current_type() != GLGizmosManager::Emboss) @@ -431,6 +354,30 @@ void GLGizmoEmboss::on_shortcut_key() { } } +bool GLGizmoEmboss::re_emboss(const ModelVolume &text_volume, std::shared_ptr> job_cancel) +{ + assert(text_volume.text_configuration.has_value()); + assert(text_volume.emboss_shape.has_value()); + if (!text_volume.text_configuration.has_value() || + !text_volume.emboss_shape.has_value()) + return false; // not valid text volume to re emboss + const TextConfiguration &tc = *text_volume.text_configuration; + const EmbossShape &es = *text_volume.emboss_shape; + const ImWchar* ranges = ImGui::GetIO().Fonts->GetGlyphRangesDefault(); + + StyleManager style_manager(ranges, create_default_styles); + StyleManager::Style style{tc.style, es.projection}; + if (!style_manager.load_style(style)) + return false; // can't load font + + TextLinesModel text_lines; + const Selection &selection = wxGetApp().plater()->canvas3D()->get_selection(); + DataBasePtr base = create_emboss_data_base(tc.text, style_manager, text_lines, selection, text_volume.type(), job_cancel); + DataUpdate data{std::move(base), text_volume.id()}; + + RaycastManager raycast_manager; // Nothing is cached now, so It need to create raycasters + return start_update_volume(std::move(data), text_volume, selection, raycast_manager); +} namespace{ ModelVolumePtrs prepare_volumes_to_slice(const ModelVolume &mv) { @@ -450,7 +397,7 @@ ModelVolumePtrs prepare_volumes_to_slice(const ModelVolume &mv) } return result; } -} +} // namespace bool GLGizmoEmboss::do_mirror(size_t axis) { @@ -464,30 +411,46 @@ bool GLGizmoEmboss::do_mirror(size_t axis) if (m_parent.get_gizmos_manager().get_current_type() != GLGizmosManager::Emboss) return false; - const TextConfiguration &tc= *m_volume->text_configuration; - if(tc.style.prop.per_glyph){ - // init textlines before mirroring on mirrored text volume transformation + const std::optional &tc = m_volume->text_configuration; + assert(tc.has_value()); + bool is_per_glyph = tc.has_value()? tc->style.prop.per_glyph : false; + + const std::optional &es = m_volume->emboss_shape; + assert(es.has_value()); + bool use_surface = es.has_value()? es->projection.use_surface : false; + if (!use_surface && !is_per_glyph) { + // do normal mirror with fix matrix + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_mirror_fnc = [&selection, &axis]() { selection.mirror((Axis) axis, get_drag_transformation_type(selection)); }; + selection_transform(selection, selection_mirror_fnc); + + m_parent.do_mirror(L("Set Mirror")); + wxGetApp().obj_manipul()->UpdateAndShow(true); + return true; + } + + Vec3d scale = Vec3d::Ones(); + scale[axis] = -1.; Transform3d tr = m_volume->get_matrix(); - const std::optional &fix_tr = tc.fix_3mf_tr; + if (es.has_value()) { + const std::optional &fix_tr = es->fix_3mf_tr; if (fix_tr.has_value()) tr = tr * (fix_tr->inverse()); + } // mirror - Vec3d scale = Vec3d::Ones(); - scale[axis] = -1.; tr = tr * Eigen::Scaling(scale); - // collect volumes in object + if (is_per_glyph) { + // init textlines before mirroring on mirrored text volume transformation ModelVolumePtrs volumes = prepare_volumes_to_slice(*m_volume); m_text_lines.init(tr, volumes, m_style_manager, m_text_lines.get_lines().size()); } - // mirror - Transform3d tr = m_volume->get_matrix(); - Vec3d scale = Vec3d::Ones(); - scale[axis] = -1.; - tr = tr * Eigen::Scaling(scale); m_volume->set_transformation(tr); + // setting to volume is not visible for user(not GLVolume) // NOTE: Staff around volume transformation change is done in job finish return process(); } @@ -528,15 +491,6 @@ bool GLGizmoEmboss::init_create(ModelVolumeType volume_type) return true; } -namespace { -TransformationType get_transformation_type(const Selection &selection) -{ - assert(selection.is_single_full_object() || selection.is_single_volume()); - return selection.is_single_volume() ? - TransformationType::Local_Relative_Joint : - TransformationType::Instance_Relative_Joint; // object -} -} // namespace bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) { @@ -546,34 +500,13 @@ bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) if (!m_dragging) return used; if (mouse_event.Dragging()) { - if (!m_rotate_start_angle.has_value()) { - // when m_rotate_start_angle is not set mean it is not Dragging - // when angle_opt is not set mean angle is Zero - const std::optional &angle_opt = m_style_manager.get_font_prop().angle; - m_rotate_start_angle = angle_opt.has_value() ? *angle_opt : 0.f; - } - - double angle = m_rotate_gizmo.get_angle(); - angle -= PI / 2; // Grabber is upward - - // temporary rotation - Selection& selection = m_parent.get_selection(); - selection.rotate(Vec3d(0., 0., angle), get_transformation_type(selection)); - - angle += *m_rotate_start_angle; - // move to range <-M_PI, M_PI> - priv::to_range_pi_pi(angle); - - // set into activ style + // check that style is activ assert(m_style_manager.is_active_font()); - if (m_style_manager.is_active_font()) { - std::optional angle_opt; - if (!is_approx(angle, 0.)) - angle_opt = angle; - m_style_manager.get_font_prop().angle = angle_opt; - } + if (!m_style_manager.is_active_font()) + return used; - volume_transformation_changing(); + std::optional &angle_opt = m_style_manager.get_style().angle; + dragging_rotate_gizmo(m_rotate_gizmo.get_angle(), angle_opt, m_rotate_start_angle, m_parent.get_selection()); } return used; } @@ -584,11 +517,9 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) if (m_volume == nullptr) return false; - std::optional up_limit; - if (m_keep_up) up_limit = priv::up_limit; const Camera &camera = wxGetApp().plater()->get_camera(); bool was_dragging = m_surface_drag.has_value(); - bool res = on_mouse_surface_drag(mouse_event, camera, m_surface_drag, m_parent, m_raycast_manager, up_limit); + bool res = on_mouse_surface_drag(mouse_event, camera, m_surface_drag, m_parent, m_raycast_manager, UP_LIMIT); bool is_dragging = m_surface_drag.has_value(); // End with surface dragging? @@ -609,13 +540,14 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) // Recalculate angle for GUI if (!m_keep_up) { - const GLVolume *gl_volume = get_selected_gl_volume(m_parent.get_selection()); + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = get_selected_gl_volume(selection); assert(gl_volume != nullptr); assert(m_style_manager.is_active_font()); if (gl_volume == nullptr || !m_style_manager.is_active_font()) return res; - m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); + m_style_manager.get_style().angle = calc_angle(selection); } volume_transformation_changing(); @@ -732,23 +664,48 @@ void GLGizmoEmboss::volume_transformation_changing() void GLGizmoEmboss::volume_transformation_changed() { - if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { + if (m_volume == nullptr || + !m_volume->text_configuration.has_value() || + !m_volume->emboss_shape.has_value() || + !m_style_manager.is_active_font()) { assert(false); return; } - const FontProp &prop = m_volume->text_configuration->style.prop; - if (prop.per_glyph) + if (!m_keep_up) { + // Re-Calculate current angle of up vector + m_style_manager.get_style().angle = calc_angle(m_parent.get_selection()); + } else { + // angle should be the same + assert(is_approx(m_style_manager.get_style().angle, calc_angle(m_parent.get_selection()))); + } + + const TextConfiguration &tc = *m_volume->text_configuration; + const EmbossShape &es = *m_volume->emboss_shape; + + bool per_glyph = tc.style.prop.per_glyph; + if (per_glyph) init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, m_text_lines.get_lines().size()); + bool use_surface = es.projection.use_surface; // Update surface by new position - if (prop.use_surface || prop.per_glyph) + if (use_surface || per_glyph) process(); + else { + // inform slicing process that model changed + // SLA supports, processing + // ensure on bed + wxGetApp().plater()->changed_object(*m_volume->get_object()); + } // Show correct value of height & depth inside of inputs calculate_scale(); } +bool GLGizmoEmboss::wants_enter_leave_snapshots() const { return true; } +std::string GLGizmoEmboss::get_gizmo_entering_text() const { return _u8L("Enter emboss gizmo"); } +std::string GLGizmoEmboss::get_gizmo_leaving_text() const { return _u8L("Leave emboss gizmo"); } +std::string GLGizmoEmboss::get_action_snapshot_name() const { return _u8L("Embossing actions"); } bool GLGizmoEmboss::on_init() { m_rotate_gizmo.init(); @@ -770,10 +727,10 @@ std::string GLGizmoEmboss::on_get_name() const { return _u8L("Emboss"); } void GLGizmoEmboss::on_render() { // no volume selected - if (m_volume == nullptr || - get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr) - return; const Selection &selection = m_parent.get_selection(); + if (m_volume == nullptr || + get_model_volume(m_volume_id, selection.get_model()->objects) == nullptr) + return; if (selection.is_empty()) return; // prevent get local coordinate system on multi volumes @@ -785,7 +742,7 @@ void GLGizmoEmboss::on_render() { if (m_text_lines.is_init()) { const Transform3d& tr = gl_volume_ptr->world_matrix(); - const auto &fix = m_volume->text_configuration->fix_3mf_tr; + const auto &fix = m_volume->emboss_shape->fix_3mf_tr; if (fix.has_value()) m_text_lines.render(tr * fix->inverse()); else @@ -887,15 +844,16 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) // Configuration creation double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); float main_toolbar_height = m_parent.get_main_toolbar_height(); - if (!m_gui_cfg.has_value() || // Exist configuration - first run + if (m_gui_cfg == nullptr || // Exist configuration - first run m_gui_cfg->screen_scale != screen_scale || // change of DPI m_gui_cfg->main_toolbar_height != main_toolbar_height // change size of view port ) { // Create cache for gui offsets - GuiCfg cfg = create_gui_configuration(); + ::GuiCfg cfg = create_gui_configuration(); cfg.screen_scale = screen_scale; cfg.main_toolbar_height = main_toolbar_height; - m_gui_cfg.emplace(std::move(cfg)); + GuiCfg gui_cfg{std::move(cfg)}; + m_gui_cfg = std::make_unique(std::move(gui_cfg)); // set position near toolbar m_set_window_offset = ImVec2(-1.f, -1.f); @@ -949,7 +907,7 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) bool is_opened = true; ImGuiWindowFlags flag = ImGuiWindowFlags_NoCollapse; - if (ImGui::Begin(on_get_name().c_str(), &is_opened, flag)) { + if (ImGui::Begin(get_name().c_str(), &is_opened, flag)) { // Need to pop var before draw window ImGui::PopStyleVar(); // WindowMinSize draw_window(); @@ -969,20 +927,6 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) close(); } -namespace priv { -/// -/// Move window for edit emboss text near to embossed object -/// NOTE: embossed object must be selected -/// -static ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size); - -/// -/// Change position of emboss window -/// -/// -/// When True Only move to be full visible otherwise reset position -static void change_window_position(std::optional &output_window_offset, bool try_to_fix); -} // namespace priv void GLGizmoEmboss::on_set_state() { @@ -1010,17 +954,12 @@ void GLGizmoEmboss::on_set_state() // change position of just opened emboss window if (m_allow_open_near_volume) { - m_set_window_offset = priv::calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); + m_set_window_offset = calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); } else { - if (m_gui_cfg.has_value()) - priv::change_window_position(m_set_window_offset, false); - else - m_set_window_offset = ImVec2(-1, -1); + m_set_window_offset = (m_gui_cfg != nullptr) ? + ImGuiWrapper::change_window_position(on_get_name().c_str(), false) : ImVec2(-1, -1); } - // when open by hyperlink it needs to show up - // or after key 'T' windows doesn't appear - m_parent.set_as_dirty(); } } @@ -1035,7 +974,6 @@ void GLGizmoEmboss::on_stop_dragging() { m_rotate_gizmo.stop_dragging(); - // TODO: when start second rotatiton previous rotation rotate draggers // This is fast fix for second try to rotate // When fixing, move grabber above text (not on side) m_rotate_gizmo.set_angle(PI/2); @@ -1043,12 +981,6 @@ void GLGizmoEmboss::on_stop_dragging() // apply rotation m_parent.do_rotate(L("Text-Rotate")); - // Re-Calculate current angle of up vector - const GLVolume *gl_volume = get_selected_gl_volume(m_parent.get_selection()); - assert(m_style_manager.is_active_font()); - assert(gl_volume != nullptr); - if (m_style_manager.is_active_font() && gl_volume != nullptr) - m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); m_rotate_start_angle.reset(); @@ -1056,106 +988,6 @@ void GLGizmoEmboss::on_stop_dragging() } void GLGizmoEmboss::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } -GLGizmoEmboss::GuiCfg GLGizmoEmboss::create_gui_configuration() -{ - GuiCfg cfg; // initialize by default values; - - float line_height = ImGui::GetTextLineHeight(); - float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); - float space = line_height_with_spacing - line_height; - const ImGuiStyle &style = ImGui::GetStyle(); - - cfg.max_style_name_width = ImGui::CalcTextSize("Maximal font name, extended").x; - - cfg.icon_width = static_cast(std::ceil(line_height)); - // make size pair number - if (cfg.icon_width % 2 != 0) ++cfg.icon_width; - - cfg.delete_pos_x = cfg.max_style_name_width + space; - int count_line_of_text = 3; - cfg.text_size = ImVec2(-FLT_MIN, line_height_with_spacing * count_line_of_text); - ImVec2 letter_m_size = ImGui::CalcTextSize("M"); - int count_letter_M_in_input = 12; - cfg.input_width = letter_m_size.x * count_letter_M_in_input; - GuiCfg::Translations &tr = cfg.translations; - - tr.font = _u8L("Font"); - tr.height = _u8L("Height"); - tr.depth = _u8L("Depth"); - - float max_text_width = std::max({ - ImGui::CalcTextSize(tr.font.c_str()).x, - ImGui::CalcTextSize(tr.height.c_str()).x, - ImGui::CalcTextSize(tr.depth.c_str()).x}); - cfg.indent = static_cast(cfg.icon_width); - cfg.input_offset = style.WindowPadding.x + cfg.indent + max_text_width + space; - - tr.use_surface = _u8L("Use surface"); - tr.per_glyph = _u8L("Per glyph orientation"); - tr.alignment = _u8L("Alignment"); - tr.char_gap = _u8L("Char gap"); - tr.line_gap = _u8L("Line gap"); - tr.boldness = _u8L("Boldness"); - tr.skew_ration = _u8L("Skew ratio"); - tr.from_surface = _u8L("From surface"); - tr.rotation = _u8L("Rotation"); - tr.collection = _u8L("Collection"); - - float max_advanced_text_width = std::max({ - ImGui::CalcTextSize(tr.use_surface.c_str()).x, - ImGui::CalcTextSize(tr.per_glyph.c_str()).x, - ImGui::CalcTextSize(tr.alignment.c_str()).x, - ImGui::CalcTextSize(tr.char_gap.c_str()).x, - ImGui::CalcTextSize(tr.line_gap.c_str()).x, - ImGui::CalcTextSize(tr.boldness.c_str()).x, - ImGui::CalcTextSize(tr.skew_ration.c_str()).x, - ImGui::CalcTextSize(tr.from_surface.c_str()).x, - ImGui::CalcTextSize(tr.rotation.c_str()).x + cfg.icon_width + 2*space, - ImGui::CalcTextSize(tr.collection.c_str()).x }); - cfg.advanced_input_offset = max_advanced_text_width - + 3 * space + cfg.indent; - cfg.lock_offset = cfg.advanced_input_offset - (cfg.icon_width + space); - // calculate window size - float window_title = line_height + 2*style.FramePadding.y + 2 * style.WindowTitleAlign.y; - float input_height = line_height_with_spacing + 2*style.FramePadding.y; - float tree_header = line_height_with_spacing; - float separator_height = 1 + style.FramePadding.y; - - // "Text is to object" + radio buttons - cfg.height_of_volume_type_selector = separator_height + line_height_with_spacing + input_height; - - float window_height = - window_title + // window title - cfg.text_size.y + // text field - input_height * 4 + // font name + height + depth + style selector - tree_header + // advance tree - separator_height + // presets separator line - line_height_with_spacing + // "Presets" - 2 * style.WindowPadding.y; - float window_width = cfg.input_offset + cfg.input_width + 2*style.WindowPadding.x - + 2 * (cfg.icon_width + space); - cfg.minimal_window_size = ImVec2(window_width, window_height); - - // 8 = useSurface, per glyph, charGap, lineGap, bold, italic, surfDist, rotation, textFaceToCamera - // 4 = 1px for fix each edit image of drag float - float advance_height = input_height * 10 + 9; - cfg.minimal_window_size_with_advance = - ImVec2(cfg.minimal_window_size.x, - cfg.minimal_window_size.y + advance_height); - - cfg.minimal_window_size_with_collections = - ImVec2(cfg.minimal_window_size_with_advance.x, - cfg.minimal_window_size_with_advance.y + input_height); - - int max_style_image_width = cfg.max_style_name_width /2 - - 2 * style.FramePadding.x; - int max_style_image_height = 1.5 * input_height; - cfg.max_style_image_size = Vec2i(max_style_image_width, max_style_image_height); - cfg.face_name_size.y() = line_height_with_spacing; - cfg.face_name_size.x() = cfg.input_width; - cfg.face_name_texture_offset_x = cfg.input_width + space; - return cfg; -} EmbossStyles GLGizmoEmboss::create_default_styles() { @@ -1227,6 +1059,50 @@ EmbossStyles GLGizmoEmboss::create_default_styles() } namespace{ +/// +/// Check installed fonts whether optional face name exist in installed fonts +/// +/// Name from style - style.prop.face_name +/// All installed good and bad fonts - not const must be possible to initialize it +/// When it could be installed it contain value(potentionaly empty string) +std::optional get_installed_face_name(const std::optional &face_name_opt, ::Facenames& face_names) +{ + // Could exist OS without getter on face_name, + // but it is able to restore font from descriptor + // Soo default value must be TRUE + if (!face_name_opt.has_value()) + return wxString(); + + wxString face_name(face_name_opt->c_str()); + + // search in enumerated fonts + // refresh list of installed font in the OS. + init_face_names(face_names); + face_names.is_init = false; + + auto cmp = [](const FaceName &fn, const wxString &wx_name) { return fn.wx_name < wx_name; }; + const std::vector &faces = face_names.faces; + // is font installed? + if (auto it = std::lower_bound(faces.begin(), faces.end(), face_name, cmp); + it != faces.end() && it->wx_name == face_name) + return face_name; + + const std::vector &bad = face_names.bad; + auto it_bad = std::lower_bound(bad.begin(), bad.end(), face_name); + if (it_bad == bad.end() || *it_bad != face_name) { + // check if wx allowed to set it up - another encoding of name + wxFontEnumerator::InvalidateCache(); + wxFont wx_font_; // temporary structure + if (wx_font_.SetFaceName(face_name) && WxFontUtils::create_font_file(wx_font_) != nullptr // can load TTF file? + ) { + return wxString(); + // QUESTION: add this name to allowed faces? + // Could create twin of font face name + // When not add it will be hard to select it again when change font + } + } + return {}; // not installed +} void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* const*/ StyleManager &style_manager, unsigned count_lines) { const GLVolume *gl_volume_ptr = selection.get_first_volume(); @@ -1241,6 +1117,10 @@ void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* if (mv.is_the_only_one_part()) return; + const std::optional &es_opt = mv.emboss_shape; + if (!es_opt.has_value()) + return; + const EmbossShape &es = *es_opt; const std::optional &tc_opt = mv.text_configuration; if (!tc_opt.has_value()) return; @@ -1258,25 +1138,11 @@ void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* // For interactivity during drag over surface it must be from gl_volume not volume. Transform3d mv_trafo = gl_volume.get_volume_transformation().get_matrix(); - if (tc.fix_3mf_tr.has_value()) - mv_trafo = mv_trafo * (tc.fix_3mf_tr->inverse()); + if (es.fix_3mf_tr.has_value()) + mv_trafo = mv_trafo * (es.fix_3mf_tr->inverse()); text_lines.init(mv_trafo, volumes, style_manager, count_lines); } -void init_new_text_line(TextLinesModel &text_lines, const Transform3d& new_text_tr, const ModelObject& mo, /* const*/ StyleManager &style_manager) -{ - // prepare volumes to slice - ModelVolumePtrs volumes; - volumes.reserve(mo.volumes.size()); - for (ModelVolume *volume : mo.volumes) { - // only part could be surface for volumes - if (!volume->is_model_part()) - continue; - volumes.push_back(volume); - } - unsigned count_lines = 1; - text_lines.init(new_text_tr, volumes, style_manager, count_lines); -} } @@ -1306,8 +1172,7 @@ void GLGizmoEmboss::set_volume_by_selection() remove_notification_not_valid_font(); // Do not use focused input value when switch volume(it must swith value) - if (m_volume != nullptr && - m_volume != volume) // when update volume it changed id BUT not pointer + if (m_volume != nullptr && m_volume != volume) // when update volume it changed id BUT not pointer ImGuiWrapper::left_inputs(); // Is selected volume text volume? @@ -1315,50 +1180,18 @@ void GLGizmoEmboss::set_volume_by_selection() if (!tc_opt.has_value()) return reset_volume(); + // Emboss shape must be setted + assert(volume->emboss_shape.has_value()); + if (!volume->emboss_shape.has_value()) + return; const TextConfiguration &tc = *tc_opt; const EmbossStyle &style = tc.style; - // Could exist OS without getter on face_name, - // but it is able to restore font from descriptor - // Soo default value must be TRUE - bool is_font_installed = true; - wxString face_name; - std::optional face_name_opt = style.prop.face_name; - if (face_name_opt.has_value()) { - face_name = wxString(face_name_opt->c_str()); - - // search in enumerated fonts - // refresh list of installed font in the OS. - init_face_names(m_face_names); - m_face_names.is_init = false; - - auto cmp = [](const FaceName &fn, const wxString& face_name)->bool { return fn.wx_name < face_name; }; - const std::vector &faces = m_face_names.faces; - auto it = std::lower_bound(faces.begin(), faces.end(), face_name, cmp); - is_font_installed = it != faces.end() && it->wx_name == face_name; - - if (!is_font_installed) { - const std::vector &bad = m_face_names.bad; - auto it_bad = std::lower_bound(bad.begin(), bad.end(), face_name); - if (it_bad == bad.end() || *it_bad != face_name){ - // check if wx allowed to set it up - another encoding of name - wxFontEnumerator::InvalidateCache(); - wxFont wx_font_; // temporary structure - if (wx_font_.SetFaceName(face_name) && - WxFontUtils::create_font_file(wx_font_) != nullptr // can load TTF file? - ) { - is_font_installed = true; - // QUESTION: add this name to allowed faces? - // Could create twin of font face name - // When not add it will be hard to select it again when change font - } - } - } - } + std::optional installed_name = get_installed_face_name(style.prop.face_name, *m_face_names); wxFont wx_font; // load wxFont from same OS when font name is installed - if (style.type == WxFontUtils::get_actual_type() && is_font_installed) + if (style.type == WxFontUtils::get_current_type() && installed_name.has_value()) wx_font = WxFontUtils::load_wxFont(style.path); // Flag that is selected same font @@ -1368,8 +1201,8 @@ void GLGizmoEmboss::set_volume_by_selection() is_exact_font = false; // Try create similar wx font by FontFamily wx_font = WxFontUtils::create_wxFont(style); - if (is_font_installed) - is_exact_font = wx_font.SetFaceName(face_name); + if (installed_name.has_value() && !installed_name->empty()) + is_exact_font = wx_font.SetFaceName(*installed_name); // Have to use some wxFont if (!wx_font.IsOk()) @@ -1379,24 +1212,27 @@ void GLGizmoEmboss::set_volume_by_selection() // Load style to style manager const auto& styles = m_style_manager.get_styles(); - auto has_same_name = [&style](const StyleManager::Item &style_item) -> bool { - const EmbossStyle &es = style_item.style; - return es.name == style.name; - }; - auto it = std::find_if(styles.begin(), styles.end(), has_same_name); - if (it == styles.end()) { + auto has_same_name = [&name = style.name](const StyleManager::Style &style_item) { return style_item.name == name; }; + + StyleManager::Style style_{style}; // copy + style_.projection = volume->emboss_shape->projection; + style_.angle = calc_angle(selection); + style_.distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + if (auto it = std::find_if(styles.begin(), styles.end(), has_same_name); + it == styles.end()) { // style was not found - m_style_manager.load_style(style, wx_font); + m_style_manager.load_style(style_, wx_font); } else { // style name is in styles list size_t style_index = it - styles.begin(); if (!m_style_manager.load_style(style_index)) { // can`t load stored style m_style_manager.erase(style_index); - m_style_manager.load_style(style, wx_font); + m_style_manager.load_style(style_, wx_font); } else { // stored style is loaded, now set modification of style - m_style_manager.get_style() = style; + m_style_manager.get_style() = style_; m_style_manager.set_wx_font(wx_font); } } @@ -1428,7 +1264,7 @@ void GLGizmoEmboss::set_volume_by_selection() // Calculate current angle of up vector assert(m_style_manager.is_active_font()); if (m_style_manager.is_active_font()) - m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); + m_style_manager.get_style().angle = calc_angle(selection); // calculate scale for height and depth inside of scaled object instance calculate_scale(); @@ -1471,32 +1307,9 @@ void GLGizmoEmboss::calculate_scale() { m_style_manager.clear_imgui_font(); } -#ifdef EXECUTE_PROCESS_ON_MAIN_THREAD -namespace priv { -// Run Job on main thread (blocking) - ONLY DEBUG -static inline void execute_job(std::shared_ptr j) -{ - struct MyCtl : public Job::Ctl - { - void update_status(int st, const std::string &msg = "") override{}; - bool was_canceled() const override { return false; } - std::future call_on_main_thread(std::function fn) override - { - return std::future{}; - } - } ctl; - j->process(ctl); - wxGetApp().plater()->CallAfter([j]() { - std::exception_ptr e_ptr = nullptr; - j->finalize(false, e_ptr); - }); -} -} // namespace priv -#endif - -namespace priv { -static bool is_text_empty(const std::string &text) { return text.empty() || text.find_first_not_of(" \n\t\r") == std::string::npos; } -} // namespace priv +namespace { +bool is_text_empty(std::string_view text) { return text.empty() || text.find_first_not_of(" \n\t\r") == std::string::npos; } +} // namespace bool GLGizmoEmboss::process() { @@ -1505,61 +1318,21 @@ bool GLGizmoEmboss::process() if (m_volume == nullptr) return false; // without text there is nothing to emboss - if (priv::is_text_empty(m_text)) return false; + if (is_text_empty(m_text)) return false; // exist loaded font file? if (!m_style_manager.is_active_font()) return false; - DataUpdate data{priv::create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), m_volume->type(), m_job_cancel), - m_volume->id()}; - std::unique_ptr job = nullptr; + const Selection& selection = m_parent.get_selection(); + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, selection, m_volume->type(), m_job_cancel); + DataUpdate data{std::move(base), m_volume->id()}; - // check cutting from source mesh - bool &use_surface = data.text_configuration.style.prop.use_surface; - bool is_object = m_volume->get_object()->volumes.size() == 1; - if (use_surface && is_object) - use_surface = false; - - assert(!data.text_configuration.style.prop.per_glyph || - get_count_lines(m_text) == m_text_lines.get_lines().size()); + // check valid count of text lines + assert(data.base->text_lines.empty() || data.base->text_lines.size() == get_count_lines(m_text)); - if (use_surface) { - // Model to cut surface from. - SurfaceVolumeData::ModelSources sources = create_volume_sources(m_volume); - if (sources.empty()) + if (!start_update_volume(std::move(data), *m_volume, selection, m_raycast_manager)) return false; - Transform3d text_tr = m_volume->get_matrix(); - auto& fix_3mf = m_volume->text_configuration->fix_3mf_tr; - if (fix_3mf.has_value()) - text_tr = text_tr * fix_3mf->inverse(); - - // when it is new applying of use surface than move origin onto surfaca - if (!m_volume->text_configuration->style.prop.use_surface) { - auto offset = calc_surface_offset(m_parent.get_selection(), m_raycast_manager); - if (offset.has_value()) - text_tr *= Eigen::Translation(*offset); - } - - // check that there is not unexpected volume type - bool is_valid_type = check(m_volume->type()); - assert(is_valid_type); - if (!is_valid_type) - return false; - - UpdateSurfaceVolumeData surface_data{std::move(data), {text_tr, std::move(sources)}}; - job = std::make_unique(std::move(surface_data)); - } else { - job = std::make_unique(std::move(data)); - } - -#ifndef EXECUTE_PROCESS_ON_MAIN_THREAD - auto &worker = wxGetApp().plater()->get_ui_job_worker(); - queue_job(worker, std::move(job)); -#else - // Run Job on main thread (blocking) - ONLY DEBUG - priv::execute_job(std::move(job)); -#endif // EXECUTE_PROCESS_ON_MAIN_THREAD // notification is removed befor object is changed by job remove_notification_not_valid_font(); @@ -1571,7 +1344,7 @@ void GLGizmoEmboss::close() // remove volume when text is empty if (m_volume != nullptr && m_volume->text_configuration.has_value() && - priv::is_text_empty(m_text)) { + is_text_empty(m_text)) { Plater &p = *wxGetApp().plater(); // is the text object? if (m_volume->is_the_only_one_part()) { @@ -1593,7 +1366,6 @@ void GLGizmoEmboss::draw_window() { #ifdef ALLOW_DEBUG_MODE if (ImGui::Button("re-process")) process(); - if (ImGui::Button("add svg")) choose_svg_file(); #endif // ALLOW_DEBUG_MODE // Setter of indent must be befor disable !!! @@ -1635,7 +1407,7 @@ void GLGizmoEmboss::draw_window() draw_style_list(); // Do not select volume type, when it is text object - if (m_volume->get_object()->volumes.size() != 1) { + if (!m_volume->is_the_only_one_part()) { ImGui::Separator(); draw_model_type(); } @@ -1679,7 +1451,7 @@ void GLGizmoEmboss::draw_window() ImGui::SameLine(); if (ImGui::Checkbox("##ALLOW_OPEN_NEAR_VOLUME", &m_allow_open_near_volume)) { if (m_allow_open_near_volume) - m_set_window_offset = priv::calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); + m_set_window_offset = calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", ((m_allow_open_near_volume) ? "Fix settings position": @@ -1729,8 +1501,10 @@ void GLGizmoEmboss::draw_text_input() warning_tool_tip += t; }; - if (priv::is_text_empty(m_text)) append_warning(_u8L("Embossed text cannot contain only white spaces.")); - if (m_text_contain_unknown_glyph) append_warning(_u8L("Text contains character glyph (represented by '?') unknown by font.")); + if (is_text_empty(m_text)) + append_warning(_u8L("Embossed text cannot contain only white spaces.")); + if (m_text_contain_unknown_glyph) + append_warning(_u8L("Text contains character glyph (represented by '?') unknown by font.")); const FontProp &prop = m_style_manager.get_font_prop(); if (prop.skew.has_value()) append_warning(_u8L("Text input doesn't show font skew.")); @@ -1771,14 +1545,13 @@ void GLGizmoEmboss::draw_text_input() // warning tooltip has to be with default font if (!warning_tool_tip.empty()) { // Multiline input has hidden window for scrolling - ImGuiWindow *input = ImGui::GetCurrentWindow()->DC.ChildWindows.front(); + const ImGuiWindow *input = ImGui::GetCurrentWindow()->DC.ChildWindows.front(); const ImGuiStyle &style = ImGui::GetStyle(); float scrollbar_width = (input->ScrollbarY) ? style.ScrollbarSize : 0.f; float scrollbar_height = (input->ScrollbarX) ? style.ScrollbarSize : 0.f; - bool hovered = ImGui::IsItemHovered(); - if (hovered) + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", warning_tool_tip.c_str()); ImVec2 cursor = ImGui::GetCursorPos(); @@ -1797,216 +1570,16 @@ void GLGizmoEmboss::draw_text_input() // IMPROVE: only extend not clear // Extend font ranges if (!range_text.empty() && - !m_imgui->contain_all_glyphs(imgui_font, range_text) ) + !ImGuiWrapper::contain_all_glyphs(imgui_font, range_text) ) m_style_manager.clear_imgui_font(); } -#include -#include "wx/hashmap.h" -std::size_t hash_value(wxString const &s) -{ - boost::hash hasher; - return hasher(s.ToStdString()); -} - -static std::string concat(std::vector data) { - std::stringstream ss; - for (const auto &d : data) - ss << d.c_str() << ", "; - return ss.str(); -} - -#include -static boost::filesystem::path get_fontlist_cache_path() -{ - return boost::filesystem::path(data_dir()) / "cache" / "fonts.cereal"; -} - -// cache font list by cereal -#include -#include -#include -#include - -// increase number when change struct FacenamesSerializer -#define FACENAMES_VERSION 1 -struct FacenamesSerializer -{ - // hash number for unsorted vector of installed font into system - size_t hash = 0; - // assumption that is loadable - std::vector good; - // Can't load for some reason - std::vector bad; -}; - -template void save(Archive &archive, wxString const &d) -{ std::string s(d.ToUTF8().data()); archive(s);} -template void load(Archive &archive, wxString &d) -{ std::string s; archive(s); d = s;} - -template void serialize(Archive &ar, FacenamesSerializer &t, const std::uint32_t version) -{ - // When performing a load, the version associated with the class - // is whatever it was when that data was originally serialized - // When we save, we'll use the version that is defined in the macro - if (version != FACENAMES_VERSION) return; - ar(t.hash, t.good, t.bad); -} -CEREAL_CLASS_VERSION(FacenamesSerializer, FACENAMES_VERSION); // register class version - -#include -bool GLGizmoEmboss::store(const Facenames &facenames) { - std::string cache_path = get_fontlist_cache_path().string(); - boost::nowide::ofstream file(cache_path, std::ios::binary); - cereal::BinaryOutputArchive archive(file); - std::vector good; - good.reserve(facenames.faces.size()); - for (const FaceName &face : facenames.faces) good.push_back(face.wx_name); - FacenamesSerializer data = {facenames.hash, good, facenames.bad}; - - assert(std::is_sorted(data.bad.begin(), data.bad.end())); - assert(std::is_sorted(data.good.begin(), data.good.end())); - - try { - archive(data); - } catch (const std::exception &ex) { - BOOST_LOG_TRIVIAL(error) << "Failed to write fontlist cache - " << cache_path << ex.what(); - return false; - } - return true; -} - -bool GLGizmoEmboss::load(Facenames &facenames) { - boost::filesystem::path path = get_fontlist_cache_path(); - std::string path_str = path.string(); - if (!boost::filesystem::exists(path)) { - BOOST_LOG_TRIVIAL(warning) << "Fontlist cache - '" << path_str << "' does not exists."; - return false; - } - boost::nowide::ifstream file(path_str, std::ios::binary); - cereal::BinaryInputArchive archive(file); - - FacenamesSerializer data; - try { - archive(data); - } catch (const std::exception &ex) { - BOOST_LOG_TRIVIAL(error) << "Failed to load fontlist cache - '" << path_str << "'. Exception: " << ex.what(); - return false; - } - - assert(std::is_sorted(data.bad.begin(), data.bad.end())); - assert(std::is_sorted(data.good.begin(), data.good.end())); - - facenames.hash = data.hash; - facenames.faces.reserve(data.good.size()); - for (const wxString &face : data.good) - facenames.faces.push_back({face}); - facenames.bad = data.bad; - return true; -} - -void GLGizmoEmboss::init_truncated_names(Facenames &face_names, float max_width) -{ - for (FaceName &face : face_names.faces) { - std::string name_str(face.wx_name.ToUTF8().data()); - face.name_truncated = ImGuiWrapper::trunc(name_str, max_width); - } - face_names.has_truncated_names = true; -} - -void GLGizmoEmboss::init_face_names(Facenames &face_names) -{ - Timer t("enumerate_fonts"); - if (face_names.is_init) return; - face_names.is_init = true; - - // to reload fonts from system, when install new one - wxFontEnumerator::InvalidateCache(); - - // try load cache - // Only not OS enumerated face has hash value 0 - if (face_names.hash == 0) { - load(face_names); - face_names.has_truncated_names = false; - } - - using namespace std::chrono; - steady_clock::time_point enumerate_start = steady_clock::now(); - ScopeGuard sg([&enumerate_start, &face_names = face_names]() { - steady_clock::time_point enumerate_end = steady_clock::now(); - long long enumerate_duration = duration_cast(enumerate_end - enumerate_start).count(); - BOOST_LOG_TRIVIAL(info) << "OS enumerate " << face_names.faces.size() << " fonts " - << "(+ " << face_names.bad.size() << " can't load " - << "= " << face_names.faces.size() + face_names.bad.size() << " fonts) " - << "in " << enumerate_duration << " ms\n" << concat(face_names.bad); - }); - wxArrayString facenames = wxFontEnumerator::GetFacenames(face_names.encoding); - size_t hash = boost::hash_range(facenames.begin(), facenames.end()); - // Zero value is used as uninitialized hash - if (hash == 0) hash = 1; - // check if it is same as last time - if (face_names.hash == hash) { - // no new installed font - BOOST_LOG_TRIVIAL(info) << "Same FontNames hash, cache is used. " - << "For clear cache delete file: " << get_fontlist_cache_path().string(); - return; - } - - BOOST_LOG_TRIVIAL(info) << ((face_names.hash == 0) ? - "FontName list is generate from scratch." : - "Hash are different. Only previous bad fonts are used and set again as bad"); - face_names.hash = hash; - - // validation lambda - auto is_valid_font = [encoding = face_names.encoding, bad = face_names.bad /*copy*/](const wxString &name) { - if (name.empty()) return false; - - // vertical font start with @, we will filter it out - // Not sure if it is only in Windows so filtering is on all platforms - if (name[0] == '@') return false; - - // previously detected bad font - auto it = std::lower_bound(bad.begin(), bad.end(), name); - if (it != bad.end() && *it == name) return false; - - wxFont wx_font(wxFontInfo().FaceName(name).Encoding(encoding)); - //* - // Faster chech if wx_font is loadable but not 100% - // names could contain not loadable font - if (!WxFontUtils::can_load(wx_font)) return false; - - /*/ - // Slow copy of font files to try load font - // After this all files are loadable - auto font_file = WxFontUtils::create_font_file(wx_font); - if (font_file == nullptr) - return false; // can't create font file - // */ - return true; - }; - - face_names.faces.clear(); - face_names.bad.clear(); - face_names.faces.reserve(facenames.size()); - std::sort(facenames.begin(), facenames.end()); - for (const wxString &name : facenames) { - if (is_valid_font(name)) { - face_names.faces.push_back({name}); - }else{ - face_names.bad.push_back(name); - } - } - assert(std::is_sorted(face_names.bad.begin(), face_names.bad.end())); - face_names.has_truncated_names = false; - store(face_names); -} // create texture for visualization font face void GLGizmoEmboss::init_font_name_texture() { Timer t("init_font_name_texture"); // check if already exists - GLuint &id = m_face_names.texture_id; + GLuint &id = m_face_names->texture_id; if (id != 0) return; // create texture for font GLenum target = GL_TEXTURE_2D; @@ -2015,7 +1588,7 @@ void GLGizmoEmboss::init_font_name_texture() { glsafe(::glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); glsafe(::glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); const Vec2i &size = m_gui_cfg->face_name_size; - GLint w = size.x(), h = m_face_names.count_cached_textures * size.y(); + GLint w = size.x(), h = m_face_names->count_cached_textures * size.y(); std::vector data(4*w * h, {0}); const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; const GLint level = 0, internal_format = GL_RGBA, border = 0; @@ -2026,107 +1599,20 @@ void GLGizmoEmboss::init_font_name_texture() { glsafe(::glBindTexture(target, no_texture_id)); // clear info about creation of texture - no one is initialized yet - for (FaceName &face : m_face_names.faces) { + for (FaceName &face : m_face_names->faces) { face.cancel = nullptr; face.is_created = nullptr; } // Prepare filtration cache - m_face_names.hide = std::vector(m_face_names.faces.size(), {false}); -} - -void GLGizmoEmboss::draw_font_preview(FaceName& face, bool is_visible) -{ - // Limit for opened font files at one moment - unsigned int &count_opened_fonts = m_face_names.count_opened_font_files; - // Size of texture - ImVec2 size(m_gui_cfg->face_name_size.x(), m_gui_cfg->face_name_size.y()); - float count_cached_textures_f = static_cast(m_face_names.count_cached_textures); - std::string state_text; - // uv0 and uv1 set to pixel 0,0 in texture - ImVec2 uv0(0.f, 0.f), uv1(1.f / size.x, 1.f / size.y / count_cached_textures_f); - if (face.is_created != nullptr) { - // not created preview - if (*face.is_created) { - // Already created preview - size_t texture_index = face.texture_index; - uv0 = ImVec2(0.f, texture_index / count_cached_textures_f); - uv1 = ImVec2(1.f, (texture_index + 1) / count_cached_textures_f); - } else { - // Not finished preview - if (is_visible) { - // when not canceled still loading - state_text = std::string(" ") + (face.cancel->load() ? - _u8L("No symbol") : - (dots.ToStdString() + _u8L("Loading"))); - } else { - // not finished and not visible cancel job - face.is_created = nullptr; - face.cancel->store(true); - } - } - } else if (is_visible && count_opened_fonts < m_gui_cfg->max_count_opened_font_files) { - ++count_opened_fonts; - face.cancel = std::make_shared(false); - face.is_created = std::make_shared(false); - - const unsigned char gray_level = 5; - // format type and level must match to texture data - const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; - const GLint level = 0; - // select next texture index - size_t texture_index = (m_face_names.texture_index + 1) % m_face_names.count_cached_textures; - - // set previous cach as deleted - for (FaceName &f : m_face_names.faces) - if (f.texture_index == texture_index) { - if (f.cancel != nullptr) f.cancel->store(true); - f.is_created = nullptr; - } - - m_face_names.texture_index = texture_index; - face.texture_index = texture_index; - - // render text to texture - FontImageData data{ - m_text, - face.wx_name, - m_face_names.encoding, - m_face_names.texture_id, - m_face_names.texture_index, - m_gui_cfg->face_name_size, - gray_level, - format, - type, - level, - &count_opened_fonts, - face.cancel, // copy - face.is_created // copy - }; - auto job = std::make_unique(std::move(data)); - auto &worker = wxGetApp().plater()->get_ui_job_worker(); - queue_job(worker, std::move(job)); - } else { - // cant start new thread at this moment so wait in queue - state_text = " " + dots.ToStdString() + " " + _u8L("Queue"); - } - - if (!state_text.empty()) { - ImGui::SameLine(m_gui_cfg->face_name_texture_offset_x); - m_imgui->text(state_text); - } - - ImGui::SameLine(m_gui_cfg->face_name_texture_offset_x); - ImTextureID tex_id = (void *) (intptr_t) m_face_names.texture_id; - ImGui::Image(tex_id, size, uv0, uv1); + m_face_names->hide = std::vector(m_face_names->faces.size(), {false}); } bool GLGizmoEmboss::select_facename(const wxString &facename) { if (!wxFontEnumerator::IsValidFacename(facename)) return false; // Select font - const wxFontEncoding &encoding = m_face_names.encoding; - wxFont wx_font(wxFontInfo().FaceName(facename).Encoding(encoding)); + wxFont wx_font(wxFontInfo().FaceName(facename).Encoding(Facenames::encoding)); if (!wx_font.IsOk()) return false; #ifdef USE_PIXEL_SIZE_IN_WX_FONT // wx font could change source file by size of font @@ -2223,19 +1709,19 @@ void GLGizmoEmboss::draw_font_list() // Fix clearance of search input, // Sometime happens that search text not disapear after font select - m_face_names.search.clear(); + m_face_names->search.clear(); } - if (ImGui::InputTextWithHint(input_id, selected, &m_face_names.search)) { + if (ImGui::InputTextWithHint(input_id, selected, &m_face_names->search)) { // update filtration result - m_face_names.hide = std::vector(m_face_names.faces.size(), {false}); + m_face_names->hide = std::vector(m_face_names->faces.size(), {false}); // search to uppercase - std::string search = m_face_names.search; // copy + std::string search = m_face_names->search; // copy std::transform(search.begin(), search.end(), search.begin(), ::toupper); - for (FaceName &face : m_face_names.faces) { - size_t index = &face - &m_face_names.faces.front(); + for (const FaceName &face : m_face_names->faces) { + size_t index = &face - &m_face_names->faces.front(); // font name to uppercase std::string name(face.wx_name.ToUTF8().data()); @@ -2243,7 +1729,7 @@ void GLGizmoEmboss::draw_font_list() // It should use C++ 20 feature https://en.cppreference.com/w/cpp/string/basic_string/starts_with bool start_with = boost::starts_with(name, search); - m_face_names.hide[index] = !start_with; + m_face_names->hide[index] = !start_with; } } if (!is_popup_open) @@ -2262,23 +1748,23 @@ void GLGizmoEmboss::draw_font_list() if (ImGui::BeginPopup(popup_id, popup_flags)) { bool set_selection_focus = false; - if (!m_face_names.is_init) { - init_face_names(m_face_names); + if (!m_face_names->is_init) { + init_face_names(*m_face_names); set_selection_focus = true; } - if (!m_face_names.has_truncated_names) - init_truncated_names(m_face_names, m_gui_cfg->input_width); + if (!m_face_names->has_truncated_names) + init_truncated_names(*m_face_names, m_gui_cfg->input_width); - if (m_face_names.texture_id == 0) + if (m_face_names->texture_id == 0) init_font_name_texture(); - for (FaceName &face : m_face_names.faces) { + for (FaceName &face : m_face_names->faces) { const wxString &wx_face_name = face.wx_name; - size_t index = &face - &m_face_names.faces.front(); + size_t index = &face - &m_face_names->faces.front(); // Filter for face names - if (m_face_names.hide[index]) + if (m_face_names->hide[index]) continue; ImGui::PushID(index); @@ -2299,7 +1785,7 @@ void GLGizmoEmboss::draw_font_list() // on first draw set focus on selected font if (set_selection_focus && is_selected) ImGui::SetScrollHereY(); - draw_font_preview(face, ImGui::IsItemVisible()); + ::draw_font_preview(face, m_text, *m_face_names, *m_gui_cfg, ImGui::IsItemVisible()); } if (!ImGui::IsWindowFocused() || @@ -2308,33 +1794,33 @@ void GLGizmoEmboss::draw_font_list() ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); - } else if (m_face_names.is_init) { + } else if (m_face_names->is_init) { // Just one after close combo box // free texture and set id to zero - m_face_names.is_init = false; - m_face_names.hide.clear(); + m_face_names->is_init = false; + m_face_names->hide.clear(); // cancel all process for generation of texture - for (FaceName &face : m_face_names.faces) + for (FaceName &face : m_face_names->faces) if (face.cancel != nullptr) face.cancel->store(true); - glsafe(::glDeleteTextures(1, &m_face_names.texture_id)); - m_face_names.texture_id = 0; + glsafe(::glDeleteTextures(1, &m_face_names->texture_id)); + m_face_names->texture_id = 0; // Remove value from search input ImGuiWrapper::left_inputs(); - m_face_names.search.clear(); + m_face_names->search.clear(); } // delete unloadable face name when try to use if (del_index.has_value()) { - auto face = m_face_names.faces.begin() + (*del_index); - std::vector& bad = m_face_names.bad; + auto face = m_face_names->faces.begin() + (*del_index); + std::vector& bad = m_face_names->bad; // sorted insert into bad fonts auto it = std::upper_bound(bad.begin(), bad.end(), face->wx_name); bad.insert(it, face->wx_name); - m_face_names.faces.erase(face); + m_face_names->faces.erase(face); // update cached file - store(m_face_names); + store(*m_face_names); } #ifdef ALLOW_ADD_FONT_BY_FILE @@ -2413,7 +1899,6 @@ void GLGizmoEmboss::draw_model_type() Plater::TakeSnapshot snapshot(plater, _L("Change Text Type"), UndoRedo::SnapshotType::GizmoAction); m_volume->set_type(*new_type); - // move inside bool is_volume_move_inside = (type == part); bool is_volume_move_outside = (*new_type == part); // Update volume position when switch (from part) or (into part) @@ -2443,19 +1928,14 @@ void GLGizmoEmboss::draw_style_rename_popup() { std::string text_in_popup = GUI::format(_L("Rename style(%1%) for embossing text"), old_name) + ": "; ImGui::Text("%s", text_in_popup.c_str()); - bool is_unique = true; - for (const auto &item : m_style_manager.get_styles()) { - const EmbossStyle &style = item.style; - if (&style == &m_style_manager.get_style()) - continue; // could be same as original name - if (style.name == new_name) is_unique = false; - } + bool is_unique = (new_name == old_name) || // could be same as before rename + m_style_manager.is_unique_style_name(new_name); bool allow_change = false; //B18 if (new_name.empty()) { - m_imgui->text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name can't be empty.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name can't be empty.")); }else if (!is_unique) { - m_imgui->text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name has to be unique.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name has to be unique.")); } else { ImGui::NewLine(); allow_change = true; @@ -2473,7 +1953,7 @@ void GLGizmoEmboss::draw_style_rename_popup() { if (store) { // rename style in all objects and volumes - for (ModelObject *mo :wxGetApp().plater()->model().objects) { + for (const ModelObject *mo :wxGetApp().plater()->model().objects) { for (ModelVolume *mv : mo->volumes) { if (!mv->text_configuration.has_value()) continue; std::string& name = mv->text_configuration->style.name; @@ -2493,7 +1973,7 @@ void GLGizmoEmboss::draw_style_rename_button() bool can_rename = m_style_manager.exist_stored_style(); std::string title = _u8L("Rename style"); const char * popup_id = title.c_str(); - if (priv::draw_button(m_icons, IconType::rename, !can_rename)) { + if (draw_button(m_icons, IconType::rename, !can_rename)) { assert(m_style_manager.get_stored_style()); ImGui::OpenPopup(popup_id); } @@ -2532,16 +2012,14 @@ void GLGizmoEmboss::draw_style_save_as_popup() { // use name inside of volume configuration as temporary new name std::string &new_name = m_volume->text_configuration->style.name; - bool is_unique = true; - for (const auto &item : m_style_manager.get_styles()) - if (item.style.name == new_name) is_unique = false; + bool is_unique = m_style_manager.is_unique_style_name(new_name); bool allow_change = false; //B18 if (new_name.empty()) { - m_imgui->text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name can't be empty.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name can't be empty.")); }else if (!is_unique) { - m_imgui->text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name has to be unique.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_BLUE_LIGHT, _u8L("Name has to be unique.")); } else { ImGui::NewLine(); allow_change = true; @@ -2574,7 +2052,7 @@ void GLGizmoEmboss::draw_style_add_button() bool only_add_style = !m_style_manager.exist_stored_style(); bool can_add = true; if (only_add_style && - m_volume->text_configuration->style.type != WxFontUtils::get_actual_type()) + m_volume->text_configuration->style.type != WxFontUtils::get_current_type()) can_add = false; std::string title = _u8L("Save as new style"); @@ -2597,7 +2075,7 @@ void GLGizmoEmboss::draw_style_add_button() } } - if (ImGui::BeginPopupModal(popup_id, 0, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::BeginPopupModal(popup_id, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { m_imgui->disable_background_fadeout_animation(); draw_style_save_as_popup(); ImGui::EndPopup(); @@ -2662,46 +2140,46 @@ void GLGizmoEmboss::draw_delete_style_button() { } } -// FIX IT: it should not change volume position before successfull change -void GLGizmoEmboss::fix_transformation(const FontProp &from, - const FontProp &to) -{ +namespace { +// FIX IT: It should not change volume position before successfull change volume by process +void fix_transformation(const StyleManager::Style &from, const StyleManager::Style &to, GLCanvas3D &canvas) { // fix Z rotation when exists difference in styles const std::optional &f_angle_opt = from.angle; const std::optional &t_angle_opt = to.angle; if (!is_approx(f_angle_opt, t_angle_opt)) { // fix rotation - float f_angle = f_angle_opt.has_value() ? *f_angle_opt : .0f; - float t_angle = t_angle_opt.has_value() ? *t_angle_opt : .0f; - do_rotate(t_angle - f_angle); + float f_angle = f_angle_opt.value_or(.0f); + float t_angle = t_angle_opt.value_or(.0f); + do_local_z_rotate(canvas, t_angle - f_angle); } // fix distance (Z move) when exists difference in styles const std::optional &f_move_opt = from.distance; const std::optional &t_move_opt = to.distance; if (!is_approx(f_move_opt, t_move_opt)) { - float f_move = f_move_opt.has_value() ? *f_move_opt : .0f; - float t_move = t_move_opt.has_value() ? *t_move_opt : .0f; - do_translate(Vec3d::UnitZ() * (t_move - f_move)); + float f_move = f_move_opt.value_or(.0f); + float t_move = t_move_opt.value_or(.0f); + do_local_z_move(canvas, t_move - f_move); } } +} // namesapce void GLGizmoEmboss::draw_style_list() { if (!m_style_manager.is_active_font()) return; - const EmbossStyle *stored_style = nullptr; + const StyleManager::Style *stored_style = nullptr; bool is_stored = m_style_manager.exist_stored_style(); if (is_stored) stored_style = m_style_manager.get_stored_style(); - const EmbossStyle &actual_style = m_style_manager.get_style(); - bool is_changed = (stored_style)? !(*stored_style == actual_style) : true; + const StyleManager::Style ¤t_style = m_style_manager.get_style(); + bool is_changed = (stored_style)? !(*stored_style == current_style) : true; bool is_modified = is_stored && is_changed; const float &max_style_name_width = m_gui_cfg->max_style_name_width; std::string &trunc_name = m_style_manager.get_truncated_name(); if (trunc_name.empty()) { // generate trunc name - std::string current_name = actual_style.name; + std::string current_name = current_style.name; ImGuiWrapper::escape_double_hash(current_name); trunc_name = ImGuiWrapper::trunc(current_name, max_style_name_width); } @@ -2723,19 +2201,19 @@ void GLGizmoEmboss::draw_style_list() { m_style_manager.init_style_images(m_gui_cfg->max_style_image_size, m_text); m_style_manager.init_trunc_names(max_style_name_width); std::optional> swap_indexes; - const std::vector &styles = m_style_manager.get_styles(); - for (const auto &item : styles) { - size_t index = &item - &styles.front(); - const EmbossStyle &style = item.style; + const StyleManager::Styles &styles = m_style_manager.get_styles(); + for (const StyleManager::Style &style : styles) { + size_t index = &style - &styles.front(); const std::string &actual_style_name = style.name; ImGui::PushID(actual_style_name.c_str()); bool is_selected = (index == m_style_manager.get_style_index()); - ImVec2 select_size(0,m_gui_cfg->max_style_image_size.y()); // 0,0 --> calculate in draw - const std::optional &img = item.image; + float select_height = static_cast(m_gui_cfg->max_style_image_size.y()); + ImVec2 select_size(0.f, select_height); // 0,0 --> calculate in draw + const std::optional &img = style.image; // allow click delete button ImGuiSelectableFlags_ flags = ImGuiSelectableFlags_AllowItemOverlap; - if (ImGui::Selectable(item.truncated_name.c_str(), is_selected, flags, select_size)) { + if (ImGui::Selectable(style.truncated_name.c_str(), is_selected, flags, select_size)) { selected_style_index = index; } else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", actual_style_name.c_str()); @@ -2767,18 +2245,18 @@ void GLGizmoEmboss::draw_style_list() { // do not keep in memory style images when no combo box open m_style_manager.free_style_images(); if (ImGui::IsItemHovered()) { - std::string style_name = add_text_modify(actual_style.name); + std::string style_name = add_text_modify(current_style.name); std::string tooltip = is_modified? - GUI::format(_L("Modified style \"%1%\""), actual_style.name): - GUI::format(_L("Current style is \"%1%\""), actual_style.name); + GUI::format(_L("Modified style \"%1%\""), current_style.name): + GUI::format(_L("Current style is \"%1%\""), current_style.name); ImGui::SetTooltip(" %s", tooltip.c_str()); } } // Check whether user wants lose actual style modification if (selected_style_index.has_value() && is_modified) { - const EmbossStyle &style = m_style_manager.get_styles()[*selected_style_index].style; - wxString message = GUI::format_wxstr(_L("Changing style to \"%1%\" will discard current style modification.\n\nWould you like to continue anyway?"), style.name); + const std::string & style_name = m_style_manager.get_styles()[*selected_style_index].name; + wxString message = GUI::format_wxstr(_L("Changing style to \"%1%\" will discard current style modification.\n\nWould you like to continue anyway?"), style_name); MessageDialog not_loaded_style_message(nullptr, message, _L("Warning"), wxICON_WARNING | wxYES | wxNO); if (not_loaded_style_message.ShowModal() != wxID_YES) selected_style_index.reset(); @@ -2786,12 +2264,12 @@ void GLGizmoEmboss::draw_style_list() { // selected style from combo box if (selected_style_index.has_value()) { - const EmbossStyle &style = m_style_manager.get_styles()[*selected_style_index].style; + const StyleManager::Style &style = m_style_manager.get_styles()[*selected_style_index]; // create copy to be able do fix transformation only when successfully load style - FontProp act_prop = actual_style.prop; // copy - FontProp new_prop = style.prop; // copy + StyleManager::Style cur_s = current_style; // copy + StyleManager::Style new_s = style; // copy if (m_style_manager.load_style(*selected_style_index)) { - fix_transformation(act_prop, new_prop); + ::fix_transformation(cur_s, new_s, m_parent); process(); } else { wxString title = _L("Not valid style."); @@ -2931,7 +2409,7 @@ bool GLGizmoEmboss::revertible(const std::string &name, const T *default_value, const std::string &undo_tooltip, float undo_offset, - Draw draw) + Draw draw) const { bool changed = exist_change(value, default_value); if (changed || default_value == nullptr) @@ -2955,42 +2433,43 @@ bool GLGizmoEmboss::revertible(const std::string &name, return draw(); } +// May be move to ImGuiWrapper +template bool imgui_input(const char *label, T *v, T step, T step_fast, const char *format, ImGuiInputTextFlags flags); +template<> bool imgui_input(const char *label, float *v, float step, float step_fast, const char *format, ImGuiInputTextFlags flags) +{ return ImGui::InputFloat(label, v, step, step_fast, format, flags); } +template<> bool imgui_input(const char *label, double *v, double step, double step_fast, const char *format, ImGuiInputTextFlags flags) +{ return ImGui::InputDouble(label, v, step, step_fast, format, flags); } -bool GLGizmoEmboss::rev_input(const std::string &name, - float &value, - const float *default_value, - const std::string &undo_tooltip, - float step, - float step_fast, - const char *format, - ImGuiInputTextFlags flags) +template +bool GLGizmoEmboss::rev_input(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, ImGuiInputTextFlags flags) const { // draw offseted input - auto draw_offseted_input = [&]()->bool{ - float input_offset = m_gui_cfg->input_offset; - float input_width = m_gui_cfg->input_width; - ImGui::SameLine(input_offset); - ImGui::SetNextItemWidth(input_width); - return ImGui::InputFloat(("##" + name).c_str(), + auto draw_offseted_input = [&offset = m_gui_cfg->input_offset, &width = m_gui_cfg->input_width, + &name, &value, &step, &step_fast, format, flags](){ + ImGui::SameLine(offset); + ImGui::SetNextItemWidth(width); + return imgui_input(("##" + name).c_str(), &value, step, step_fast, format, flags); }; float undo_offset = ImGui::GetStyle().FramePadding.x; return revertible(name, value, default_value, undo_tooltip, undo_offset, draw_offseted_input); } +template bool GLGizmoEmboss::rev_input_mm(const std::string &name, - float &value, - const float *default_value_ptr, + T &value, + const T *default_value_ptr, const std::string &undo_tooltip, - float step, - float step_fast, + T step, + T step_fast, const char *format, bool use_inch, - const std::optional& scale) + const std::optional &scale) const { // _variable which temporary keep value - float value_ = value; - float default_value_; + T value_ = value; + T default_value_; if (use_inch) { // calc value in inch value_ *= ObjectManipulation::mm_to_in; @@ -3018,11 +2497,11 @@ bool GLGizmoEmboss::rev_input_mm(const std::string &name, bool GLGizmoEmboss::rev_checkbox(const std::string &name, bool &value, const bool *default_value, - const std::string &undo_tooltip) + const std::string &undo_tooltip) const { // draw offseted input - auto draw_offseted_input = [&]() -> bool { - ImGui::SameLine(m_gui_cfg->advanced_input_offset); + auto draw_offseted_input = [&offset = m_gui_cfg->advanced_input_offset, &name, &value](){ + ImGui::SameLine(offset); return ImGui::Checkbox(("##" + name).c_str(), &value); }; float undo_offset = ImGui::GetStyle().FramePadding.x; @@ -3034,7 +2513,7 @@ bool GLGizmoEmboss::set_height() { float &value = m_style_manager.get_font_prop().size_in_mm; // size can't be zero or negative - priv::Limits::apply(value, priv::limits.size_in_mm); + apply(value, limits.size_in_mm); if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { assert(false); @@ -3073,29 +2552,24 @@ void GLGizmoEmboss::draw_height(bool use_inch) process(); } -bool GLGizmoEmboss::set_depth() -{ - float &value = m_style_manager.get_font_prop().emboss; - - // size can't be zero or negative - priv::Limits::apply(value, priv::limits.emboss); - - // only different value need process - return !is_approx(value, m_volume->text_configuration->style.prop.emboss); -} void GLGizmoEmboss::draw_depth(bool use_inch) { - float &value = m_style_manager.get_font_prop().emboss; - const EmbossStyle* stored_style = m_style_manager.get_stored_style(); - const float *stored = ((stored_style)? &stored_style->prop.emboss : nullptr); + double &value = m_style_manager.get_style().projection.depth; + const StyleManager::Style * stored_style = m_style_manager.get_stored_style(); + const double *stored = ((stored_style!=nullptr)? &stored_style->projection.depth : nullptr); const std::string revert_emboss_depth = _u8L("Revert embossed depth."); const char *size_format = ((use_inch) ? "%.3f in" : "%.2f mm"); const std::string name = m_gui_cfg->translations.depth; - if (rev_input_mm(name, value, stored, revert_emboss_depth, 0.1f, 1.f, size_format, use_inch, m_scale_depth)) - if (set_depth()) + if (rev_input_mm(name, value, stored, revert_emboss_depth, 0.1, 1., size_format, use_inch, m_scale_depth)){ + // size can't be zero or negative + apply(value, limits.emboss); + + // only different value need process + if(!is_approx(value, m_volume->emboss_shape->projection.depth)) process(); } +} bool GLGizmoEmboss::rev_slider(const std::string &name, std::optional& value, @@ -3104,7 +2578,7 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, int v_min, int v_max, const std::string& format, - const wxString &tooltip) + const wxString &tooltip) const { auto draw_slider_optional_int = [&]() -> bool { float slider_offset = m_gui_cfg->advanced_input_offset; @@ -3126,7 +2600,7 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, float v_min, float v_max, const std::string& format, - const wxString &tooltip) + const wxString &tooltip) const { auto draw_slider_optional_float = [&]() -> bool { float slider_offset = m_gui_cfg->advanced_input_offset; @@ -3148,7 +2622,7 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, float v_min, float v_max, const std::string &format, - const wxString &tooltip) + const wxString &tooltip) const { auto draw_slider_float = [&]() -> bool { float slider_offset = m_gui_cfg->advanced_input_offset; @@ -3163,37 +2637,6 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, undo_tooltip, undo_offset, draw_slider_float); } -void GLGizmoEmboss::do_translate(const Vec3d &relative_move) -{ - assert(m_volume != nullptr); - assert(m_volume->text_configuration.has_value()); - Selection &selection = m_parent.get_selection(); - assert(!selection.is_empty()); - selection.setup_cache(); - selection.translate(relative_move, TransformationType::Local); - - std::string snapshot_name; // empty mean no store undo / redo - // NOTE: it use L instead of _L macro because prefix _ is appended inside - // function do_move - // snapshot_name = L("Set surface distance"); - m_parent.do_move(snapshot_name); -} - -void GLGizmoEmboss::do_rotate(float relative_z_angle) -{ - assert(m_volume != nullptr); - assert(m_volume->text_configuration.has_value()); - Selection &selection = m_parent.get_selection(); - assert(!selection.is_empty()); - selection.setup_cache(); - selection.rotate(Vec3d(0., 0., relative_z_angle), get_transformation_type(selection)); - - std::string snapshot_name; // empty meand no store undo / redo - // NOTE: it use L instead of _L macro because prefix _ is appended - // inside function do_move - // snapshot_name = L("Set text rotation"); - m_parent.do_rotate(snapshot_name); -} void GLGizmoEmboss::draw_advanced() { @@ -3216,8 +2659,7 @@ void GLGizmoEmboss::draw_advanced() ", unitPerEm=" + std::to_string(font_info.unit_per_em) + ", cache(" + std::to_string(cache_size) + " glyphs)"; if (font_file->infos.size() > 1) { - unsigned int collection = font_prop.collection_number.has_value() ? - *font_prop.collection_number : 0; + unsigned int collection = current_prop.collection_number.value_or(0); ff_property += ", collect=" + std::to_string(collection+1) + "/" + std::to_string(font_file->infos.size()); } m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, ff_property); @@ -3226,26 +2668,23 @@ void GLGizmoEmboss::draw_advanced() bool exist_change = false; auto &tr = m_gui_cfg->translations; - const EmbossStyle *stored_style = nullptr; + const StyleManager::Style *stored_style = nullptr; if (m_style_manager.exist_stored_style()) stored_style = m_style_manager.get_stored_style(); bool is_the_only_one_part = m_volume->is_the_only_one_part(); - bool can_use_surface = (font_prop.use_surface)? true : // already used surface must have option to uncheck + bool can_use_surface = (m_volume->emboss_shape->projection.use_surface)? true : // already used surface must have option to uncheck !is_the_only_one_part; m_imgui->disabled_begin(!can_use_surface); const bool *def_use_surface = stored_style ? - &stored_style->prop.use_surface : nullptr; - if (rev_checkbox(tr.use_surface, font_prop.use_surface, def_use_surface, + &stored_style->projection.use_surface : nullptr; + StyleManager::Style ¤t_style = m_style_manager.get_style(); + bool &use_surface = current_style.projection.use_surface; + if (rev_checkbox(tr.use_surface, use_surface, def_use_surface, _u8L("Revert using of model surface."))) { - if (font_prop.use_surface) { + if (use_surface) // when using surface distance is not used - font_prop.distance.reset(); - - // there should be minimal embossing depth - if (font_prop.emboss < 0.1) - font_prop.emboss = 1; - } + current_style.distance.reset(); process(); } m_imgui->disabled_end(); // !can_use_surface @@ -3272,9 +2711,9 @@ void GLGizmoEmboss::draw_advanced() m_text_lines.reset(); m_imgui->disabled_end(); // !can_use_per_glyph - auto draw_align = [&align = font_prop.align, gui_cfg = m_gui_cfg, &icons = m_icons]() { + auto draw_align = [&align = font_prop.align, input_offset = m_gui_cfg->advanced_input_offset, &icons = m_icons]() { bool is_change = false; - ImGui::SameLine(gui_cfg->advanced_input_offset); + ImGui::SameLine(input_offset); if (align.first==FontProp::HorizontalAlign::left) draw(get_icon(icons, IconType::align_horizontal_left, IconState::hovered)); else if (draw_button(icons, IconType::align_horizontal_left)) { align.first=FontProp::HorizontalAlign::left; is_change = true; } else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _CTX_utf8(L_CONTEXT("Left", "Alignment"), "Alignment").c_str()); @@ -3319,13 +2758,15 @@ void GLGizmoEmboss::draw_advanced() &stored_style->prop.char_gap : nullptr; int half_ascent = font_info.ascent / 2; - int min_char_gap = -half_ascent, max_char_gap = half_ascent; - if (rev_slider(tr.char_gap, font_prop.char_gap, def_char_gap, _u8L("Revert gap between characters"), + int min_char_gap = -half_ascent; + int max_char_gap = half_ascent; + FontProp ¤t_prop = current_style.prop; + if (rev_slider(tr.char_gap, current_prop.char_gap, def_char_gap, _u8L("Revert gap between characters"), min_char_gap, max_char_gap, units_fmt, _L("Distance between characters"))){ // Condition prevent recalculation when insertint out of limits value by imgui input - if (!priv::Limits::apply(font_prop.char_gap, priv::limits.char_gap) || - !m_volume->text_configuration->style.prop.char_gap.has_value() || - m_volume->text_configuration->style.prop.char_gap != font_prop.char_gap) { + const std::optional &volume_char_gap = m_volume->text_configuration->style.prop.char_gap; + if (!apply(current_prop.char_gap, limits.char_gap) || + !volume_char_gap.has_value() || volume_char_gap != current_prop.char_gap) { // char gap is stored inside of imgui font atlas m_style_manager.clear_imgui_font(); exist_change = true; @@ -3337,13 +2778,14 @@ void GLGizmoEmboss::draw_advanced() m_imgui->disabled_begin(!is_multiline); auto def_line_gap = stored_style ? &stored_style->prop.line_gap : nullptr; - int min_line_gap = -half_ascent, max_line_gap = half_ascent; - if (rev_slider(tr.line_gap, font_prop.line_gap, def_line_gap, _u8L("Revert gap between lines"), + int min_line_gap = -half_ascent; + int max_line_gap = half_ascent; + if (rev_slider(tr.line_gap, current_prop.line_gap, def_line_gap, _u8L("Revert gap between lines"), min_line_gap, max_line_gap, units_fmt, _L("Distance between lines"))){ // Condition prevent recalculation when insertint out of limits value by imgui input - if (!priv::Limits::apply(font_prop.line_gap, priv::limits.line_gap) || - !m_volume->text_configuration->style.prop.line_gap.has_value() || - m_volume->text_configuration->style.prop.line_gap != font_prop.line_gap) { + const std::optional &volume_line_gap = m_volume->text_configuration->style.prop.line_gap; + if (!apply(current_prop.line_gap, limits.line_gap) || + !volume_line_gap.has_value() || volume_line_gap != current_prop.line_gap) { // line gap is planed to be stored inside of imgui font atlas m_style_manager.clear_imgui_font(); if (font_prop.per_glyph) @@ -3356,41 +2798,39 @@ void GLGizmoEmboss::draw_advanced() // input boldness auto def_boldness = stored_style ? &stored_style->prop.boldness : nullptr; - if (rev_slider(tr.boldness, font_prop.boldness, def_boldness, _u8L("Undo boldness"), - priv::limits.boldness.gui.min, priv::limits.boldness.gui.max, units_fmt, _L("Tiny / Wide glyphs"))){ - if (!priv::Limits::apply(font_prop.boldness, priv::limits.boldness.values) || - !m_volume->text_configuration->style.prop.boldness.has_value() || - m_volume->text_configuration->style.prop.boldness != font_prop.boldness) + if (rev_slider(tr.boldness, current_prop.boldness, def_boldness, _u8L("Undo boldness"), + limits.boldness.gui.min, limits.boldness.gui.max, units_fmt, _L("Tiny / Wide glyphs"))){ + const std::optional &volume_boldness = m_volume->text_configuration->style.prop.boldness; + if (!apply(current_prop.boldness, limits.boldness.values) || + !volume_boldness.has_value() || volume_boldness != current_prop.boldness) exist_change = true; } // input italic auto def_skew = stored_style ? &stored_style->prop.skew : nullptr; - if (rev_slider(tr.skew_ration, font_prop.skew, def_skew, _u8L("Undo letter's skew"), - priv::limits.skew.gui.min, priv::limits.skew.gui.max, "%.2f", _L("Italic strength ratio"))){ - if (!priv::Limits::apply(font_prop.skew, priv::limits.skew.values) || - !m_volume->text_configuration->style.prop.skew.has_value() || - m_volume->text_configuration->style.prop.skew != font_prop.skew) + if (rev_slider(tr.skew_ration, current_prop.skew, def_skew, _u8L("Undo letter's skew"), + limits.skew.gui.min, limits.skew.gui.max, "%.2f", _L("Italic strength ratio"))){ + const std::optional &volume_skew = m_volume->text_configuration->style.prop.skew; + if (!apply(current_prop.skew, limits.skew.values) || + !volume_skew.has_value() ||volume_skew != current_prop.skew) exist_change = true; } // input surface distance - bool allowe_surface_distance = - !m_volume->text_configuration->style.prop.use_surface && - !m_volume->is_the_only_one_part(); - std::optional &distance = font_prop.distance; - float prev_distance = distance.has_value() ? *distance : .0f, - min_distance = -2 * font_prop.emboss, - max_distance = 2 * font_prop.emboss; + bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); + std::optional &distance = current_style.distance; + float prev_distance = distance.value_or(.0f); + float min_distance = static_cast(-2 * current_style.projection.depth); + float max_distance = static_cast(2 * current_style.projection.depth); auto def_distance = stored_style ? - &stored_style->prop.distance : nullptr; + &stored_style->distance : nullptr; m_imgui->disabled_begin(!allowe_surface_distance); + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); const std::string undo_move_tooltip = _u8L("Undo translation"); const wxString move_tooltip = _L("Distance of the center of the text to the model surface."); bool is_moved = false; - bool use_inch = wxGetApp().app_config->get_bool("use_inches"); if (use_inch) { std::optional distance_inch; if (distance.has_value()) distance_inch = (*distance * ObjectManipulation::mm_to_in); @@ -3403,9 +2843,9 @@ void GLGizmoEmboss::draw_advanced() max_distance *= ObjectManipulation::mm_to_in; if (rev_slider(tr.from_surface, distance_inch, def_distance, undo_move_tooltip, min_distance, max_distance, "%.3f in", move_tooltip)) { if (distance_inch.has_value()) { - font_prop.distance = *distance_inch * ObjectManipulation::in_to_mm; + distance = *distance_inch * ObjectManipulation::in_to_mm; } else { - font_prop.distance.reset(); + distance.reset(); } is_moved = true; } @@ -3418,83 +2858,77 @@ void GLGizmoEmboss::draw_advanced() if (font_prop.per_glyph){ process(); } else { - m_volume->text_configuration->style.prop.distance = font_prop.distance; - float act_distance = font_prop.distance.has_value() ? *font_prop.distance : .0f; - do_translate(Vec3d::UnitZ() * (act_distance - prev_distance)); + do_local_z_move(m_parent, distance.value_or(.0f) - prev_distance); } } - m_imgui->disabled_end(); + m_imgui->disabled_end(); // allowe_surface_distance // slider for Clock-wise angle in degress // stored angle is optional CCW and in radians // Convert stored value to degress // minus create clock-wise roation from CCW - const std::optional &angle_opt = m_style_manager.get_font_prop().angle; - float angle = angle_opt.has_value() ? *angle_opt: 0.f; + float angle = current_style.angle.value_or(0.f); float angle_deg = static_cast(-angle * 180 / M_PI); float def_angle_deg_val = - (!stored_style || !stored_style->prop.angle.has_value()) ? - 0.f : (*stored_style->prop.angle * -180 / M_PI); + (!stored_style || !stored_style->angle.has_value()) ? + 0.f : (*stored_style->angle * -180 / M_PI); float* def_angle_deg = stored_style ? &def_angle_deg_val : nullptr; if (rev_slider(tr.rotation, angle_deg, def_angle_deg, _u8L("Undo rotation"), - priv::limits.angle.min, priv::limits.angle.max, u8"%.2f °", + limits.angle.min, limits.angle.max, u8"%.2f °", _L("Rotate text Clock-wise."))) { // convert back to radians and CCW - float angle_rad = static_cast(-angle_deg * M_PI / 180.0); - priv::to_range_pi_pi(angle_rad); + double angle_rad = -angle_deg * M_PI / 180.0; + Geometry::to_range_pi_pi(angle_rad); - float diff_angle = angle_rad - angle; - do_rotate(diff_angle); + double diff_angle = angle_rad - angle; + do_local_z_rotate(m_parent, diff_angle); // calc angle after rotation - const GLVolume *gl_volume = get_selected_gl_volume(m_parent.get_selection()); + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = get_selected_gl_volume(selection); assert(gl_volume != nullptr); assert(m_style_manager.is_active_font()); if (m_style_manager.is_active_font() && gl_volume != nullptr) - m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); + m_style_manager.get_style().angle = calc_angle(selection); if (font_prop.per_glyph) reinit_text_lines(m_text_lines.get_lines().size()); // recalculate for surface cut - if (font_prop.use_surface || font_prop.per_glyph) + if (use_surface || font_prop.per_glyph) process(); } // Keep up - lock button icon + if (!m_volume->is_the_only_one_part()) { ImGui::SameLine(m_gui_cfg->lock_offset); const IconManager::Icon &icon = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::activable); const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock_bold : IconType::unlock_bold, IconState::activable); const IconManager::Icon &icon_disable = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::disabled); - if (button(icon, icon_hover, icon_disable)) { + if (button(icon, icon_hover, icon_disable)) m_keep_up = !m_keep_up; - if (m_keep_up) { - // copy angle to volume - m_volume->text_configuration->style.prop.angle = font_prop.angle; - } - } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", (m_keep_up? _u8L("Unlock the text's rotation when moving text along the object's surface."): _u8L("Lock the text's rotation when moving text along the object's surface.") ).c_str()); + } // when more collection add selector if (ff.font_file->infos.size() > 1) { ImGui::Text("%s", tr.collection.c_str()); ImGui::SameLine(m_gui_cfg->advanced_input_offset); ImGui::SetNextItemWidth(m_gui_cfg->input_width); - unsigned int selected = font_prop.collection_number.has_value() ? - *font_prop.collection_number : 0; + unsigned int selected = current_prop.collection_number.value_or(0); if (ImGui::BeginCombo("## Font collection", std::to_string(selected).c_str())) { for (unsigned int i = 0; i < ff.font_file->infos.size(); ++i) { ImGui::PushID(1 << (10 + i)); bool is_selected = (i == selected); if (ImGui::Selectable(std::to_string(i).c_str(), is_selected)) { - if (i == 0) font_prop.collection_number.reset(); - else font_prop.collection_number = i; + if (i == 0) current_prop.collection_number.reset(); + else current_prop.collection_number = i; exist_change = true; } ImGui::PopID(); @@ -3517,29 +2951,26 @@ void GLGizmoEmboss::draw_advanced() if (ImGui::Button(_u8L("Set text to face camera").c_str())) { assert(get_selected_volume(m_parent.get_selection()) == m_volume); const Camera &cam = wxGetApp().plater()->get_camera(); - const FontProp &prop = m_style_manager.get_font_prop(); - if (priv::apply_camera_dir(cam, m_parent, m_keep_up) && - (prop.use_surface || prop.per_glyph)){ - if (prop.per_glyph) - reinit_text_lines(); - process(); - } + auto wanted_up_limit = m_keep_up ? std::optional(UP_LIMIT) : std::optional{}; + if (face_selected_volume_to_camera(cam, m_parent, wanted_up_limit)) + volume_transformation_changed(); } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", _u8L("Orient the text towards the camera.").c_str()); } + //ImGui::SameLine(); if (ImGui::Button("Re-emboss")) GLGizmoEmboss::re_emboss(*m_volume); #ifdef ALLOW_DEBUG_MODE - ImGui::Text("family = %s", (font_prop.family.has_value() ? - font_prop.family->c_str() : + ImGui::Text("family = %s", (current_prop.family.has_value() ? + current_prop.family->c_str() : " --- ")); - ImGui::Text("face name = %s", (font_prop.face_name.has_value() ? - font_prop.face_name->c_str() : + ImGui::Text("face name = %s", (current_prop.face_name.has_value() ? + current_prop.face_name->c_str() : " --- ")); ImGui::Text("style = %s", - (font_prop.style.has_value() ? font_prop.style->c_str() : + (current_prop.style.has_value() ? current_prop.style->c_str() : " --- ")); - ImGui::Text("weight = %s", (font_prop.weight.has_value() ? - font_prop.weight->c_str() : + ImGui::Text("weight = %s", (current_prop.weight.has_value() ? + current_prop.weight->c_str() : " --- ")); std::string descriptor = style.path; @@ -3556,9 +2987,9 @@ void GLGizmoEmboss::set_minimal_window_size(bool is_advance_edit_style) float diff_y = window_size.y - min_win_size_prev.y; m_is_advanced_edit_style = is_advance_edit_style; const ImVec2 &min_win_size = get_minimal_window_size(); - ImGui::SetWindowSize(ImVec2(0.f, min_win_size.y + diff_y), - ImGuiCond_Always); - priv::change_window_position(m_set_window_offset, true); + ImVec2 new_window_size(0.f, min_win_size.y + diff_y); + ImGui::SetWindowSize(new_window_size, ImGuiCond_Always); + m_set_window_offset = ImGuiWrapper::change_window_position(on_get_name().c_str(), true); } ImVec2 GLGizmoEmboss::get_minimal_window_size() const @@ -3571,8 +3002,8 @@ ImVec2 GLGizmoEmboss::get_minimal_window_size() const else res = m_gui_cfg->minimal_window_size_with_collections; - bool is_object = m_volume->get_object()->volumes.size() == 1; - if (!is_object) + // Can change type of volume + if (!m_volume->is_the_only_one_part()) res.y += m_gui_cfg->height_of_volume_type_selector; return res; } @@ -3585,7 +3016,7 @@ bool GLGizmoEmboss::choose_font_by_wxdialog() data.RestrictSelection(wxFONTRESTRICT_SCALABLE); // set previous selected font EmbossStyle &selected_style = m_style_manager.get_style(); - if (selected_style.type == WxFontUtils::get_actual_type()) { + if (selected_style.type == WxFontUtils::get_current_type()) { std::optional selected_font = WxFontUtils::load_wxFont( selected_style.path); if (selected_font.has_value()) data.SetInitialFont(*selected_font); @@ -3657,7 +3088,7 @@ bool GLGizmoEmboss::choose_true_type_file() // use first valid font for (auto &input_file : input_files) { std::string path = std::string(input_file.c_str()); - std::string name = priv::get_file_name(path); + std::string name = get_file_name(path); //make_unique_name(name, m_font_list); const FontProp& prop = m_style_manager.get_font_prop(); EmbossStyle style{ name, path, EmbossStyle::Type::file_path, prop }; @@ -3670,41 +3101,6 @@ bool GLGizmoEmboss::choose_true_type_file() } #endif // ALLOW_ADD_FONT_BY_FILE -#ifdef ALLOW_DEBUG_MODE -bool GLGizmoEmboss::choose_svg_file() -{ - wxArrayString input_files; - wxString fontDir = wxEmptyString; - wxString selectedFile = wxEmptyString; - wxFileDialog dialog(nullptr, _L("Choose SVG file")+":", fontDir, - selectedFile, file_wildcards(FT_SVG), - wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dialog.ShowModal() == wxID_OK) dialog.GetPaths(input_files); - if (input_files.IsEmpty()) return false; - if (input_files.size() != 1) return false; - auto & input_file = input_files.front(); - std::string path = std::string(input_file.c_str()); - std::string name = priv::get_file_name(path); - - NSVGimage *image = nsvgParseFromFile(path.c_str(), "mm", 96.0f); - ExPolygons polys = NSVGUtils::to_ExPolygons(image); - nsvgDelete(image); - - BoundingBox bb; - for (const auto &p : polys) bb.merge(p.contour.points); - const FontProp &fp = m_style_manager.get_font_prop(); - float scale = fp.size_in_mm / std::max(bb.max.x(), bb.max.y()); - auto project = std::make_unique( - std::make_unique(fp.emboss / scale), scale); - indexed_triangle_set its = polygons2model(polys, *project); - return false; - // test store: - // for (auto &poly : polys) poly.scale(1e5); - // SVG svg("converted.svg", BoundingBox(polys.front().contour.points)); - // svg.draw(polys); - //return add_volume(name, its); -} -#endif // ALLOW_DEBUG_MODE void GLGizmoEmboss::create_notification_not_valid_font( const TextConfiguration &tc) @@ -3788,22 +3184,89 @@ void GLGizmoEmboss::init_icons() m_icons = m_icon_manager.init(filenames, size, type); } -const IconManager::Icon &priv::get_icon(const IconManager::VIcons& icons, IconType type, IconState state) { return *icons[(unsigned) type][(unsigned) state]; } -bool priv::draw_button(const IconManager::VIcons &icons, IconType type, bool disable) +static std::size_t hash_value(wxString const &s){ + boost::hash hasher; + return hasher(s.ToStdString()); +} + +// increase number when change struct FacenamesSerializer +constexpr std::uint32_t FACENAMES_VERSION = 1; +struct FacenamesSerializer { + // hash number for unsorted vector of installed font into system + size_t hash = 0; + // assumption that is loadable + std::vector good; + // Can't load for some reason + std::vector bad; +}; + +template void save(Archive &archive, wxString const &d) +{ std::string s(d.ToUTF8().data()); archive(s);} +template void load(Archive &archive, wxString &d) +{ std::string s; archive(s); d = s;} +template void serialize(Archive &ar, FacenamesSerializer &t, const std::uint32_t version) +{ + // When performing a load, the version associated with the class + // is whatever it was when that data was originally serialized + // When we save, we'll use the version that is defined in the macro + if (version != FACENAMES_VERSION) return; + ar(t.hash, t.good, t.bad); +} +CEREAL_CLASS_VERSION(::FacenamesSerializer, FACENAMES_VERSION); // register class version + +///////////// +// private namespace implementation +/////////////// +namespace { + +const IconManager::Icon &get_icon(const IconManager::VIcons& icons, IconType type, IconState state) { + return *icons[(unsigned) type][(unsigned) state]; } + +bool draw_button(const IconManager::VIcons &icons, IconType type, bool disable){ return Slic3r::GUI::button( get_icon(icons, type, IconState::activable), get_icon(icons, type, IconState::hovered), get_icon(icons, type, IconState::disabled), - disable - ); + disable);} + +TextDataBase::TextDataBase(DataBase &&parent, + const FontFileWithCache &font_file, + TextConfiguration &&text_configuration, + const EmbossProjection &projection) + : DataBase(std::move(parent)), m_font_file(font_file) /* copy */, m_text_configuration(std::move(text_configuration)) +{ + assert(m_font_file.has_value()); + shape.projection = projection; // copy + + const FontProp &fp = m_text_configuration.style.prop; + const FontFile &ff = *m_font_file.font_file; + shape.scale = get_text_shape_scale(fp, ff); } -///////////// -// priv namespace implementation -/////////////// +EmbossShape &TextDataBase::create_shape() +{ + if (!shape.shapes_with_ids.empty()) + return shape; -DataBase priv::create_emboss_data_base(const std::string &text, + // create shape by configuration + const char *text = m_text_configuration.text.c_str(); + std::wstring text_w = boost::nowide::widen(text); + const FontProp &fp = m_text_configuration.style.prop; + auto was_canceled = [&c = cancel](){ return c->load(); }; + + shape.shapes_with_ids = text2vshapes(m_font_file, text_w, fp, was_canceled); + return shape; +} + +void TextDataBase::write(ModelVolume &volume) const +{ + DataBase::write(volume); + volume.text_configuration = m_text_configuration; // copy + assert(volume.emboss_shape.has_value()); +} + +std::unique_ptr create_emboss_data_base(const std::string &text, StyleManager &style_manager, TextLinesModel &text_lines, const Selection &selection, @@ -3824,14 +3287,13 @@ DataBase priv::create_emboss_data_base(const std::string &text, return {}; // no active font in style, should never happend !!! } - const EmbossStyle &es = style_manager.get_style(); + const StyleManager::Style &style = style_manager.get_style(); // actualize font path - during changes in gui it could be corrupted // volume must store valid path assert(style_manager.get_wx_font().IsOk()); - assert(es.path.compare(WxFontUtils::store_wxFont(style_manager.get_wx_font())) == 0); - TextConfiguration tc{es, text}; + assert(style.path.compare(WxFontUtils::store_wxFont(style_manager.get_wx_font())) == 0); - if (es.prop.per_glyph) { + if (style.prop.per_glyph) { if (!text_lines.is_init()) init_text_lines(text_lines, selection, style_manager); } else @@ -3846,146 +3308,26 @@ DataBase priv::create_emboss_data_base(const std::string &text, cancel->store(true); // create new shared ptr to cancel new job cancel = std::make_shared>(false); - return Slic3r::GUI::Emboss::DataBase{style_manager.get_font_file_with_cache(), tc, volume_name, is_outside, cancel, text_lines.get_lines()}; + DataBase base(volume_name, cancel); + base.is_outside = is_outside; + base.text_lines = text_lines.get_lines(); + base.from_surface = style.distance; + + FontFileWithCache &font = style_manager.get_font_file_with_cache(); + TextConfiguration tc{static_cast(style), text}; + return std::make_unique(std::move(base), font, std::move(tc), style.projection); } -void priv::start_create_object_job(DataBase &emboss_data, const Vec2d &coor) +CreateVolumeParams create_input(GLCanvas3D &canvas, const StyleManager::Style &style, RaycastManager& raycaster, ModelVolumeType volume_type) { - // start creation of new object + auto gizmo = static_cast(GLGizmosManager::Emboss); + const GLVolume *gl_volume = get_first_hovered_gl_volume(canvas); Plater *plater = wxGetApp().plater(); - const Camera &camera = plater->get_camera(); - const Pointfs &bed_shape = plater->build_volume().bed_shape(); - - // can't create new object with distance from surface - FontProp &prop = emboss_data.text_configuration.style.prop; - if (prop.distance.has_value()) prop.distance.reset(); - - // can't create new object with using surface - if (prop.use_surface) - prop.use_surface = false; - - // Transform3d volume_tr = priv::create_transformation_on_bed(mouse_pos, camera, bed_shape, prop.emboss / 2); - DataCreateObject data{std::move(emboss_data), coor, camera, bed_shape}; - auto job = std::make_unique(std::move(data)); - Worker &worker = plater->get_ui_job_worker(); - queue_job(worker, std::move(job)); + return CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), + plater->get_ui_job_worker(), volume_type, raycaster, gizmo, gl_volume, style.distance, style.angle}; } -void priv::start_create_volume_job(const ModelObject *object, - const Transform3d volume_trmat, - DataBase &emboss_data, - ModelVolumeType volume_type) -{ - bool &use_surface = emboss_data.text_configuration.style.prop.use_surface; - std::unique_ptr job; - if (use_surface) { - // Model to cut surface from. - SurfaceVolumeData::ModelSources sources = create_sources(object->volumes); - if (sources.empty()) { - use_surface = false; - } else { - SurfaceVolumeData sfvd{volume_trmat, std::move(sources)}; - CreateSurfaceVolumeData surface_data{std::move(emboss_data), std::move(sfvd), volume_type, object->id()}; - job = std::make_unique(std::move(surface_data)); - } - } - if (!use_surface) { - // create volume - DataCreateVolume data{std::move(emboss_data), volume_type, object->id(), volume_trmat}; - job = std::make_unique(std::move(data)); - } - - Plater *plater = wxGetApp().plater(); - Worker &worker = plater->get_ui_job_worker(); - queue_job(worker, std::move(job)); -} - -bool priv::start_create_volume_on_surface_job(DataBase &emboss_data, - ModelVolumeType volume_type, - const Vec2d &screen_coor, - const GLVolume *gl_volume, - RaycastManager &raycaster, - TextLinesModel &text_lines, - StyleManager &style_manager, - GLCanvas3D &canvas) -{ - assert(gl_volume != nullptr); - if (gl_volume == nullptr) return false; - if (gl_volume->volume_idx() < 0) return false; - - Plater *plater = wxGetApp().plater(); - const ModelObjectPtrs &objects = plater->model().objects; - - int object_idx = gl_volume->object_idx(); - if (object_idx < 0 || static_cast(object_idx) >= objects.size()) return false; - const ModelObject *obj_ptr = objects[object_idx]; - if (obj_ptr == nullptr) return false; - const ModelObject &obj = *obj_ptr; - size_t vol_id = obj.volumes[gl_volume->volume_idx()]->id().id; - auto cond = RaycastManager::AllowVolumes({vol_id}); - - RaycastManager::Meshes meshes = create_meshes(canvas, cond); - raycaster.actualize(obj, &cond, &meshes); - - const Camera &camera = plater->get_camera(); - std::optional hit = ray_from_camera(raycaster, screen_coor, camera, &cond); - - // context menu for add text could be open only by right click on an - // object. After right click, object is selected and object_idx is set - // also hit must exist. But there is options to add text by object list - if (!hit.has_value()) - return false; - - // Create result volume transformation - Transform3d surface_trmat = create_transformation_onto_surface(hit->position, hit->normal, priv::up_limit); - const FontProp &font_prop = emboss_data.text_configuration.style.prop; - apply_transformation(font_prop, surface_trmat); - Transform3d instance = gl_volume->get_instance_transformation().get_matrix(); - Transform3d volume_trmat = instance.inverse() * surface_trmat; - - if (font_prop.per_glyph){ - init_new_text_line(text_lines, volume_trmat, obj, style_manager); - emboss_data.text_lines = text_lines.get_lines(); - } - start_create_volume_job(obj_ptr, volume_trmat, emboss_data, volume_type); - return true; -} - -void priv::find_closest_volume(const Selection &selection, - const Vec2d &screen_center, - const Camera &camera, - const ModelObjectPtrs &objects, - Vec2d *closest_center, - const GLVolume **closest_volume) -{ - assert(closest_center != nullptr); - assert(closest_volume != nullptr); - assert(*closest_volume == nullptr); - const Selection::IndicesList &indices = selection.get_volume_idxs(); - assert(!indices.empty()); // no selected volume - if (indices.empty()) return; - - double center_sq_distance = std::numeric_limits::max(); - for (unsigned int id : indices) { - const GLVolume *gl_volume = selection.get_volume(id); - const ModelVolume *volume = get_model_volume(*gl_volume, objects); - if (volume == nullptr || !volume->is_model_part()) continue; - Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *gl_volume); - Vec2d c = hull.centroid().cast(); - Vec2d d = c - screen_center; - bool is_bigger_x = std::fabs(d.x()) > std::fabs(d.y()); - if ((is_bigger_x && d.x() * d.x() > center_sq_distance) || - (!is_bigger_x && d.y() * d.y() > center_sq_distance)) continue; - - double distance = d.squaredNorm(); - if (center_sq_distance < distance) continue; - center_sq_distance = distance; - *closest_center = c; - *closest_volume = gl_volume; - } -} - -ImVec2 priv::calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size) +ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size) { const Selection::IndicesList indices = selection.get_volume_idxs(); // no selected volume @@ -4004,86 +3346,383 @@ ImVec2 priv::calc_fine_position(const Selection &selection, const ImVec2 &window return offset; } -// Need internals to get window -#include "imgui/imgui_internal.h" -void priv::change_window_position(std::optional& output_window_offset, bool try_to_fix) { - const char* name = "Emboss"; - ImGuiWindow *window = ImGui::FindWindowByName(name); - // is window just created - if (window == NULL) - return; - - // position of window on screen - ImVec2 position = window->Pos; - ImVec2 size = window->SizeFull; - - // screen size - ImVec2 screen = ImGui::GetMainViewport()->Size; - - if (position.x < 0) { - if (position.y < 0) - output_window_offset = ImVec2(0, 0); - else - output_window_offset = ImVec2(0, position.y); - } else if (position.y < 0) { - output_window_offset = ImVec2(position.x, 0); - } else if (screen.x < (position.x + size.x)) { - if (screen.y < (position.y + size.y)) - output_window_offset = ImVec2(screen.x - size.x, screen.y - size.y); - else - output_window_offset = ImVec2(screen.x - size.x, position.y); - } else if (screen.y < (position.y + size.y)) { - output_window_offset = ImVec2(position.x, screen.y - size.y); +std::string concat(std::vector data) { + std::stringstream ss; + for (const auto &d : data) + ss << d.c_str() << ", "; + return ss.str(); } - if (!try_to_fix && output_window_offset.has_value()) - output_window_offset = ImVec2(-1, -1); // Cannot +boost::filesystem::path get_fontlist_cache_path(){ + return boost::filesystem::path(data_dir()) / "cache" / "fonts.cereal"; } -bool priv::apply_camera_dir(const Camera &camera, GLCanvas3D &canvas, bool keep_up) { - const Vec3d &cam_dir = camera.get_dir_forward(); +bool store(const Facenames &facenames) { + std::string cache_path = get_fontlist_cache_path().string(); + boost::nowide::ofstream file(cache_path, std::ios::binary); + ::cereal::BinaryOutputArchive archive(file); + std::vector good; + good.reserve(facenames.faces.size()); + for (const FaceName &face : facenames.faces) good.push_back(face.wx_name); + FacenamesSerializer data = {facenames.hash, good, facenames.bad}; - Selection &sel = canvas.get_selection(); - if (sel.is_empty()) return false; - - // camera direction transformed into volume coordinate system - Transform3d to_world = world_matrix_fixed(sel); - Vec3d cam_dir_tr = to_world.inverse().linear() * cam_dir; - cam_dir_tr.normalize(); + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); - Vec3d emboss_dir(0., 0., -1.); - - // check wether cam_dir is already used - if (is_approx(cam_dir_tr, emboss_dir)) return false; - - assert(sel.get_volume_idxs().size() == 1); - GLVolume *gl_volume = sel.get_volume(*sel.get_volume_idxs().begin()); - - Transform3d vol_rot; - Transform3d vol_tr = gl_volume->get_volume_transformation().get_matrix(); - // check whether cam_dir is opposit to emboss dir - if (is_approx(cam_dir_tr, -emboss_dir)) { - // rotate 180 DEG by y - vol_rot = Eigen::AngleAxis(M_PI_2, Vec3d(0., 1., 0.)); - } else { - // calc params for rotation - Vec3d axe = emboss_dir.cross(cam_dir_tr); - axe.normalize(); - double angle = std::acos(emboss_dir.dot(cam_dir_tr)); - vol_rot = Eigen::AngleAxis(angle, axe); + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to write fontlist cache - " << cache_path << ex.what(); + return false; } - - Vec3d offset = vol_tr * Vec3d::Zero(); - Vec3d offset_inv = vol_rot.inverse() * offset; - Transform3d res = vol_tr * - Eigen::Translation(-offset) * - vol_rot * - Eigen::Translation(offset_inv); - //Transform3d res = vol_tr * vol_rot; - gl_volume->set_volume_transformation(Geometry::Transformation(res)); - get_model_volume(*gl_volume, sel.get_model()->objects)->set_transformation(res); return true; } +bool load(Facenames &facenames) { + boost::filesystem::path path = get_fontlist_cache_path(); + std::string path_str = path.string(); + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "Fontlist cache - '" << path_str << "' does not exists."; + return false; + } + boost::nowide::ifstream file(path_str, std::ios::binary); + cereal::BinaryInputArchive archive(file); + + FacenamesSerializer data; + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to load fontlist cache - '" << path_str << "'. Exception: " << ex.what(); + return false; + } + + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); + + facenames.hash = data.hash; + facenames.faces.reserve(data.good.size()); + for (const wxString &face : data.good) + facenames.faces.push_back({face}); + facenames.bad = data.bad; + return true; +} + +void init_truncated_names(Facenames &face_names, float max_width) +{ + for (FaceName &face : face_names.faces) { + std::string name_str(face.wx_name.ToUTF8().data()); + face.name_truncated = ImGuiWrapper::trunc(name_str, max_width); + } + face_names.has_truncated_names = true; +} + +void init_face_names(Facenames &face_names) +{ + Timer t("enumerate_fonts"); + if (face_names.is_init) return; + face_names.is_init = true; + + // to reload fonts from system, when install new one + wxFontEnumerator::InvalidateCache(); + + // try load cache + // Only not OS enumerated face has hash value 0 + if (face_names.hash == 0) { + load(face_names); + face_names.has_truncated_names = false; + } + using namespace std::chrono; + steady_clock::time_point enumerate_start = steady_clock::now(); + ScopeGuard sg([&enumerate_start, &face_names = face_names]() { + steady_clock::time_point enumerate_end = steady_clock::now(); + long long enumerate_duration = duration_cast(enumerate_end - enumerate_start).count(); + BOOST_LOG_TRIVIAL(info) << "OS enumerate " << face_names.faces.size() << " fonts " + << "(+ " << face_names.bad.size() << " can't load " + << "= " << face_names.faces.size() + face_names.bad.size() << " fonts) " + << "in " << enumerate_duration << " ms\n" << concat(face_names.bad); + }); + wxArrayString facenames = wxFontEnumerator::GetFacenames(face_names.encoding); + size_t hash = boost::hash_range(facenames.begin(), facenames.end()); + // Zero value is used as uninitialized hash + if (hash == 0) hash = 1; + // check if it is same as last time + if (face_names.hash == hash) { + // no new installed font + BOOST_LOG_TRIVIAL(info) << "Same FontNames hash, cache is used. " + << "For clear cache delete file: " << get_fontlist_cache_path().string(); + return; +} + + BOOST_LOG_TRIVIAL(info) << ((face_names.hash == 0) ? + "FontName list is generate from scratch." : + "Hash are different. Only previous bad fonts are used and set again as bad"); + face_names.hash = hash; + + // validation lambda + auto is_valid_font = [encoding = face_names.encoding, bad = face_names.bad /*copy*/](const wxString &name) { + if (name.empty()) return false; + + // vertical font start with @, we will filter it out + // Not sure if it is only in Windows so filtering is on all platforms + if (name[0] == '@') return false; + + // previously detected bad font + auto it = std::lower_bound(bad.begin(), bad.end(), name); + if (it != bad.end() && *it == name) return false; + + wxFont wx_font(wxFontInfo().FaceName(name).Encoding(encoding)); + //* + // Faster chech if wx_font is loadable but not 100% + // names could contain not loadable font + if (!WxFontUtils::can_load(wx_font)) return false; + + /*/ + // Slow copy of font files to try load font + // After this all files are loadable + auto font_file = WxFontUtils::create_font_file(wx_font); + if (font_file == nullptr) + return false; // can't create font file + // */ + return true; + }; + + face_names.faces.clear(); + face_names.bad.clear(); + face_names.faces.reserve(facenames.size()); + std::sort(facenames.begin(), facenames.end()); + for (const wxString &name : facenames) { + if (is_valid_font(name)) { + face_names.faces.push_back({name}); + }else{ + face_names.bad.push_back(name); + } + } + assert(std::is_sorted(face_names.bad.begin(), face_names.bad.end())); + face_names.has_truncated_names = false; + store(face_names); +} + +void draw_font_preview(FaceName &face, const std::string& text, Facenames &faces, const GuiCfg &cfg, bool is_visible){ + // Limit for opened font files at one moment + unsigned int &count_opened_fonts = faces.count_opened_font_files; + // Size of texture + ImVec2 size(cfg.face_name_size.x(), cfg.face_name_size.y()); + float count_cached_textures_f = static_cast(faces.count_cached_textures); + std::string state_text; + // uv0 and uv1 set to pixel 0,0 in texture + ImVec2 uv0(0.f, 0.f), uv1(1.f / size.x, 1.f / size.y / count_cached_textures_f); + if (face.is_created != nullptr) { + // not created preview + if (*face.is_created) { + // Already created preview + size_t texture_index = face.texture_index; + uv0 = ImVec2(0.f, texture_index / count_cached_textures_f); + uv1 = ImVec2(1.f, (texture_index + 1) / count_cached_textures_f); + } else { + // Not finished preview + if (is_visible) { + // when not canceled still loading + state_text = (face.cancel->load()) ? + " " + _u8L("No symbol"): + " ... " + _u8L("Loading"); + } else { + // not finished and not visible cancel job + face.is_created = nullptr; + face.cancel->store(true); + } + } + } else if (is_visible && count_opened_fonts < cfg.max_count_opened_font_files) { + ++count_opened_fonts; + face.cancel = std::make_shared(false); + face.is_created = std::make_shared(false); + + const unsigned char gray_level = 5; + // format type and level must match to texture data + const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; + const GLint level = 0; + // select next texture index + size_t texture_index = (faces.texture_index + 1) % faces.count_cached_textures; + + // set previous cach as deleted + for (FaceName &f : faces.faces) + if (f.texture_index == texture_index) { + if (f.cancel != nullptr) f.cancel->store(true); + f.is_created = nullptr; + } + + faces.texture_index = texture_index; + face.texture_index = texture_index; + + // render text to texture + FontImageData data{ + text, + face.wx_name, + faces.encoding, + faces.texture_id, + faces.texture_index, + cfg.face_name_size, + gray_level, + format, + type, + level, + &count_opened_fonts, + face.cancel, // copy + face.is_created // copy + }; + auto job = std::make_unique(std::move(data)); + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::move(job)); + } else { + // cant start new thread at this moment so wait in queue + state_text = " ... " + _u8L("In queue"); + } + + if (!state_text.empty()) { + ImGui::SameLine(cfg.face_name_texture_offset_x); + ImGui::Text("%s", state_text.c_str()); + } + + ImGui::SameLine(cfg.face_name_texture_offset_x); + ImTextureID tex_id = (void *) (intptr_t) faces.texture_id; + ImGui::Image(tex_id, size, uv0, uv1); +} + +GuiCfg create_gui_configuration() +{ + GuiCfg cfg; // initialize by default values; + + float line_height = ImGui::GetTextLineHeight(); + float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); + float space = line_height_with_spacing - line_height; + const ImGuiStyle &style = ImGui::GetStyle(); + + cfg.max_style_name_width = ImGui::CalcTextSize("Maximal font name, extended").x; + + cfg.icon_width = static_cast(std::ceil(line_height)); + // make size pair number + if (cfg.icon_width % 2 != 0) ++cfg.icon_width; + + cfg.delete_pos_x = cfg.max_style_name_width + space; + const float count_line_of_text = 3.f; + cfg.text_size = ImVec2(-FLT_MIN, line_height_with_spacing * count_line_of_text); + ImVec2 letter_m_size = ImGui::CalcTextSize("M"); + const float count_letter_M_in_input = 12.f; + cfg.input_width = letter_m_size.x * count_letter_M_in_input; + GuiCfg::Translations &tr = cfg.translations; + + // TRN - Input label. Be short as possible + // Select look of letter shape + tr.font = _u8L("Font"); + // TRN - Input label. Be short as possible + // Height of one text line - Font Ascent + tr.height = _u8L("Height"); + // TRN - Input label. Be short as possible + // Size in emboss direction + tr.depth = _u8L("Depth"); + + float max_text_width = std::max({ + ImGui::CalcTextSize(tr.font.c_str()).x, + ImGui::CalcTextSize(tr.height.c_str()).x, + ImGui::CalcTextSize(tr.depth.c_str()).x}); + cfg.indent = static_cast(cfg.icon_width); + cfg.input_offset = style.WindowPadding.x + cfg.indent + max_text_width + space; + + // TRN - Input label. Be short as possible + // Copy surface of model on surface of the embossed text + tr.use_surface = _u8L("Use surface"); + // TRN - Input label. Be short as possible + // Option to change projection on curved surface + // for each character(glyph) in text separately + tr.per_glyph = _u8L("Per glyph"); + // TRN - Input label. Be short as possible + // Align Top|Middle|Bottom and Left|Center|Right + tr.alignment = _u8L("Alignment"); + // TRN - Input label. Be short as possible + tr.char_gap = _u8L("Char gap"); + // TRN - Input label. Be short as possible + tr.line_gap = _u8L("Line gap"); + // TRN - Input label. Be short as possible + tr.boldness = _u8L("Boldness"); + + // TRN - Input label. Be short as possible + // Like Font italic + tr.skew_ration = _u8L("Skew ratio"); + + // TRN - Input label. Be short as possible + // Distance from model surface to be able + // move text as part fully into not flat surface + // move text as modifier fully out of not flat surface + tr.from_surface = _u8L("From surface"); + + // TRN - Input label. Be short as possible + // Angle between Y axis and text line direction. + tr.rotation = _u8L("Rotation"); + + // TRN - Input label. Be short as possible + // Keep vector from bottom to top of text aligned with printer Y axis + tr.keep_up = _u8L("Keep up"); + + // TRN - Input label. Be short as possible. + // Some Font file contain multiple fonts inside and + // this is numerical selector of font inside font collections + tr.collection = _u8L("Collection"); + + float max_advanced_text_width = std::max({ + ImGui::CalcTextSize(tr.use_surface.c_str()).x, + ImGui::CalcTextSize(tr.per_glyph.c_str()).x, + ImGui::CalcTextSize(tr.alignment.c_str()).x, + ImGui::CalcTextSize(tr.char_gap.c_str()).x, + ImGui::CalcTextSize(tr.line_gap.c_str()).x, + ImGui::CalcTextSize(tr.boldness.c_str()).x, + ImGui::CalcTextSize(tr.skew_ration.c_str()).x, + ImGui::CalcTextSize(tr.from_surface.c_str()).x, + ImGui::CalcTextSize(tr.rotation.c_str()).x + cfg.icon_width + 2*space, + ImGui::CalcTextSize(tr.keep_up.c_str()).x, + ImGui::CalcTextSize(tr.collection.c_str()).x }); + cfg.advanced_input_offset = max_advanced_text_width + + 3 * space + cfg.indent; + + cfg.lock_offset = cfg.advanced_input_offset - (cfg.icon_width + space); + // calculate window size + float window_title = line_height + 2*style.FramePadding.y + 2 * style.WindowTitleAlign.y; + float input_height = line_height_with_spacing + 2*style.FramePadding.y; + float tree_header = line_height_with_spacing; + float separator_height = 2 + style.FramePadding.y; + + // "Text is to object" + radio buttons + cfg.height_of_volume_type_selector = separator_height + line_height_with_spacing + input_height; + + float window_height = + window_title + // window title + cfg.text_size.y + // text field + input_height * 4 + // font name + height + depth + style selector + tree_header + // advance tree + separator_height + // presets separator line + line_height_with_spacing + // "Presets" + 2 * style.WindowPadding.y; + float window_width = cfg.input_offset + cfg.input_width + 2*style.WindowPadding.x + + 2 * (cfg.icon_width + space); + cfg.minimal_window_size = ImVec2(window_width, window_height); + + // 9 = useSurface, charGap, lineGap, bold, italic, surfDist, rotation, keepUp, textFaceToCamera + // 4 = 1px for fix each edit image of drag float + float advance_height = input_height * 10 + 9; + cfg.minimal_window_size_with_advance = + ImVec2(cfg.minimal_window_size.x, + cfg.minimal_window_size.y + advance_height); + + cfg.minimal_window_size_with_collections = + ImVec2(cfg.minimal_window_size_with_advance.x, + cfg.minimal_window_size_with_advance.y + input_height); + + int max_style_image_width = static_cast(std::round(cfg.max_style_name_width/2 - 2 * style.FramePadding.x)); + int max_style_image_height = static_cast(std::round(1.5 * input_height)); + cfg.max_style_image_size = Vec2i(max_style_image_width, max_style_image_height); + cfg.face_name_size = Vec2i(cfg.input_width, line_height_with_spacing); + cfg.face_name_texture_offset_x = cfg.face_name_size.x() + space; + return cfg; +} +} // namespace + // any existing icon filename to not influence GUI const std::string GLGizmoEmboss::M_ICON_FILENAME = "cut.svg"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp index 4efa37f..d1aff65 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp @@ -5,7 +5,7 @@ #include "GLGizmoRotate.hpp" #include "slic3r/GUI/IconManager.hpp" #include "slic3r/GUI/SurfaceDrag.hpp" -#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/I18N.hpp" // TODO: not needed #include "slic3r/GUI/TextLines.hpp" #include "slic3r/Utils/RaycastManager.hpp" #include "slic3r/Utils/EmbossStyleManager.hpp" @@ -16,7 +16,6 @@ #include "libslic3r/Emboss.hpp" #include "libslic3r/Point.hpp" -#include "libslic3r/Model.hpp" #include "libslic3r/TextConfiguration.hpp" #include @@ -33,7 +32,7 @@ namespace Slic3r::GUI { class GLGizmoEmboss : public GLGizmoBase { public: - GLGizmoEmboss(GLCanvas3D& parent); + explicit GLGizmoEmboss(GLCanvas3D& parent); //B34 void create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos, std::string m_text); @@ -43,12 +42,12 @@ public: /// /// Object part / Negative volume / Modifier /// Define position of new volume - void create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); + bool create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); /// /// Create new text without given position /// /// Object part / Negative volume / Modifier - void create_volume(ModelVolumeType volume_type); + bool create_volume(ModelVolumeType volume_type); //B34 void change_height(double height); @@ -65,6 +64,12 @@ public: /// True on success start job otherwise False bool do_mirror(size_t axis); + /// + /// Call on change inside of object conatining projected volume + /// + /// Way to stop re_emboss job + /// True on success otherwise False + static bool re_emboss(const ModelVolume &text, std::shared_ptr> job_cancel = nullptr); protected: bool on_init() override; std::string on_get_name() const override; @@ -90,10 +95,10 @@ protected: /// Propagete normaly return false. bool on_mouse(const wxMouseEvent &mouse_event) override; - bool wants_enter_leave_snapshots() const override { return true; } - std::string get_gizmo_entering_text() const override { return _u8L("Enter emboss gizmo"); } - std::string get_gizmo_leaving_text() const override { return _u8L("Leave emboss gizmo"); } - std::string get_action_snapshot_name() const override { return _u8L("Embossing actions"); } + bool wants_enter_leave_snapshots() const override; + std::string get_gizmo_entering_text() const override; + std::string get_gizmo_leaving_text() const override; + std::string get_action_snapshot_name() const override; private: void volume_transformation_changing(); void volume_transformation_changed(); @@ -111,7 +116,6 @@ private: void draw_window(); void draw_text_input(); void draw_model_type(); - void fix_transformation(const FontProp &from, const FontProp &to); void draw_style_list(); void draw_delete_style_button(); void draw_style_rename_popup(); @@ -120,8 +124,6 @@ private: void draw_style_save_as_popup(); void draw_style_add_button(); void init_font_name_texture(); - struct FaceName; - void draw_font_preview(FaceName &face, bool is_visible); void draw_font_list_line(); void draw_font_list(); void draw_height(bool use_inch); @@ -130,8 +132,6 @@ private: // call after set m_style_manager.get_style().prop.size_in_mm bool set_height(); - // call after set m_style_manager.get_style().prop.emboss - bool set_depth(); bool draw_italic_button(); bool draw_bold_button(); @@ -139,30 +139,25 @@ private: bool select_facename(const wxString& facename); - void do_translate(const Vec3d& relative_move); - void do_rotate(float relative_z_angle); - - bool rev_input_mm(const std::string &name, float &value, const float *default_value, - const std::string &undo_tooltip, float step, float step_fast, const char *format, - bool use_inch, const std::optional& scale); + template bool rev_input_mm(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, bool use_inch, const std::optional& scale) const; /// /// Reversible input float with option to restor default value /// TODO: make more general, static and move to ImGuiWrapper /// /// True when value changed otherwise FALSE. - bool rev_input(const std::string &name, float &value, const float *default_value, - const std::string &undo_tooltip, float step, float step_fast, const char *format, - ImGuiInputTextFlags flags = 0); - bool rev_checkbox(const std::string &name, bool &value, const bool* default_value, const std::string &undo_tooltip); + template bool rev_input(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, ImGuiInputTextFlags flags = 0) const; + bool rev_checkbox(const std::string &name, bool &value, const bool* default_value, const std::string &undo_tooltip) const; bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, - const std::string &undo_tooltip, int v_min, int v_max, const std::string &format, const wxString &tooltip); + const std::string &undo_tooltip, int v_min, int v_max, const std::string &format, const wxString &tooltip) const; bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, - const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip); + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip) const; bool rev_slider(const std::string &name, float &value, const float *default_value, - const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip); - template - bool revertible(const std::string &name, T &value, const T *default_value, const std::string &undo_tooltip, float undo_offset, Draw draw); + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip) const; + template bool revertible(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, float undo_offset, Draw draw) const; bool m_should_set_minimal_windows_size = false; void set_minimal_window_size(bool is_advance_edit_style); @@ -174,71 +169,13 @@ private: void on_mouse_change_selection(const wxMouseEvent &mouse_event); // When open text loaded from .3mf it could be written with unknown font - bool m_is_unknown_font; + bool m_is_unknown_font = false; void create_notification_not_valid_font(const TextConfiguration& tc); void create_notification_not_valid_font(const std::string& text); void remove_notification_not_valid_font(); - // This configs holds GUI layout size given by translated texts. - // etc. When language changes, GUI is recreated and this class constructed again, - // so the change takes effect. (info by GLGizmoFdmSupports.hpp) - struct GuiCfg - { - // Detect invalid config values when change monitor DPI - double screen_scale; - float main_toolbar_height; - - // Zero means it is calculated in init function - ImVec2 minimal_window_size = ImVec2(0, 0); - ImVec2 minimal_window_size_with_advance = ImVec2(0, 0); - ImVec2 minimal_window_size_with_collections = ImVec2(0, 0); - float height_of_volume_type_selector = 0.f; - float input_width = 0.f; - float delete_pos_x = 0.f; - float max_style_name_width = 0.f; - unsigned int icon_width = 0; - - // maximal width and height of style image - Vec2i max_style_image_size = Vec2i(0, 0); - - float indent = 0.f; - float input_offset = 0.f; - float advanced_input_offset = 0.f; - - float lock_offset = 0.f; - - ImVec2 text_size; - - // maximal size of face name image - Vec2i face_name_size = Vec2i(100, 0); - float face_name_texture_offset_x = 0.f; - - // maximal texture generate jobs running at once - unsigned int max_count_opened_font_files = 10; - - // Only translations needed for calc GUI size - struct Translations - { - std::string font; - std::string height; - std::string depth; - - // advanced - std::string use_surface; - std::string per_glyph; - std::string alignment; - std::string char_gap; - std::string line_gap; - std::string boldness; - std::string skew_ration; - std::string from_surface; - std::string rotation; - std::string collection; - }; - Translations translations; - }; - std::optional m_gui_cfg; - static GuiCfg create_gui_configuration(); + struct GuiCfg; + std::unique_ptr m_gui_cfg; // Is open tree with advanced options bool m_is_advanced_edit_style = false; @@ -252,62 +189,9 @@ private: // Keep information about stored styles and loaded actual style to compare with Emboss::StyleManager m_style_manager; - struct FaceName{ - wxString wx_name; - std::string name_truncated = ""; - size_t texture_index = 0; - // State for generation of texture - // when start generate create share pointers - std::shared_ptr> cancel = nullptr; - // R/W only on main thread - finalize of job - std::shared_ptr is_created = nullptr; - }; - - // Keep sorted list of loadable face names - struct Facenames - { - // flag to keep need of enumeration fonts from OS - // false .. wants new enumeration check by Hash - // true .. already enumerated(During opened combo box) - bool is_init = false; - - bool has_truncated_names = false; - - // data of can_load() faces - std::vector faces = {}; - // Sorter set of Non valid face names in OS - std::vector bad = {}; - - // Configuration of font encoding - static constexpr wxFontEncoding encoding = wxFontEncoding::wxFONTENCODING_SYSTEM; - - // Identify if preview texture exists - GLuint texture_id = 0; - - // protection for open too much font files together - // Gtk:ERROR:../../../../gtk/gtkiconhelper.c:494:ensure_surface_for_gicon: assertion failed (error == NULL): Failed to load /usr/share/icons/Yaru/48x48/status/image-missing.png: Error opening file /usr/share/icons/Yaru/48x48/status/image-missing.png: Too many open files (g-io-error-quark, 31) - // This variable must exist until no CreateFontImageJob is running - unsigned int count_opened_font_files = 0; - - // Configuration for texture height - const int count_cached_textures = 32; - - // index for new generated texture index(must be lower than count_cached_textures) - size_t texture_index = 0; - - // hash created from enumerated font from OS - // check when new font was installed - size_t hash = 0; - - // filtration pattern - std::string search = ""; - std::vector hide; // result of filtration - } m_face_names; - static bool store(const Facenames &facenames); - static bool load(Facenames &facenames); - - static void init_face_names(Facenames &facenames); - static void init_truncated_names(Facenames &face_names, float max_width); + // pImpl to hide implementation of FaceNames to .cpp file + struct Facenames; // forward declaration + std::unique_ptr m_face_names; // Text to emboss std::string m_text; // Sequence of Unicode UTF8 symbols @@ -317,7 +201,7 @@ private: // current selected volume // NOTE: Be carefull could be uninitialized (removed from Model) - ModelVolume *m_volume; + ModelVolume *m_volume = nullptr; // When work with undo redo stack there could be situation that // m_volume point to unexisting volume so One need also objectID @@ -327,7 +211,7 @@ private: bool m_text_contain_unknown_glyph = false; // cancel for previous update of volume to cancel finalize part - std::shared_ptr> m_job_cancel; + std::shared_ptr> m_job_cancel = nullptr; // Keep information about curvature of text line around surface TextLinesModel m_text_lines; @@ -341,8 +225,8 @@ private: // Keep data about dragging only during drag&drop std::optional m_surface_drag; - // TODO: it should be accessible by other gizmo too. - // May be move to plater? + // Keep old scene triangle data in AABB trees, + // all the time it need actualize before use. RaycastManager m_raycast_manager; // For text on scaled objects diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 7774c76..9b9e033 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -57,7 +57,7 @@ bool GLGizmoMmuSegmentation::on_is_activable() const return GLGizmoPainterBase::on_is_activable() && wxGetApp().extruders_edited_cnt() > 1; } -static std::vector get_extruders_colors() +std::vector get_extruders_colors() { std::vector colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); std::vector ret; @@ -524,6 +524,7 @@ void GLGizmoMmuSegmentation::update_model_object() const void GLGizmoMmuSegmentation::init_model_triangle_selectors() { + const int extruders_count = wxGetApp().extruders_edited_cnt(); const ModelObject *mo = m_c->selection_info()->model_object(); m_triangle_selectors.clear(); @@ -538,8 +539,8 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors() // This mesh does not account for the possible Z up SLA offset. const TriangleMesh *mesh = &mv->mesh(); - int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0; - m_triangle_selectors.emplace_back(std::make_unique(*mesh, m_modified_extruders_colors, m_original_extruders_colors[size_t(extruder_idx)])); + size_t extruder_idx = get_extruder_color_idx(*mv, extruders_count); + m_triangle_selectors.emplace_back(std::make_unique(*mesh, m_modified_extruders_colors, m_original_extruders_colors[extruder_idx])); // Reset of TriangleSelector is done inside TriangleSelectorMmGUI's constructor, so we don't need it to perform it again in deserialize(). m_triangle_selectors.back()->deserialize(mv->mmu_segmentation_facets.get_data(), false); m_triangle_selectors.back()->request_update_render_data(); @@ -588,12 +589,6 @@ void TriangleSelectorMmGui::render(ImGuiWrapper* imgui, const Transform3d& matri if (!shader) return; assert(shader->get_name() == "mm_gouraud"); - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d& view_matrix = camera.get_view_matrix(); - shader->set_uniform("view_model_matrix", view_matrix * matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader->set_uniform("view_normal_matrix", view_normal_matrix); for (size_t color_idx = 0; color_idx < m_gizmo_scene.triangle_indices.size(); ++color_idx) { if (m_gizmo_scene.has_VBOs(color_idx)) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 21138c5..1d53aed 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -66,8 +66,9 @@ public: class TriangleSelectorMmGui : public TriangleSelectorGUI { public: + TriangleSelectorMmGui() = delete; // Plus 1 in the initialization of m_gizmo_scene is because the first position is allocated for non-painted triangles, and the indices above colors.size() are allocated for seed fill. - TriangleSelectorMmGui(const TriangleMesh& mesh, const std::vector& colors, const ColorRGBA& default_volume_color) + explicit TriangleSelectorMmGui(const TriangleMesh& mesh, const std::vector& colors, const ColorRGBA& default_volume_color) : TriangleSelectorGUI(mesh), m_colors(colors), m_default_volume_color(default_volume_color), m_gizmo_scene(2 * (colors.size() + 1)) {} ~TriangleSelectorMmGui() override = default; @@ -148,6 +149,15 @@ private: std::map m_desc; }; +std::vector get_extruders_colors(); + +inline size_t get_extruder_color_idx(const ModelVolume &model_volume, const int extruders_count) +{ + if (const int extruder_id = model_volume.extruder_id(); extruder_id <= 0 || extruder_id > extruders_count) + return 0; + else + return extruder_id - 1; +} } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 677f258..be7ddf3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -200,10 +200,12 @@ void GLGizmoRotate::init_data_from_selection(const Selection& selection) const auto [box, box_trafo] = m_force_local_coordinate ? selection.get_bounding_box_in_reference_system(ECoordinatesType::Local) : selection.get_bounding_box_in_current_reference_system(); m_bounding_box = box; - m_center = box_trafo.translation(); + const std::pair sphere = selection.get_bounding_sphere(); + m_center = sphere.first; + m_radius = Offset + sphere.second; m_orient_matrix = box_trafo; - m_radius = Offset + m_bounding_box.radius(); + m_orient_matrix.translation() = m_center; m_snap_coarse_in_radius = m_radius / 3.0f; m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; m_snap_fine_in_radius = m_radius; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp new file mode 100644 index 0000000..96f68e2 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp @@ -0,0 +1,2239 @@ +#include "GLGizmoSVG.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#include "slic3r/GUI/MainFrame.hpp" // to update title when add text +#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/Jobs/EmbossJob.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +#include "libslic3r/Point.hpp" +#include "libslic3r/SVG.hpp" // debug store +#include "libslic3r/Geometry.hpp" // covex hull 2d +#include "libslic3r/Timer.hpp" // covex hull 2d +#include "libslic3r/Emboss.hpp" // heal_shape + +#include "libslic3r/NSVGUtils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/ClipperUtils.hpp" // union_ex + +#include "imgui/imgui_stdlib.h" // using std::string for inputs +#include "nanosvg/nanosvg.h" // load SVG file + +#include // detection of change DPI +#include + +#include +#include // measure enumeration of fonts +#include // save for svg +#include +#include + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + +GLGizmoSVG::GLGizmoSVG(GLCanvas3D &parent) + : GLGizmoBase(parent, M_ICON_FILENAME, -3) + , m_gui_cfg(nullptr) + , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) +{ + m_rotate_gizmo.set_group_id(0); + m_rotate_gizmo.set_force_local_coordinate(true); +} + +// Private functions to create emboss volume +namespace{ + +// Variable keep limits for variables +const struct Limits +{ + MinMax depth{0.01, 1e4}; // in mm + MinMax size{0.01f, 1e4f}; // in mm (width + height) + MinMax ui_size{5.f, 100.f}; // in mm (width + height) - only slider values + MinMax ui_size_in{.1f, 4.f};// in inches (width + height) - only slider values + MinMax relative_scale_ratio{1e-5, 1e4}; // change size + // distance text object from surface + MinMax angle{-180.f, 180.f}; // in degrees +} limits; + +// Store path to directory with svg for import and export svg's +wxString last_used_directory = wxEmptyString; + +/// +/// Open file dialog with svg files +/// +/// File path to svg +std::string choose_svg_file(); + +constexpr double get_tesselation_tolerance(double scale){ + constexpr double tesselation_tolerance_in_mm = .1; //8e-2; + constexpr double tesselation_tolerance_scaled = (tesselation_tolerance_in_mm*tesselation_tolerance_in_mm) / SCALING_FACTOR / SCALING_FACTOR; + return tesselation_tolerance_scaled / scale / scale; +} + +/// +/// Let user to choose file with (S)calable (V)ector (G)raphics - SVG. +/// Than let select contour +/// +/// SVG file path, when empty promt user to select one +/// EmbossShape to create +EmbossShape select_shape(std::string_view filepath = "", double tesselation_tolerance_in_mm = get_tesselation_tolerance(1.)); + +/// +/// Create new embos data +/// +/// Cancel for previous job +/// To distiquish whether it is outside of model +/// SVG file path +/// Base data for emboss SVG +DataBasePtr create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath = ""); + +/// +/// Separate file name from file path. +/// String after last delimiter and before last point +/// +/// path return by file dialog +/// File name without directory path +std::string get_file_name(const std::string &file_path); + +/// +/// Create volume name from shape information +/// +/// File path +/// Name for volume +std::string volume_name(const EmbossShape& shape); + +/// +/// Create input for volume creation +/// +/// parent of gizmo +/// Keep scene +/// Type of volume to be created +/// Params +CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager &raycaster, ModelVolumeType volume_type); + +enum class IconType : unsigned { + reset_value, + reset_value_hover, + refresh, + refresh_hover, + change_file, + bake, + bake_inactive, + save, + exclamation, + lock, + lock_hover, + unlock, + unlock_hover, + reflection_x, + reflection_x_hover, + reflection_y, + reflection_y_hover, + // automatic calc of icon's count + _count +}; +// Do not forgot add loading of file in funtion: +// IconManager::Icons init_icons( + +const IconManager::Icon &get_icon(const IconManager::Icons &icons, IconType type) { + return *icons[static_cast(type)]; } + +// This configs holds GUI layout size given by translated texts. +// etc. When language changes, GUI is recreated and this class constructed again, +// so the change takes effect. (info by GLGizmoFdmSupports.hpp) +struct GuiCfg +{ + // Detect invalid config values when change monitor DPI + double screen_scale = -1.; + float main_toolbar_height = -1.f; + + // Define bigger size(width or height) + unsigned texture_max_size_px = 256; + + // Zero means it is calculated in init function + ImVec2 window_size = ImVec2(0, 0); + ImVec2 window_size_for_object = ImVec2(0, 0); // without change type + + float input_width = 0.f; + float input_offset = 0.f; + + float icon_width = 0.f; + + // offset for checbox for lock up vector + float lock_offset = 0.f; + // Only translations needed for calc GUI size + struct Translations + { + std::string depth; + std::string size; + std::string use_surface; + std::string rotation; + std::string distance; // from surface + std::string mirror; + }; + Translations translations; +}; +GuiCfg create_gui_configuration(); + +} // namespace + +// use private definition +struct GLGizmoSVG::GuiCfg: public ::GuiCfg{}; + +bool GLGizmoSVG::create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type); + if (!base) return false; // Uninterpretable svg + return start_create_volume(input, std::move(base), mouse_pos); +} + +bool GLGizmoSVG::create_volume(ModelVolumeType volume_type) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel,volume_type); + if (!base) return false; // Uninterpretable svg + return start_create_volume_without_position(input, std::move(base)); +} + +bool GLGizmoSVG::create_volume(std::string_view svg_file, ModelVolumeType volume_type){ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); + if (!base) return false; // Uninterpretable svg + return start_create_volume_without_position(input, std::move(base)); +} + +bool GLGizmoSVG::create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); + if (!base) return false; // Uninterpretable svg + return start_create_volume(input, std::move(base), mouse_pos); +} + +bool GLGizmoSVG::is_svg(const ModelVolume &volume) { + return volume.emboss_shape.has_value(); +} + +bool GLGizmoSVG::is_svg_object(const ModelVolume &volume) { + if (!volume.emboss_shape.has_value()) return false; + if (volume.type() != ModelVolumeType::MODEL_PART) return false; + for (const ModelVolume *v : volume.get_object()->volumes) { + if (v->id() == volume.id()) continue; + if (v->type() == ModelVolumeType::MODEL_PART) return false; + } + return true; +} + +bool GLGizmoSVG::on_mouse_for_rotation(const wxMouseEvent &mouse_event) +{ + if (mouse_event.Moving()) return false; + + bool used = use_grabbers(mouse_event); + if (!m_dragging) return used; + + if (mouse_event.Dragging()) + dragging_rotate_gizmo(m_rotate_gizmo.get_angle(), m_angle, m_rotate_start_angle, m_parent.get_selection()); + + return used; +} + +bool GLGizmoSVG::on_mouse_for_translate(const wxMouseEvent &mouse_event) +{ + // exist selected volume? + if (m_volume == nullptr) + return false; + + auto up_limit = m_keep_up ? std::optional(UP_LIMIT) : std::optional{}; + const Camera &camera = wxGetApp().plater()->get_camera(); + + bool was_dragging = m_surface_drag.has_value(); + bool res = on_mouse_surface_drag(mouse_event, camera, m_surface_drag, m_parent, m_raycast_manager, up_limit); + bool is_dragging = m_surface_drag.has_value(); + + // End with surface dragging? + if (was_dragging && !is_dragging) { + // Update surface by new position + if (m_volume->emboss_shape->projection.use_surface) + process(); + + // TODO: Remove it when it will be stable + // Distance should not change during dragging + const GLVolume *gl_volume = m_parent.get_selection().get_first_volume(); + m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + // Show correct value of height & depth inside of inputs + calculate_scale(); + } + + // Start with dragging + else if (!was_dragging && is_dragging) { + // Cancel job to prevent interuption of dragging (duplicit result) + if (m_job_cancel != nullptr) + m_job_cancel->store(true); + } + + // during drag + else if (was_dragging && is_dragging) { + // update scale of selected volume --> should be approx the same + calculate_scale(); + + // Recalculate angle for GUI + if (!m_keep_up) + m_angle = calc_angle(m_parent.get_selection()); + } + return res; +} + +void GLGizmoSVG::volume_transformation_changed() +{ + if (m_volume == nullptr || + !m_volume->emboss_shape.has_value()) { + assert(false); + return; + } + + if (!m_keep_up) { + // update current style + m_angle = calc_angle(m_parent.get_selection()); + } else { + // angle should be the same + assert(is_approx(m_angle, calc_angle(m_parent.get_selection()))); + } + + // Update surface by new position + if (m_volume->emboss_shape->projection.use_surface) { + process(); + } else { + // inform slicing process that model changed + // SLA supports, processing + // ensure on bed + wxGetApp().plater()->changed_object(*m_volume->get_object()); + } + + // Show correct value of height & depth inside of inputs + calculate_scale(); +} + +bool GLGizmoSVG::on_mouse(const wxMouseEvent &mouse_event) +{ + // not selected volume + if (m_volume == nullptr || + get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr || + !m_volume->emboss_shape.has_value()) return false; + + if (on_mouse_for_rotation(mouse_event)) return true; + if (on_mouse_for_translate(mouse_event)) return true; + + return false; +} + +bool GLGizmoSVG::wants_enter_leave_snapshots() const { return true; } +std::string GLGizmoSVG::get_gizmo_entering_text() const { return _u8L("Enter SVG gizmo"); } +std::string GLGizmoSVG::get_gizmo_leaving_text() const { return _u8L("Leave SVG gizmo"); } +std::string GLGizmoSVG::get_action_snapshot_name() const { return _u8L("SVG actions"); } + +bool GLGizmoSVG::on_init() +{ + m_rotate_gizmo.init(); + ColorRGBA gray_color(.6f, .6f, .6f, .3f); + m_rotate_gizmo.set_highlight_color(gray_color); + // Set rotation gizmo upwardrotate + m_rotate_gizmo.set_angle(PI / 2); + return true; +} + +std::string GLGizmoSVG::on_get_name() const { return _u8L("SVG"); } + +void GLGizmoSVG::on_render() { + if (const Selection &selection = m_parent.get_selection(); + selection.volumes_count() != 1 || // only one selected volume + m_volume == nullptr || // already selected volume in gizmo + get_model_volume(m_volume_id, selection.get_model()->objects) == nullptr) // still exist model + return; + + bool is_surface_dragging = m_surface_drag.has_value(); + bool is_parent_dragging = m_parent.is_mouse_dragging(); + // Do NOT render rotation grabbers when dragging object + bool is_rotate_by_grabbers = m_dragging; + if (is_rotate_by_grabbers || + (!is_surface_dragging && !is_parent_dragging)) { + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + m_rotate_gizmo.render(); + } +} + +void GLGizmoSVG::on_register_raycasters_for_picking(){ + m_rotate_gizmo.register_raycasters_for_picking(); +} +void GLGizmoSVG::on_unregister_raycasters_for_picking(){ + m_rotate_gizmo.unregister_raycasters_for_picking(); +} + +namespace{ +IconManager::Icons init_icons(IconManager &mng, const GuiCfg &cfg) +{ + mng.release(); + + ImVec2 size(cfg.icon_width, cfg.icon_width); + // icon order has to match the enum IconType + IconManager::InitTypes init_types{ + {"undo.svg", size, IconManager::RasterType::white_only_data}, // reset_value + {"undo.svg", size, IconManager::RasterType::color}, // reset_value_hover + {"refresh.svg", size, IconManager::RasterType::white_only_data}, // refresh + {"refresh.svg", size, IconManager::RasterType::color}, // refresh_hovered + {"open.svg", size, IconManager::RasterType::color}, // changhe_file + {"burn.svg", size, IconManager::RasterType::color}, // bake + {"burn.svg", size, IconManager::RasterType::gray_only_data}, // bake_inactive + {"save.svg", size, IconManager::RasterType::color}, // save + {"exclamation.svg", size, IconManager::RasterType::color}, // exclamation + {"lock_closed.svg", size, IconManager::RasterType::white_only_data}, // lock + {"lock_open_f.svg", size, IconManager::RasterType::white_only_data}, // lock_hovered + {"lock_open.svg", size, IconManager::RasterType::white_only_data}, // unlock + {"lock_closed_f.svg",size, IconManager::RasterType::white_only_data}, // unlock_hovered + {"reflection_x.svg", size, IconManager::RasterType::white_only_data}, // reflection_x + {"reflection_x.svg", size, IconManager::RasterType::color}, // reflection_x_hovered + {"reflection_y.svg", size, IconManager::RasterType::white_only_data}, // reflection_y + {"reflection_y.svg", size, IconManager::RasterType::color}, // reflection_y_hovered + }; + + assert(init_types.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/icons/"; + for (IconManager::InitType &init_type : init_types) + init_type.filepath = path + init_type.filepath; + + return mng.init(init_types); + + //IconManager::VIcons vicons = mng.init(init_types); + // + //// flatten icons + //IconManager::Icons icons; + //icons.reserve(vicons.size()); + //for (IconManager::Icons &i : vicons) + // icons.push_back(i.front()); + //return icons; +} + +bool reset_button(const IconManager::Icons &icons) +{ + float reset_offset = ImGui::GetStyle().FramePadding.x; + ImGui::SameLine(reset_offset); + + // from GLGizmoCut + //std::string label_id = "neco"; + //std::string btn_label; + //btn_label += ImGui::RevertButton; + //return ImGui::Button((btn_label + "##" + label_id).c_str()); + + return clickable(get_icon(icons, IconType::reset_value), get_icon(icons, IconType::reset_value_hover)); +} + +} // namespace + +void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) +{ + set_volume_by_selection(); + + // Configuration creation + double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); + float main_toolbar_height = m_parent.get_main_toolbar_height(); + if (m_gui_cfg == nullptr || // Exist configuration - first run + m_gui_cfg->screen_scale != screen_scale || // change of DPI + m_gui_cfg->main_toolbar_height != main_toolbar_height // change size of view port + ) { + // Create cache for gui offsets + ::GuiCfg cfg = create_gui_configuration(); + cfg.screen_scale = screen_scale; + cfg.main_toolbar_height = main_toolbar_height; + + GuiCfg gui_cfg{std::move(cfg)}; + m_gui_cfg = std::make_unique(std::move(gui_cfg)); + + // set position near toolbar + m_set_window_offset = ImVec2(-1.f, -1.f); + + m_icons = init_icons(m_icon_manager, *m_gui_cfg); // need regeneration when change resolution(move between monitors) + } + + const ImVec2 &window_size = m_volume->is_the_only_one_part() ? + m_gui_cfg->window_size_for_object : m_gui_cfg->window_size; + ImGui::SetNextWindowSize(window_size); + // Draw origin position of text during dragging + if (m_surface_drag.has_value()) { + ImVec2 mouse_pos = ImGui::GetMousePos(); + ImVec2 center( + mouse_pos.x + m_surface_drag->mouse_offset.x(), + mouse_pos.y + m_surface_drag->mouse_offset.y()); + ImU32 color = ImGui::GetColorU32( + m_surface_drag->exist_hit ? + ImVec4(1.f, 1.f, 1.f, .75f) : // transparent white + ImVec4(1.f, .3f, .3f, .75f) + ); // Warning color + const float radius = 16.f; + ImGuiWrapper::draw_cross_hair(center, radius, color); + } + + // check if is set window offset + if (m_set_window_offset.has_value()) { + if (m_set_window_offset->y < 0) + // position near toolbar + m_set_window_offset = ImVec2(x, std::min(y, bottom_limit - window_size.y)); + + ImGui::SetNextWindowPos(*m_set_window_offset, ImGuiCond_Always); + m_set_window_offset.reset(); + } + + bool is_opened = true; + constexpr ImGuiWindowFlags flag = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; + if (ImGui::Begin(on_get_name().c_str(), &is_opened, flag)) + draw_window(); + + ImGui::End(); + if (!is_opened) + close(); +} + +void GLGizmoSVG::on_set_state() +{ + // enable / disable bed from picking + // Rotation gizmo must work through bed + m_parent.set_raycaster_gizmos_on_top(GLGizmoBase::m_state == GLGizmoBase::On); + + m_rotate_gizmo.set_state(GLGizmoBase::m_state); + + // Closing gizmo. e.g. selecting another one + if (GLGizmoBase::m_state == GLGizmoBase::Off) { + reset_volume(); + } else if (GLGizmoBase::m_state == GLGizmoBase::On) { + // Try(when exist) set text configuration by volume + set_volume_by_selection(); + + m_set_window_offset = (m_gui_cfg != nullptr) ? + ImGuiWrapper::change_window_position(on_get_name().c_str(), false) : ImVec2(-1, -1); + } +} + +void GLGizmoSVG::data_changed(bool is_serializing) { + set_volume_by_selection(); + if (!is_serializing && m_volume == nullptr) + close(); +} + +void GLGizmoSVG::on_start_dragging() { m_rotate_gizmo.start_dragging(); } +void GLGizmoSVG::on_stop_dragging() +{ + m_rotate_gizmo.stop_dragging(); + + // TODO: when start second rotatiton previous rotation rotate draggers + // This is fast fix for second try to rotate + // When fixing, move grabber above text (not on side) + m_rotate_gizmo.set_angle(PI/2); + + // apply rotation + // TRN This is an item label in the undo-redo stack. + m_parent.do_rotate(L("SVG-Rotate")); + m_rotate_start_angle.reset(); + volume_transformation_changed(); + + // recalculate for surface cut + if (m_volume != nullptr && + m_volume->emboss_shape.has_value() && + m_volume->emboss_shape->projection.use_surface) + process(); +} +void GLGizmoSVG::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } + +#include "slic3r/GUI/BitmapCache.hpp" +#include "nanosvg/nanosvgrast.h" +#include "libslic3r/AABBTreeLines.hpp" // aabb lines for draw filled expolygon + +namespace{ + +// inspired by Xiaolin Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm +// Draw inner part of polygon CCW line as full brightness(edge of expolygon) +void wu_draw_line_side(Linef line, + const std::function& plot) { + auto ipart = [](float x) -> int {return static_cast(std::floor(x));}; + auto round = [](float x) -> float {return std::round(x);}; + auto fpart = [](float x) -> float {return x - std::floor(x);}; + auto rfpart = [=](float x) -> float {return 1 - fpart(x);}; + + Vec2d d = line.b - line.a; + const bool steep = abs(d.y()) > abs(d.x()); + bool is_full; // identify full brightness pixel + if (steep) { + is_full = d.y() >= 0; + std::swap(line.a.x(), line.a.y()); + std::swap(line.b.x(), line.b.y()); + std::swap(d.x(), d.y()); + }else + is_full = d.x() < 0; // opposit direction of y + + if (line.a.x() > line.b.x()) { + std::swap(line.a.x(), line.b.x()); + std::swap(line.a.y(), line.b.y()); + d *= -1; + } + const float gradient = (d.x() == 0) ? 1. : d.y() / d.x(); + + int xpx11; + float intery; + { + const float xend = round(line.a.x()); + const float yend = line.a.y() + gradient * (xend - line.a.x()); + const float xgap = rfpart(line.a.x() + 0.5f); + xpx11 = int(xend); + const int ypx11 = ipart(yend); + if (steep) { + plot(ypx11, xpx11, is_full? 1.f : (rfpart(yend) * xgap)); + plot(ypx11 + 1, xpx11, !is_full? 1.f : ( fpart(yend) * xgap)); + } else { + plot(xpx11, ypx11, is_full? 1.f : (rfpart(yend) * xgap)); + plot(xpx11, ypx11 + 1,!is_full? 1.f : ( fpart(yend) * xgap)); + } + intery = yend + gradient; + } + + int xpx12; + { + const float xend = round(line.b.x()); + const float yend = line.b.y() + gradient * (xend - line.b.x()); + const float xgap = rfpart(line.b.x() + 0.5f); + xpx12 = int(xend); + const int ypx12 = ipart(yend); + if (steep) { + plot(ypx12, xpx12, is_full? 1.f : (rfpart(yend) * xgap)); + plot(ypx12 + 1, xpx12, !is_full? 1.f : ( fpart(yend) * xgap)); + } else { + plot(xpx12, ypx12, is_full? 1.f : (rfpart(yend) * xgap)); + plot(xpx12, ypx12 + 1, !is_full? 1.f : ( fpart(yend) * xgap)); + } + } + + if (steep) { + if (is_full){ + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, 1.f); + plot(ipart(intery) + 1, x, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, rfpart(intery)); + plot(ipart(intery) + 1, x, 1.f ); + intery += gradient; + } + } + } else { + if (is_full){ + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), 1.f); + plot(x, ipart(intery) + 1, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), rfpart(intery)); + plot(x, ipart(intery) + 1, 1.f); + intery += gradient; + } + } + } +} + +#ifdef MORE_DRAWING +// Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm +void wu_draw_line(Linef line, + const std::function& plot) { + auto ipart = [](float x) -> int {return int(std::floor(x));}; + auto round = [](float x) -> float {return std::round(x);}; + auto fpart = [](float x) -> float {return x - std::floor(x);}; + auto rfpart = [=](float x) -> float {return 1 - fpart(x);}; + + Vec2d d = line.b - line.a; + const bool steep = abs(d.y()) > abs(d.x()); + if (steep) { + std::swap(line.a.x(), line.a.y()); + std::swap(line.b.x(), line.b.y()); + } + if (line.a.x() > line.b.x()) { + std::swap(line.a.x(), line.b.x()); + std::swap(line.a.y(), line.b.y()); + } + d = line.b - line.a; + const float gradient = (d.x() == 0) ? 1 : d.y() / d.x(); + + int xpx11; + float intery; + { + const float xend = round(line.a.x()); + const float yend = line.a.y() + gradient * (xend - line.a.x()); + const float xgap = rfpart(line.a.x() + 0.5); + xpx11 = int(xend); + const int ypx11 = ipart(yend); + if (steep) { + plot(ypx11, xpx11, rfpart(yend) * xgap); + plot(ypx11 + 1, xpx11, fpart(yend) * xgap); + } else { + plot(xpx11, ypx11, rfpart(yend) * xgap); + plot(xpx11, ypx11 + 1, fpart(yend) * xgap); + } + intery = yend + gradient; + } + + int xpx12; + { + const float xend = round(line.b.x()); + const float yend = line.b.y() + gradient * (xend - line.b.x()); + const float xgap = rfpart(line.b.x() + 0.5); + xpx12 = int(xend); + const int ypx12 = ipart(yend); + if (steep) { + plot(ypx12, xpx12, rfpart(yend) * xgap); + plot(ypx12 + 1, xpx12, fpart(yend) * xgap); + } else { + plot(xpx12, ypx12, rfpart(yend) * xgap); + plot(xpx12, ypx12 + 1, fpart(yend) * xgap); + } + } + + if (steep) { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, rfpart(intery)); + plot(ipart(intery) + 1, x, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), rfpart(intery)); + plot(x, ipart(intery) + 1, fpart(intery)); + intery += gradient; + } + } +} + +void draw(const ExPolygonsWithIds &shapes_with_ids, unsigned max_size) +{ + ImVec2 actual_pos = ImGui::GetCursorPos(); + // draw shapes + BoundingBox bb; + for (const ExPolygonsWithId &shape : shapes_with_ids) + bb.merge(get_extents(shape.expoly)); + + Point bb_size = bb.size(); + double scale = max_size / (double) std::max(bb_size.x(), bb_size.y()); + ImVec2 win_offset = ImGui::GetWindowPos(); + Point offset(win_offset.x + actual_pos.x, win_offset.y + actual_pos.y); + offset += bb_size / 2 * scale; + auto draw_polygon = [&scale, offset](Slic3r::Polygon p) { + p.scale(scale, -scale); // Y mirror + p.translate(offset); + ImGuiWrapper::draw(p); + }; + + for (const ExPolygonsWithId &shape : shapes_with_ids) { + for (const ExPolygon &expoly : shape.expoly) { + draw_polygon(expoly.contour); + for (const Slic3r::Polygon &hole : expoly.holes) + draw_polygon(hole); + } + } +} + +#endif // MORE_DRAWING + +template // N .. count of channels per pixel +void draw_side_outline(const ExPolygons &shape, const std::array &color, std::vector &data, size_t data_width, double scale) +{ + int count_lines = data.size() / (N * data_width); + size_t data_line = N * data_width; + auto get_offset = [count_lines, data_line](int x, int y) { + // NOTE: y has opposit direction in texture + return (count_lines - y - 1) * data_line + x * N; + }; + + // overlap color + auto draw = [&data, data_width, count_lines, get_offset, &color](int x, int y, float brightess) { + if (x < 0 || y < 0 || static_cast(x) >= data_width || y >= count_lines) + return; // out of image + size_t offset = get_offset(x, y); + bool change_color = false; + for (size_t i = 0; i < N - 1; ++i) { + if(data[offset + i] != color[i]){ + data[offset + i] = color[i]; + change_color = true; + } + } + + unsigned char &alpha = data[offset + N - 1]; + if (alpha == 0 || change_color){ + alpha = static_cast(std::round(brightess * 255)); + } else if (alpha != 255){ + alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); + } + }; + + BoundingBox bb_unscaled = get_extents(shape); + Linesf lines = to_linesf(shape); + BoundingBoxf bb(bb_unscaled.min.cast(), bb_unscaled.max.cast()); + + // scale lines to pixels + if (!is_approx(scale, 1.)) { + for (Linef &line : lines) { + line.a *= scale; + line.b *= scale; + } + bb.min *= scale; + bb.max *= scale; + } + + for (const Linef &line : lines) + wu_draw_line_side(line, draw); +} + +/// +/// Draw filled ExPolygon into data +/// line by line inspired by: http://alienryderflex.com/polygon_fill/ +/// +/// Count channels for one pixel(RGBA = 4) +/// Shape to draw +/// Color of shape contain count of channels(N) +/// Image(2d) stored in 1d array +/// Count of pixel on one line(size in data = N x data_width) +/// Shape scale for conversion to pixels +template // N .. count of channels per pixel +void draw_filled(const ExPolygons &shape, const std::array& color, std::vector &data, size_t data_width, double scale){ + assert(data.size() % N == 0); + assert(data.size() % data_width == 0); + assert((data.size() % (N*data_width)) == 0); + + BoundingBox bb_unscaled = get_extents(shape); + + Linesf lines = to_linesf(shape); + BoundingBoxf bb( + bb_unscaled.min.cast(), + bb_unscaled.max.cast()); + + // scale lines to pixels + if (!is_approx(scale, 1.)) { + for (Linef &line : lines) { + line.a *= scale; + line.b *= scale; + } + bb.min *= scale; + bb.max *= scale; + } + + int count_lines = data.size() / (N * data_width); + size_t data_line = N * data_width; + auto get_offset = [count_lines, data_line](int x, int y) { + // NOTE: y has opposit direction in texture + return (count_lines - y - 1) * data_line + x * N; + }; + auto set_color = [&data, &color, get_offset](int x, int y) { + size_t offset = get_offset(x, y); + if (data[offset + N - 1] != 0) + return; // already setted by line + for (unsigned i = 0; i < N; ++i) + data[offset + i] = color[i]; + }; + + // anti aliased drawing of lines + auto draw = [&data, width = static_cast(data_width), count_lines, get_offset, &color](int x, int y, float brightess) { + if (x < 0 || y < 0 || x >= width || y >= count_lines) + return; // out of image + size_t offset = get_offset(x, y); + unsigned char &alpha = data[offset + N - 1]; + if (alpha == 0){ + alpha = static_cast(std::round(brightess * 255)); + for (size_t i = 0; i < N-1; ++i) + data[offset + i] = color[i]; + } else if (alpha != 255){ + alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); + } + }; + + for (const Linef& line: lines) + wu_draw_line_side(line, draw); + + auto tree = Slic3r::AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + + // range for intersection line + double x1 = bb.min.x() - 1.f; + double x2 = bb.max.x() + 1.f; + + int max_y = std::min(count_lines, static_cast(std::round(bb.max.y()))); + for (int y = std::max(0, static_cast(std::round(bb.min.y()))); y < max_y; ++y){ + double y_f = y + .5; // 0.5 ... intersection in center of pixel of pixel + Linef line(Vec2d(x1, y_f), Vec2d(x2, y_f)); + using Intersection = std::pair; + using Intersections = std::vector; + // sorted .. false + // + Intersections intersections = Slic3r::AABBTreeLines::get_intersections_with_line(lines, tree, line); + if (intersections.empty()) + continue; + + assert((intersections.size() % 2) == 0); + + // sort intersections by x + std::sort(intersections.begin(), intersections.end(), + [](const Intersection &i1, const Intersection &i2) { return i1.first.x() < i2.first.x(); }); + + // draw lines + for (size_t i = 0; i < intersections.size(); i+=2) { + const Vec2d& p2 = intersections[i+1].first; + if (p2.x() < 0) + continue; // out of data + + const Vec2d& p1 = intersections[i].first; + if (p1.x() > data_width) + break; // out of data + + // clamp to data + int max_x = std::min(static_cast(data_width-1), static_cast(std::round(p2.x()))); + for (int x = std::max(0, static_cast(std::round(p1.x()))); x <= max_x; ++x) + set_color(x, y); + } + } +} + +/// Union shape defined by glyphs +ExPolygons union_ex(const ExPolygonsWithIds &shapes) +{ + // unify to one expolygon + ExPolygons result; + for (const ExPolygonsWithId &shape : shapes) { + if (shape.expoly.empty()) + continue; + expolygons_append(result, shape.expoly); + } + return union_ex(result); +} + +// init texture by draw expolygons into texture +bool init_texture(Texture &texture, const ExPolygonsWithIds& shapes_with_ids, unsigned max_size_px, const std::vector& shape_warnings){ + BoundingBox bb = get_extents(shapes_with_ids); + Point bb_size = bb.size(); + double bb_width = bb_size.x(); // [in mm] + double bb_height = bb_size.y(); // [in mm] + + bool is_widder = bb_size.x() > bb_size.y(); + double scale = 0.f; + if (is_widder) { + scale = max_size_px / bb_width; + texture.width = max_size_px; + texture.height = static_cast(std::ceil(bb_height * scale)); + } else { + scale = max_size_px / bb_height; + texture.width = static_cast(std::ceil(bb_width * scale)); + texture.height = max_size_px; + } + const int n_pixels = texture.width * texture.height; + if (n_pixels <= 0) + return false; + + constexpr int channels_count = 4; + std::vector data(n_pixels * channels_count, {0}); + + // Union All shapes + ExPolygons shape = union_ex(shapes_with_ids); + + // align to texture + translate(shape, -bb.min); + size_t texture_width = static_cast(texture.width); + unsigned char alpha = 255; // without transparency + std::array color_shape{201, 201, 201, alpha}; // from degin by @JosefZachar + std::array color_error{237, 28, 36, alpha}; // from icon: resources/icons/flag_red.svg + std::array color_warning{237, 107, 33, alpha}; // icons orange + // draw unhealedable shape + for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids) + if (!shapes_with_id.is_healed) { + ExPolygons bad_shape = shapes_with_id.expoly; // copy + translate(bad_shape, -bb.min); // align to texture + draw_side_outline<4>(bad_shape, color_error, data, texture_width, scale); + } + // Draw shape with warning + if (!shape_warnings.empty()) { + for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids){ + assert(shapes_with_id.id < shape_warnings.size()); + if (shapes_with_id.id >= shape_warnings.size()) + continue; + if (shape_warnings[shapes_with_id.id].empty()) + continue; // no warnings for shape + ExPolygons warn_shape = shapes_with_id.expoly; // copy + translate(warn_shape, -bb.min); // align to texture + draw_side_outline<4>(warn_shape, color_warning, data, texture_width, scale); + } + } + + // Draw rest of shape + draw_filled<4>(shape, color_shape, data, texture_width, scale); + + // sends data to gpu + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + if (texture.id != 0) + glsafe(::glDeleteTextures(1, &texture.id)); + glsafe(::glGenTextures(1, &texture.id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, texture.id)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) texture.width, (GLsizei) texture.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, + (const void *) data.data())); + + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + + GLuint NO_TEXTURE_ID = 0; + glsafe(::glBindTexture(GL_TEXTURE_2D, NO_TEXTURE_ID)); + return true; +} + +bool is_closed(NSVGpath *path){ + for (; path != NULL; path = path->next) + if (path->next == NULL && path->closed) + return true; + return false; +} + +void add_comma_separated(std::string &result, const std::string &add){ + if (!result.empty()) + result += ", "; + result += add; +} + +const float warning_preccission = 1e-4f; +std::string create_fill_warning(const NSVGshape &shape) { + if (!(shape.flags & NSVG_FLAGS_VISIBLE) || + shape.fill.type == NSVG_PAINT_NONE) + return {}; // not visible + + std::string warning; + if ((shape.opacity - 1.f + warning_preccission) <= 0.f) + add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); + + // if(shape->flags != NSVG_FLAGS_VISIBLE) add_warning(_u8L("Visibility flag")); + bool is_fill_gradient = shape.fillGradient[0] != '\0'; + if (is_fill_gradient) + add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.fillGradient)); + + switch (shape.fill.type) { + case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined fill type")); break; + case NSVG_PAINT_LINEAR_GRADIENT: + if (!is_fill_gradient) + add_comma_separated(warning, _u8L("Linear gradient")); + break; + case NSVG_PAINT_RADIAL_GRADIENT: + if (!is_fill_gradient) + add_comma_separated(warning, _u8L("Radial gradient")); + break; + // case NSVG_PAINT_NONE: + // case NSVG_PAINT_COLOR: + // default: break; + } + + // Unfilled is only line which could be opened + if (shape.fill.type != NSVG_PAINT_NONE && !is_closed(shape.paths)) + add_comma_separated(warning, _u8L("Open filled path")); + return warning; +} + +std::string create_stroke_warning(const NSVGshape &shape) { + + std::string warning; + if (!(shape.flags & NSVG_FLAGS_VISIBLE) || + shape.stroke.type == NSVG_PAINT_NONE || + shape.strokeWidth <= 1e-5f) + return {}; // not visible + + if ((shape.opacity - 1.f + warning_preccission) <= 0.f) + add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); + + bool is_stroke_gradient = shape.strokeGradient[0] != '\0'; + if (is_stroke_gradient) + add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.strokeGradient)); + + switch (shape.stroke.type) { + case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined stroke type")); break; + case NSVG_PAINT_LINEAR_GRADIENT: + if (!is_stroke_gradient) + add_comma_separated(warning, _u8L("Linear gradient")); + break; + case NSVG_PAINT_RADIAL_GRADIENT: + if (!is_stroke_gradient) + add_comma_separated(warning, _u8L("Radial gradient")); + break; + // case NSVG_PAINT_COLOR: + // case NSVG_PAINT_NONE: + // default: break; + } + + return warning; +} + +/// +/// Create warnings about shape +/// +/// Input svg loaded to shapes +/// Vector of warnings with same size as EmbossShape::shapes_with_ids +/// or Empty when no warnings -> for fast checking that every thing is all right(more common case) +std::vector create_shape_warnings(const EmbossShape &shape, float scale){ + const std::shared_ptr& image_ptr = shape.svg_file->image; + assert(image_ptr != nullptr); + if (image_ptr == nullptr) + return {std::string{"Uninitialized SVG image"}}; + + const NSVGimage &image = *image_ptr; + std::vector result; + auto add_warning = [&result, &image](size_t index, const std::string &message) { + if (result.empty()) + result = std::vector(get_shapes_count(image) * 2); + std::string &res = result[index]; + if (res.empty()) + res = message; + else + res += '\n' + message; + }; + + if (!shape.final_shape.is_healed) { + for (const ExPolygonsWithId &i : shape.shapes_with_ids) + if (!i.is_healed) + add_warning(i.id, _u8L("Path can't be healed from selfintersection and multiple points.")); + + // This waning is not connected to NSVGshape. It is about union of paths, but Zero index is shown first + size_t index = 0; + add_warning(index, _u8L("Final shape constains selfintersection or multiple points with same coordinate.")); + } + + size_t shape_index = 0; + for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next, ++shape_index) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)){ + add_warning(shape_index * 2, GUI::format(_L("Shape is marked as invisible (%1%)."), shape->id)); + continue; + } + + std::string fill_warning = create_fill_warning(*shape); + if (!fill_warning.empty()) { + // TRN: The first placeholder is shape identifier, the second one is text describing the problem. + add_warning(shape_index * 2, GUI::format(_L("Fill of shape (%1%) contains unsupported: %2%."), shape->id, fill_warning)); + } + + float minimal_width_in_mm = 1e-3f; + if (shape->strokeWidth <= minimal_width_in_mm * scale) { + add_warning(shape_index * 2, GUI::format(_L("Stroke of shape (%1%) is too thin (minimal width is %2% mm)."), shape->id, minimal_width_in_mm)); + continue; + } + std::string stroke_warning = create_stroke_warning(*shape); + if (!stroke_warning.empty()) + add_warning(shape_index * 2 + 1, GUI::format(_L("Stroke of shape (%1%) contains unsupported: %2%."), shape->id, stroke_warning)); + } + return result; +} + + +} // namespace + +void GLGizmoSVG::set_volume_by_selection() +{ + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = get_selected_gl_volume(selection); + if (gl_volume == nullptr) + return reset_volume(); + + const ModelObjectPtrs &objects = selection.get_model()->objects; + ModelVolume *volume =get_model_volume(*gl_volume, objects); + if (volume == nullptr) + return reset_volume(); + + // is same volume as actual selected? + if (volume->id() == m_volume_id) + return; + + // Do not use focused input value when switch volume(it must swith value) + if (m_volume != nullptr && + m_volume != volume) // when update volume it changed id BUT not pointer + ImGuiWrapper::left_inputs(); + + // is valid svg volume? + if (!is_svg(*volume)) + return reset_volume(); + + // cancel previous job + if (m_job_cancel != nullptr) { + m_job_cancel->store(true); + m_job_cancel = nullptr; + } + + // calculate scale for height and depth inside of scaled object instance + calculate_scale(); // must be before calculation of tesselation + + EmbossShape &es = *volume->emboss_shape; + EmbossShape::SvgFile &svg_file = *es.svg_file; + if (svg_file.image == nullptr) { + if (init_image(svg_file) == nullptr) + return reset_volume(); + } + assert(svg_file.image != nullptr); + assert(svg_file.image.get() != nullptr); + const NSVGimage &image = *svg_file.image; + ExPolygonsWithIds &shape_ids = es.shapes_with_ids; + if (shape_ids.empty()) { + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; + shape_ids = create_shape_with_ids(image, params); + } + + reset_volume(); // clear cached data + + m_volume = volume; + m_volume_id = volume->id(); + m_volume_shape = es; // copy + m_shape_warnings = create_shape_warnings(es, get_scale_for_tolerance()); + + // Calculate current angle of up vector + m_angle = calc_angle(selection); + m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + m_shape_bb = get_extents(m_volume_shape.shapes_with_ids); +} +namespace { +void delete_texture(Texture& texture){ + if (texture.id != 0) { + glsafe(::glDeleteTextures(1, &texture.id)); + texture.id = 0; + } +} +} +void GLGizmoSVG::reset_volume() +{ + if (m_volume == nullptr) + return; // already reseted + + m_volume = nullptr; + m_volume_id.id = 0; + m_volume_shape.shapes_with_ids.clear(); + m_filename_preview.clear(); + m_shape_warnings.clear(); + // delete texture after finish imgui draw + wxGetApp().plater()->CallAfter([&texture = m_texture]() { delete_texture(texture); }); +} + +void GLGizmoSVG::calculate_scale() { + // be carefull m_volume is not set yet + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) + return; + + Transform3d to_world = gl_volume->world_matrix(); + + const ModelVolume *volume_ptr = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume_ptr != nullptr); + assert(volume_ptr->emboss_shape.has_value()); + // Fix for volume loaded from 3mf + if (volume_ptr != nullptr && + volume_ptr->emboss_shape.has_value()) { + const std::optional &fix_tr = volume_ptr->emboss_shape->fix_3mf_tr; + if (fix_tr.has_value()) + to_world = to_world * (fix_tr->inverse()); + } + + auto to_world_linear = to_world.linear(); + auto calc = [&to_world_linear](const Vec3d &axe, std::optional& scale) { + Vec3d axe_world = to_world_linear * axe; + double norm_sq = axe_world.squaredNorm(); + if (is_approx(norm_sq, 1.)) { + if (!scale.has_value()) + return; + scale.reset(); + } else { + scale = sqrt(norm_sq); + } + }; + + calc(Vec3d::UnitX(), m_scale_width); + calc(Vec3d::UnitY(), m_scale_height); + calc(Vec3d::UnitZ(), m_scale_depth); +} + +float GLGizmoSVG::get_scale_for_tolerance(){ + return std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f)); } + +bool GLGizmoSVG::process() +{ + // no volume is selected -> selection from right panel + assert(m_volume != nullptr); + if (m_volume == nullptr) + return false; + + assert(m_volume->emboss_shape.has_value()); + if (!m_volume->emboss_shape.has_value()) + return false; + + // Cancel previous Job, when it is in process + // worker.cancel(); --> Use less in this case I want cancel only previous EmbossJob no other jobs + // Cancel only EmbossUpdateJob no others + if (m_job_cancel != nullptr) + m_job_cancel->store(true); + // create new shared ptr to cancel new job + m_job_cancel = std::make_shared>(false); + + EmbossShape shape = m_volume_shape; // copy + auto base = std::make_unique(m_volume->name, m_job_cancel, std::move(shape)); + base->is_outside = m_volume->type() == ModelVolumeType::MODEL_PART; + DataUpdate data{std::move(base), m_volume_id}; + return start_update_volume(std::move(data), *m_volume, m_parent.get_selection(), m_raycast_manager); +} + +void GLGizmoSVG::close() +{ + // close gizmo == open it again + auto &mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); + reset_volume(); +} + +void GLGizmoSVG::draw_window() +{ + assert(m_volume != nullptr); + assert(m_volume_id.valid()); + if (m_volume == nullptr || + m_volume_id.invalid()) { + ImGui::Text("Not valid state please report reproduction steps on github"); + return; + } + + assert(m_volume->emboss_shape.has_value()); + if (!m_volume->emboss_shape.has_value()) { + ImGui::Text("No embossed file"); + return; + } + + assert(m_volume->emboss_shape->svg_file.has_value()); + if (!m_volume->emboss_shape->svg_file.has_value()){ + ImGui::Text("Missing svg file in embossed shape"); + return; + } + + assert(m_volume->emboss_shape->svg_file->file_data != nullptr); + if (m_volume->emboss_shape->svg_file->file_data == nullptr){ + ImGui::Text("Missing data of svg file"); + return; + } + + draw_preview(); + draw_filename(); + + // Is SVG baked? + if (m_volume == nullptr) return; + + ImGui::Separator(); + + ImGui::Indent(m_gui_cfg->icon_width); + draw_depth(); + draw_size(); + draw_use_surface(); + + draw_distance(); + draw_rotation(); + draw_mirroring(); + draw_face_the_camera(); + + ImGui::Unindent(m_gui_cfg->icon_width); + + if (!m_volume->is_the_only_one_part()) { + ImGui::Separator(); + draw_model_type(); + } +} + +void GLGizmoSVG::draw_face_the_camera(){ + if (ImGui::Button(_u8L("Face the camera").c_str())) { + const Camera &cam = wxGetApp().plater()->get_camera(); + auto wanted_up_limit = (m_keep_up) ? std::optional(UP_LIMIT) : std::optional{}; + if (face_selected_volume_to_camera(cam, m_parent, wanted_up_limit)) + volume_transformation_changed(); + } +} + +void GLGizmoSVG::draw_preview(){ + // init texture when not initialized yet. + // drag&drop is out of rendering scope so texture must be created on this place + if (m_texture.id == 0) { + const ExPolygonsWithIds &shapes = m_volume->emboss_shape->shapes_with_ids; + init_texture(m_texture, shapes, m_gui_cfg->texture_max_size_px, m_shape_warnings); + } + + //::draw(m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px); + + if (m_texture.id != 0) { + ImTextureID id = (void *) static_cast(m_texture.id); + ImVec2 s(m_texture.width, m_texture.height); + + std::optional spacing; + // is texture over full height? + if (m_texture.height != m_gui_cfg->texture_max_size_px) { + spacing = (m_gui_cfg->texture_max_size_px - m_texture.height) / 2.f; + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing); + } + // is texture over full width? + unsigned window_width = static_cast( + ImGui::GetWindowSize().x - 2*ImGui::GetStyle().WindowPadding.x); + if (window_width > m_texture.width){ + float space = (window_width - m_texture.width) / 2.f; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + space); + } + + ImGui::Image(id, s); + //if(ImGui::IsItemHovered()){ + // const EmbossShape &es = *m_volume->emboss_shape; + // size_t count_of_shapes = get_shapes_count(*es.svg_file.image); + // size_t count_of_expolygons = 0; + // size_t count_of_points = 0; + // for (const auto &shape : es.shapes_with_ids) { + // for (const ExPolygon &expoly : shape.expoly){ + // ++count_of_expolygons; + // count_of_points += count_points(expoly); + // } + // } + // // Do not translate it is only for debug + // std::string tooltip = GUI::format("%1% shapes, which create %2% polygons with %3% line segments", + // count_of_shapes, count_of_expolygons, count_of_points); + // ImGui::SetTooltip("%s", tooltip.c_str()); + //} + + if (spacing.has_value()) + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing); + } +} + +void GLGizmoSVG::draw_filename(){ + const EmbossShape &es = *m_volume->emboss_shape; + const EmbossShape::SvgFile &svg = *es.svg_file; + if (m_filename_preview.empty()){ + // create filename preview + if (!svg.path.empty()) { + m_filename_preview = get_file_name(svg.path); + } else if (!svg.path_in_3mf.empty()) { + m_filename_preview = get_file_name(svg.path_in_3mf); + } + + if (m_filename_preview.empty()) + // TRN - Preview of filename after clear local filepath. + m_filename_preview = _u8L("Unknown filename"); + + m_filename_preview = ImGuiWrapper::trunc(m_filename_preview, m_gui_cfg->input_width); + } + + if (!m_shape_warnings.empty()){ + draw(get_icon(m_icons, IconType::exclamation)); + if (ImGui::IsItemHovered()) { + std::string tooltip; + for (const std::string &w: m_shape_warnings){ + if (w.empty()) + continue; + if (!tooltip.empty()) + tooltip += "\n"; + tooltip += w; + } + ImGui::SetTooltip("%s", tooltip.c_str()); + } + ImGui::SameLine(); + } + + // Remove space between filename and gray suffix ".svg" + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGui::Text("%s", m_filename_preview.c_str()); + bool is_hovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_GREY_LIGHT, ".svg"); + ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing + + is_hovered |= ImGui::IsItemHovered(); + if (is_hovered) { + std::string tooltip = GUI::format(_L("SVG file path is \"%1%\""), svg.path); + ImGui::SetTooltip("%s", tooltip.c_str()); + } + + bool file_changed = false; + + // Re-Load button + bool can_reload = !m_volume_shape.svg_file->path.empty(); + if (can_reload) { + ImGui::SameLine(); + if (clickable(get_icon(m_icons, IconType::refresh), get_icon(m_icons, IconType::refresh_hover))) { + if (!boost::filesystem::exists(m_volume_shape.svg_file->path)) { + m_volume_shape.svg_file->path.clear(); + } else { + file_changed = true; + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reload SVG file from disk.").c_str()); + } + + ImGuiComboFlags flags = ImGuiComboFlags_PopupAlignLeft | ImGuiComboFlags_NoPreview; + ImGui::SameLine(); + if (ImGui::BeginCombo("##file_options", nullptr, flags)) { + ScopeGuard combo_sg([]() { ImGui::EndCombo(); }); + + draw(get_icon(m_icons, IconType::change_file)); + ImGui::SameLine(); + if (ImGui::Selectable((_L("Change file") + dots).ToUTF8().data())) { + std::string new_path = choose_svg_file(); + if (!new_path.empty()) { + file_changed = true; + m_volume_shape.svg_file = {}; // clear data + m_volume_shape.svg_file->path = new_path; + } + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Change to another .svg file").c_str()); + } + + std::string forget_path = _u8L("Forget the file path"); + if (m_volume->emboss_shape->svg_file->path.empty()){ + draw(get_icon(m_icons, IconType::bake_inactive)); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, forget_path.c_str()); + } else { + draw(get_icon(m_icons, IconType::bake)); + ImGui::SameLine(); + if (ImGui::Selectable(forget_path.c_str())) { + // set .svg_file.path_in_3mf to remember file name + m_volume->emboss_shape->svg_file->path.clear(); + m_volume_shape.svg_file->path.clear(); + m_filename_preview.clear(); + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Do NOT save local path to 3MF file.\n" + "Also disables 'reload from disk' option.").c_str()); + } + } + + //draw(get_icon(m_icons, IconType::bake)); + //ImGui::SameLine(); + //if (ImGui::Selectable(_u8L("Bake 2 ©").c_str())) { + // EmbossShape::SvgFile &svg = m_volume_shape.svg_file; + // std::stringstream ss; + // Slic3r::save(*svg.image, ss); + // svg.file_data = std::make_unique(ss.str()); + // svg.image = nsvgParse(*svg.file_data); + // assert(svg.image.get() != NULL); + // if (svg.image.get() != NULL) { + // m_volume->emboss_shape->svg_file = svg; // copy - write changes into volume + // } else { + // svg = m_volume->emboss_shape->svg_file; // revert changes + // } + //} else if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("%s", _u8L("Use only paths from svg - recreate svg").c_str()); + //} + + draw(get_icon(m_icons, IconType::bake)); + ImGui::SameLine(); + // TRN: An menu option to convert the SVG into an unmodifiable model part. + if (ImGui::Selectable(_u8L("Bake").c_str())) { + m_volume->emboss_shape.reset(); + close(); + } else if (ImGui::IsItemHovered()) { + // TRN: Tooltip for the menu item. + ImGui::SetTooltip("%s", _u8L("Bake into model as uneditable part").c_str()); + } + + draw(get_icon(m_icons, IconType::save)); + ImGui::SameLine(); + if (ImGui::Selectable((_L("Save as") + dots).ToUTF8().data())) { + wxWindow *parent = nullptr; + GUI::FileType file_type = FT_SVG; + wxString wildcard = file_wildcards(file_type); + wxString dlg_title = _L("Save SVG file"); + const EmbossShape::SvgFile& svg = *m_volume_shape.svg_file; + wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; + wxFileDialog dlg(parent, dlg_title, last_used_directory, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (dlg.ShowModal() == wxID_OK ){ + last_used_directory = dlg.GetDirectory(); + wxString out_path = dlg.GetPath(); + std::string path{out_path.c_str()}; + //Slic3r::save(*m_volume_shape.svg_file.image, path); + + std::ofstream stream(path); + if (stream.is_open()){ + stream << *svg.file_data; + + // change source file + m_filename_preview.clear(); + m_volume_shape.svg_file->path = path; + m_volume_shape.svg_file->path_in_3mf.clear(); // possible change name + m_volume->emboss_shape->svg_file = m_volume_shape.svg_file; // copy - write changes into volume + } else { + BOOST_LOG_TRIVIAL(error) << "Opening file: \"" << path << "\" Failed"; + } + + } + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Save as '.svg' file").c_str()); + } + + //draw(get_icon(m_icons, IconType::save)); + //ImGui::SameLine(); + //if (ImGui::Selectable((_L("Save used as") + dots).ToUTF8().data())) { + // GUI::FileType file_type = FT_SVG; + // wxString wildcard = file_wildcards(file_type); + // wxString dlg_title = _L("Export SVG file:"); + // wxString dlg_dir = from_u8(wxGetApp().app_config->get_last_dir()); + // const EmbossShape::SvgFile& svg = m_volume_shape.svg_file; + // wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; + // wxFileDialog dlg(nullptr, dlg_title, dlg_dir, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + // if (dlg.ShowModal() == wxID_OK ){ + // wxString out_path = dlg.GetPath(); + // std::string path{out_path.c_str()}; + // Slic3r::save(*m_volume_shape.svg_file.image, path); + // } + //} else if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("%s", _u8L("Save only used path as '.svg' file").c_str()); + //} + } + + if (file_changed) { + float scale = get_scale_for_tolerance(); + double tes_tol = get_tesselation_tolerance(scale); + EmbossShape es_ = select_shape(m_volume_shape.svg_file->path, tes_tol); + m_volume_shape.svg_file = std::move(es_.svg_file); + m_volume_shape.shapes_with_ids = std::move(es_.shapes_with_ids); + m_volume_shape.final_shape = {}; // clear cache + m_shape_warnings = create_shape_warnings(m_volume_shape, scale); + init_texture(m_texture, m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px, m_shape_warnings); + process(); + } +} + +void GLGizmoSVG::draw_depth() +{ + ImGuiWrapper::text(m_gui_cfg->translations.depth); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + double &value = m_volume_shape.projection.depth; + constexpr double step = 1.; + constexpr double step_fast = 10.; + std::optional result_scale; + const char *size_format = "%.1f mm"; + double input = value; + if (use_inch) { + size_format = "%.2f in"; + // input in inches + input *= ObjectManipulation::mm_to_in * m_scale_depth.value_or(1.f); + result_scale = ObjectManipulation::in_to_mm / m_scale_depth.value_or(1.f); + } else if (m_scale_depth.has_value()) { + // scale input + input *= (*m_scale_depth); + result_scale = 1. / (*m_scale_depth); + } + + if (ImGui::InputDouble("##depth", &input, step, step_fast, size_format)) { + if (result_scale.has_value()) + input *= (*result_scale); + apply(input, limits.depth); + if (!is_approx(input, value, 1e-4)){ + value = input; + process(); + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Size in emboss direction.").c_str()); +} + +void GLGizmoSVG::draw_size() +{ + ImGuiWrapper::text(m_gui_cfg->translations.size); + if (ImGui::IsItemHovered()){ + size_t count_points = 0; + for (const auto &s : m_volume_shape.shapes_with_ids) + count_points += Slic3r::count_points(s.expoly); + // TRN: The placeholder contains a number. + ImGui::SetTooltip("%s", GUI::format(_L("Scale also changes amount of curve samples (%1%)"), count_points).c_str()); + } + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + + Point size = m_shape_bb.size(); + double width = size.x() * m_volume_shape.scale * m_scale_width.value_or(1.f); + if (use_inch) width *= ObjectManipulation::mm_to_in; + double height = size.y() * m_volume_shape.scale * m_scale_height.value_or(1.f); + if (use_inch) height *= ObjectManipulation::mm_to_in; + + const auto is_valid_scale_ratio = [limit = &limits.relative_scale_ratio](double ratio) { + if (std::fabs(ratio - 1.) < limit->min) + return false; // too small ratio --> without effect + + if (ratio > limit->max) + return false; + + if (ratio < 1e-4) + return false; // negative scale is not allowed + + return true; + }; + + std::optional new_relative_scale; + if (m_keep_ratio) { + std::stringstream ss; + ss << std::setprecision(2) << std::fixed << width << " x " << height << " " << (use_inch ? "in" : "mm"); + + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + const MinMax &minmax = use_inch ? limits.ui_size_in : limits.ui_size; + // convert to float for slider + float width_f = static_cast(width); + if (m_imgui->slider_float("##width_size_slider", &width_f, minmax.min, minmax.max, ss.str().c_str(), 1.f, false)) { + double width_ratio = width_f / width; + if (is_valid_scale_ratio(width_ratio)) { + m_scale_width = m_scale_width.value_or(1.f) * width_ratio; + m_scale_height = m_scale_height.value_or(1.f) * width_ratio; + new_relative_scale = Vec3d(width_ratio, width_ratio, 1.); + } + } + } else { + ImGuiInputTextFlags flags = 0; + + float space = m_gui_cfg->icon_width / 2; + float input_width = m_gui_cfg->input_width / 2 - space / 2; + float second_offset = m_gui_cfg->input_offset + input_width + space; + + const char *size_format = (use_inch) ? "%.2f in" : "%.1f mm"; + double step = -1.0; + double fast_step = -1.0; + + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(input_width); + double prev_width = width; + if (ImGui::InputDouble("##width", &width, step, fast_step, size_format, flags)) { + double width_ratio = width / prev_width; + if (is_valid_scale_ratio(width_ratio)) { + m_scale_width = m_scale_width.value_or(1.f) * width_ratio; + new_relative_scale = Vec3d(width_ratio, 1., 1.); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Width of SVG.").c_str()); + + ImGui::SameLine(second_offset); + ImGui::SetNextItemWidth(input_width); + double prev_height = height; + if (ImGui::InputDouble("##height", &height, step, fast_step, size_format, flags)) { + double height_ratio = height / prev_height; + if (is_valid_scale_ratio(height_ratio)) { + m_scale_height = m_scale_height.value_or(1.f) * height_ratio; + new_relative_scale = Vec3d(1., height_ratio, 1.); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Height of SVG.").c_str()); + } + + // Lock on ratio m_keep_ratio + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons, m_keep_ratio ? IconType::lock : IconType::unlock); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_ratio ? IconType::lock_hover : IconType::unlock_hover); + if (button(icon, icon_hover, icon)) + m_keep_ratio = !m_keep_ratio; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Lock/unlock the aspect ratio of the SVG.").c_str()); + + + // reset button + bool can_reset = m_scale_width.has_value() || m_scale_height.has_value(); + if (can_reset) { + if (reset_button(m_icons)) { + new_relative_scale = Vec3d(1./m_scale_width.value_or(1.f), 1./m_scale_height.value_or(1.f), 1.); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset scale").c_str()); + } + + if (new_relative_scale.has_value()){ + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_scale_fnc = [&selection, rel_scale = *new_relative_scale]() { + selection.scale(rel_scale, get_drag_transformation_type(selection)); + }; + selection_transform(selection, selection_scale_fnc); + + m_parent.do_scale(L("Resize")); + wxGetApp().obj_manipul()->set_dirty(); + // should be the almost same + calculate_scale(); + + NSVGimage *img = m_volume_shape.svg_file->image.get(); + assert(img != NULL); + if (img != NULL){ + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; + m_volume_shape.shapes_with_ids = create_shape_with_ids(*img, params); + m_volume_shape.final_shape = {}; // reset cache for final shape + process(); + } + } +} + +void GLGizmoSVG::draw_use_surface() +{ + bool can_use_surface = (m_volume->emboss_shape->projection.use_surface)? true : // already used surface must have option to uncheck + !m_volume->is_the_only_one_part(); + m_imgui->disabled_begin(!can_use_surface); + ScopeGuard sc([imgui = m_imgui]() { imgui->disabled_end(); }); + + ImGuiWrapper::text(m_gui_cfg->translations.use_surface); + ImGui::SameLine(m_gui_cfg->input_offset); + + if (ImGui::Checkbox("##useSurface", &m_volume_shape.projection.use_surface)) + process(); +} + +void GLGizmoSVG::draw_distance() +{ + const EmbossProjection& projection = m_volume->emboss_shape->projection; + bool use_surface = projection.use_surface; + bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); + + float prev_distance = m_distance.value_or(.0f); + float min_distance = static_cast(-2 * projection.depth); + float max_distance = static_cast(2 * projection.depth); + + m_imgui->disabled_begin(!allowe_surface_distance); + ScopeGuard sg([imgui = m_imgui]() { imgui->disabled_end(); }); + + ImGuiWrapper::text(m_gui_cfg->translations.distance); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + const wxString move_tooltip = _L("Distance of the center of the SVG to the model surface."); + bool is_moved = false; + if (use_inch) { + std::optional distance_inch; + if (m_distance.has_value()) distance_inch = (*m_distance * ObjectManipulation::mm_to_in); + min_distance = static_cast(min_distance * ObjectManipulation::mm_to_in); + max_distance = static_cast(max_distance * ObjectManipulation::mm_to_in); + if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.3f in", 1.f, false, move_tooltip)) { + if (distance_inch.has_value()) { + m_distance = *distance_inch * ObjectManipulation::in_to_mm; + } else { + m_distance.reset(); + } + is_moved = true; + } + } else { + if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.2f mm", 1.f, false, move_tooltip)) + is_moved = true; + } + + bool can_reset = m_distance.has_value(); + if (can_reset) { + if (reset_button(m_icons)) { + m_distance.reset(); + is_moved = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset distance").c_str()); + } + + if (is_moved) + do_local_z_move(m_parent, m_distance.value_or(.0f) - prev_distance); +} + +void GLGizmoSVG::draw_rotation() +{ + ImGuiWrapper::text(m_gui_cfg->translations.rotation); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + // slider for Clock-wise angle in degress + // stored angle is optional CCW and in radians + // Convert stored value to degress + // minus create clock-wise roation from CCW + float angle = m_angle.value_or(0.f); + float angle_deg = static_cast(-angle * 180 / M_PI); + if (m_imgui->slider_float("##angle", &angle_deg, limits.angle.min, limits.angle.max, u8"%.2f °", 1.f, false, _L("Rotate text Clock-wise."))){ + // convert back to radians and CCW + double angle_rad = -angle_deg * M_PI / 180.0; + Geometry::to_range_pi_pi(angle_rad); + + double diff_angle = angle_rad - angle; + + do_local_z_rotate(m_parent, diff_angle); + + // calc angle after rotation + m_angle = calc_angle(m_parent.get_selection()); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process(); + } + + // Reset button + if (m_angle.has_value()) { + if (reset_button(m_icons)) { + do_local_z_rotate(m_parent, -(*m_angle)); + m_angle.reset(); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset rotation").c_str()); + } + + // Keep up - lock button icon + if (!m_volume->is_the_only_one_part()) { + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons,m_keep_up ? IconType::lock : IconType::unlock); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock_hover : IconType::unlock_hover); + if (button(icon, icon_hover, icon)) + m_keep_up = !m_keep_up; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Lock/unlock rotation angle when dragging above the surface.").c_str()); + } +} + +void GLGizmoSVG::draw_mirroring() +{ + ImGui::Text("%s", m_gui_cfg->translations.mirror.c_str()); + ImGui::SameLine(m_gui_cfg->input_offset); + Axis axis = Axis::UNKNOWN_AXIS; + if(clickable(get_icon(m_icons, IconType::reflection_x), get_icon(m_icons, IconType::reflection_x_hover))){ + axis = Axis::X; + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Mirror vertically").c_str()); + } + + ImGui::SameLine(); + if (clickable(get_icon(m_icons, IconType::reflection_y), get_icon(m_icons, IconType::reflection_y_hover))) { + axis = Axis::Y; + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Mirror horizontally").c_str()); + } + + if (axis != Axis::UNKNOWN_AXIS){ + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_mirror_fnc = [&selection, &axis](){ + selection.mirror(axis, get_drag_transformation_type(selection)); + }; + selection_transform(selection, selection_mirror_fnc); + m_parent.do_mirror(L("Set Mirror")); + + // Mirror is ignoring keep up !! + if (m_keep_up) + m_angle = calc_angle(selection); + + volume_transformation_changed(); + + + if (m_volume_shape.projection.use_surface) + process(); + } +} + +void GLGizmoSVG::draw_model_type() +{ + bool is_last_solid_part = m_volume->is_the_only_one_part(); + std::string title = _u8L("Operation"); + if (is_last_solid_part) { + ImVec4 color{.5f, .5f, .5f, 1.f}; + m_imgui->text_colored(color, title.c_str()); + } else { + ImGui::Text("%s", title.c_str()); + } + + std::optional new_type; + ModelVolumeType modifier = ModelVolumeType::PARAMETER_MODIFIER; + ModelVolumeType negative = ModelVolumeType::NEGATIVE_VOLUME; + ModelVolumeType part = ModelVolumeType::MODEL_PART; + ModelVolumeType type = m_volume->type(); + + //TRN EmbossOperation + if (ImGui::RadioButton(_u8L("Join").c_str(), type == part)) + new_type = part; + else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Click to change text into object part.").c_str()); + ImGui::SameLine(); + + std::string last_solid_part_hint = _u8L("You can't change a type of the last solid part of the object."); + if (ImGui::RadioButton(_CTX_utf8(L_CONTEXT("Cut", "EmbossOperation"), "EmbossOperation").c_str(), type == negative)) + new_type = negative; + else if (ImGui::IsItemHovered()) { + if (is_last_solid_part) + ImGui::SetTooltip("%s", last_solid_part_hint.c_str()); + else if (type != negative) + ImGui::SetTooltip("%s", _u8L("Click to change part type into negative volume.").c_str()); + } + + // In simple mode are not modifiers + if (wxGetApp().plater()->printer_technology() != ptSLA && wxGetApp().get_mode() != ConfigOptionMode::comSimple) { + ImGui::SameLine(); + if (ImGui::RadioButton(_u8L("Modifier").c_str(), type == modifier)) + new_type = modifier; + else if (ImGui::IsItemHovered()) { + if (is_last_solid_part) + ImGui::SetTooltip("%s", last_solid_part_hint.c_str()); + else if (type != modifier) + ImGui::SetTooltip("%s", _u8L("Click to change part type into modifier.").c_str()); + } + } + + if (m_volume != nullptr && new_type.has_value() && !is_last_solid_part) { + GUI_App &app = wxGetApp(); + Plater * plater = app.plater(); + // TRN: This is the name of the action that shows in undo/redo stack (changing part type from SVG to something else). + Plater::TakeSnapshot snapshot(plater, _L("Change SVG Type"), UndoRedo::SnapshotType::GizmoAction); + m_volume->set_type(*new_type); + + bool is_volume_move_inside = (type == part); + bool is_volume_move_outside = (*new_type == part); + // Update volume position when switch (from part) or (into part) + if ((is_volume_move_inside || is_volume_move_outside)) + process(); + + // inspiration in ObjectList::change_part_type() + // how to view correct side panel with objects + ObjectList *obj_list = app.obj_list(); + wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection( + obj_list->get_selected_obj_idx(), + [volume = m_volume](const ModelVolume *vol) { return vol == volume; }); + if (!sel.IsEmpty()) obj_list->select_item(sel.front()); + + // NOTE: on linux, function reorder_volumes_and_get_selection call GLCanvas3D::reload_scene(refresh_immediately = false) + // which discard m_volume pointer and set it to nullptr also selection is cleared so gizmo is automaticaly closed + auto &mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() != GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); + // TODO: select volume back - Ask @Sasa + } +} + + +///////////// +// private namespace implementation +/////////////// +namespace { + +std::string get_file_name(const std::string &file_path) +{ + if (file_path.empty()) + return file_path; + + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + if (pos_last_delimiter == std::string::npos) { + // should not happend that in path is not delimiter + assert(false); + pos_last_delimiter = 0; + } + + size_t pos_point = file_path.find_last_of('.'); + if (pos_point == std::string::npos || pos_point < pos_last_delimiter // last point is inside of directory path + ) { + // there is no extension + assert(false); + pos_point = file_path.size(); + } + + size_t offset = pos_last_delimiter + 1; // result should not contain last delimiter ( +1 ) + size_t count = pos_point - pos_last_delimiter - 1; // result should not contain extension point ( -1 ) + return file_path.substr(offset, count); +} + +std::string volume_name(const EmbossShape &shape) +{ + std::string file_name = get_file_name(shape.svg_file->path); + if (!file_name.empty()) + return file_name; + return "SVG shape"; +} + +CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager& raycaster, ModelVolumeType volume_type) +{ + auto gizmo = static_cast(GLGizmosManager::Svg); + const GLVolume *gl_volume = get_first_hovered_gl_volume(canvas); + Plater *plater = wxGetApp().plater(); + return CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), + plater->get_ui_job_worker(), volume_type, raycaster, gizmo, gl_volume}; +} + +GuiCfg create_gui_configuration() { + GuiCfg cfg; // initialize by default values; + + float line_height = ImGui::GetTextLineHeight(); + float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); + + float space = line_height_with_spacing - line_height; + + cfg.icon_width = std::max(std::round(line_height/8)*8, 8.f); + + GuiCfg::Translations &tr = cfg.translations; + + float lock_width = cfg.icon_width + 3 * space; + // TRN - Input label. Be short as possible + tr.depth = _u8L("Depth"); + // TRN - Input label. Be short as possible + tr.size = _u8L("Size"); + // TRN - Input label. Be short as possible + tr.use_surface = _u8L("Use surface"); + // TRN - Input label. Be short as possible + tr.distance = _u8L("From surface"); + // TRN - Input label. Be short as possible + tr.rotation = _u8L("Rotation"); + // TRN - Input label. Be short as possible + tr.mirror = _u8L("Mirror"); + float max_tr_width = std::max({ + ImGui::CalcTextSize(tr.depth.c_str()).x, + ImGui::CalcTextSize(tr.size.c_str()).x + lock_width, + ImGui::CalcTextSize(tr.use_surface.c_str()).x, + ImGui::CalcTextSize(tr.distance.c_str()).x, + ImGui::CalcTextSize(tr.rotation.c_str()).x + lock_width, + ImGui::CalcTextSize(tr.mirror.c_str()).x, + }); + + const ImGuiStyle &style = ImGui::GetStyle(); + cfg.input_offset = style.WindowPadding.x + max_tr_width + space + cfg.icon_width; + cfg.lock_offset = cfg.input_offset - (cfg.icon_width + 2 * space); + + ImVec2 letter_m_size = ImGui::CalcTextSize("M"); + const float count_letter_M_in_input = 12.f; + cfg.input_width = letter_m_size.x * count_letter_M_in_input; + cfg.texture_max_size_px = std::round((cfg.input_width + cfg.input_offset + cfg.icon_width + space)/8) * 8; + + // calculate window size + float window_input_width = cfg.input_offset + cfg.input_width + style.WindowPadding.x + space; + float window_image_width = cfg.texture_max_size_px + 2*style.WindowPadding.x; + + float window_title = line_height + 2 * style.FramePadding.y + 2 * style.WindowTitleAlign.y; + float input_height = line_height_with_spacing + 2 * style.FramePadding.y; + float separator_height = 2 + style.FramePadding.y; + float window_height = + window_title + // window title + cfg.texture_max_size_px + 2 * style.FramePadding.y + // preview (-- not sure with padding -> fix retina height) + line_height_with_spacing + // filename + separator_height + // separator - orange line + input_height * 7 + // depth + size + use_surface + FromSurface + Rotation + Mirror + FaceTheCamera + 2 * style.WindowPadding.y; + cfg.window_size_for_object = ImVec2(std::max(window_input_width, window_image_width), window_height); + float change_type_height = separator_height + line_height_with_spacing + input_height; + cfg.window_size = ImVec2(cfg.window_size_for_object.x, cfg.window_size_for_object.y + change_type_height); + return cfg; +} + +std::string choose_svg_file() +{ + wxWindow *parent = nullptr; + wxString message = _L("Choose SVG file for emboss:"); + wxString selected_file = wxEmptyString; + wxString wildcard = file_wildcards(FT_SVG); + long style = wxFD_OPEN | wxFD_FILE_MUST_EXIST; + wxFileDialog dialog(parent, message, last_used_directory, selected_file, wildcard, style); + if (dialog.ShowModal() != wxID_OK) { + BOOST_LOG_TRIVIAL(warning) << "SVG file for emboss was NOT selected."; + return {}; + } + + wxArrayString input_files; + dialog.GetPaths(input_files); + if (input_files.IsEmpty()) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result is empty."; + return {}; + } + + if (input_files.size() != 1) + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result contain multiple files but only first is used."; + + std::string path = into_u8(input_files.front()); + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return invalid path."; + return {}; + } + + if (!boost::algorithm::iends_with(path, ".svg")) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return path without '.svg' tail"; + return {}; + } + + last_used_directory = dialog.GetDirectory(); + return path; +} + +EmbossShape select_shape(std::string_view filepath, double tesselation_tolerance) +{ + EmbossShape shape; + shape.projection.depth = 10.; + shape.projection.use_surface = false; + + EmbossShape::SvgFile svg; + if (filepath.empty()) { + // When empty open file dialog + svg.path = choose_svg_file(); + if (svg.path.empty()) + return {}; // file was not selected + } else { + svg.path = filepath; // copy + } + + + boost::filesystem::path path(svg.path); + if (!boost::filesystem::exists(path)) { + show_error(nullptr, GUI::format(_u8L("File does NOT exist (%1%)."), svg.path)); + return {}; + } + + if (!boost::algorithm::iends_with(svg.path, ".svg")) { + show_error(nullptr, GUI::format(_u8L("Filename has to end with \".svg\" but you selected %1%"), svg.path)); + return {}; + } + + if(init_image(svg) == nullptr) { + show_error(nullptr, GUI::format(_u8L("Nano SVG parser can't load from file (%1%)."), svg.path)); + return {}; + } + + // Set default and unchanging scale + NSVGLineParams params{tesselation_tolerance}; + shape.shapes_with_ids = create_shape_with_ids(*svg.image, params); + + // Must contain some shapes !!! + if (shape.shapes_with_ids.empty()) { + show_error(nullptr, GUI::format(_u8L("SVG file does NOT contain a single path to be embossed (%1%)."), svg.path)); + return {}; + } + shape.svg_file = std::move(svg); + return shape; +} + +DataBasePtr create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath) +{ + EmbossShape shape = select_shape(filepath); + + if (shape.shapes_with_ids.empty()) + // canceled selection of SVG file + return nullptr; + + // Cancel previous Job, when it is in process + // worker.cancel(); --> Use less in this case I want cancel only previous EmbossJob no other jobs + // Cancel only EmbossUpdateJob no others + if (cancel != nullptr) + cancel->store(true); + // create new shared ptr to cancel new job + cancel = std::make_shared>(false); + + std::string name = volume_name(shape); + + auto result = std::make_unique(name, cancel /*copy*/, std::move(shape)); + result->is_outside = volume_type == ModelVolumeType::MODEL_PART; + return result; +} +} // namespace + +// any existing icon filename to not influence GUI +const std::string GLGizmoSVG::M_ICON_FILENAME = "cut.svg"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp new file mode 100644 index 0000000..36b1258 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp @@ -0,0 +1,203 @@ +#ifndef slic3r_GLGizmoSVG_hpp_ +#define slic3r_GLGizmoSVG_hpp_ + +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, +// which overrides our localization "L" macro. +#include "GLGizmoBase.hpp" +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/SurfaceDrag.hpp" +#include "slic3r/GUI/GLTexture.hpp" +#include "slic3r/Utils/RaycastManager.hpp" +#include "slic3r/GUI/IconManager.hpp" + +#include +#include +#include + +#include "libslic3r/Emboss.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Model.hpp" + +#include +#include + +namespace Slic3r{ +class ModelVolume; +enum class ModelVolumeType : int; +} + +namespace Slic3r::GUI { + +struct Texture{ + unsigned id{0}; + unsigned width{0}; + unsigned height{0}; +}; + +class GLGizmoSVG : public GLGizmoBase +{ +public: + explicit GLGizmoSVG(GLCanvas3D &parent); + + /// + /// Create new embossed text volume by type on position of mouse + /// + /// Object part / Negative volume / Modifier + /// Define position of new volume + /// True on succesfull start creation otherwise False + bool create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); // first open file dialog + + /// + /// Create new text without given position + /// + /// Object part / Negative volume / Modifier + /// True on succesfull start creation otherwise False + bool create_volume(ModelVolumeType volume_type); // first open file dialog + + /// + /// Create volume from already selected svg file + /// + /// File path + /// Position on screen where to create volume + /// Object part / Negative volume / Modifier + /// True on succesfull start creation otherwise False + bool create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type = ModelVolumeType::MODEL_PART); + bool create_volume(std::string_view svg_file, ModelVolumeType volume_type = ModelVolumeType::MODEL_PART); + + /// + /// Check whether volume is object containing only emboss volume + /// + /// Pointer to volume + /// True when object otherwise False + static bool is_svg_object(const ModelVolume &volume); + + /// + /// Check whether volume has emboss data + /// + /// Pointer to volume + /// True when constain emboss data otherwise False + static bool is_svg(const ModelVolume &volume); + +protected: + bool on_init() override; + std::string on_get_name() const override; + void on_render() override; + void on_register_raycasters_for_picking() override; + void on_unregister_raycasters_for_picking() override; + void on_render_input_window(float x, float y, float bottom_limit) override; + bool on_is_activable() const override { return true; } + bool on_is_selectable() const override { return false; } + void on_set_state() override; + void data_changed(bool is_serializing) override; // selection changed + void on_set_hover_id() override{ m_rotate_gizmo.set_hover_id(m_hover_id); } + void on_enable_grabber(unsigned int id) override { m_rotate_gizmo.enable_grabber(); } + void on_disable_grabber(unsigned int id) override { m_rotate_gizmo.disable_grabber(); } + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData &data) override; + + /// + /// Rotate by text on dragging rotate grabers + /// + /// Information about mouse + /// Propagete normaly return false. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + bool wants_enter_leave_snapshots() const override; + std::string get_gizmo_entering_text() const override; + std::string get_gizmo_leaving_text() const override; + std::string get_action_snapshot_name() const override; +private: + void set_volume_by_selection(); + void reset_volume(); + + // create volume from text - main functionality + bool process(); + void close(); + void draw_window(); + void draw_preview(); + void draw_filename(); + void draw_depth(); + void draw_size(); + void draw_use_surface(); + void draw_distance(); + void draw_rotation(); + void draw_mirroring(); + void draw_face_the_camera(); + void draw_model_type(); + + // process mouse event + bool on_mouse_for_rotation(const wxMouseEvent &mouse_event); + bool on_mouse_for_translate(const wxMouseEvent &mouse_event); + + void volume_transformation_changed(); + + struct GuiCfg; + std::unique_ptr m_gui_cfg; + + // actual selected only one volume - with emboss data + ModelVolume *m_volume = nullptr; + + // Is used to edit eboss and send changes to job + // Inside volume is current state of shape WRT Volume + EmbossShape m_volume_shape; // copy from m_volume for edit + + // same index as volumes in + std::vector m_shape_warnings; + + // When work with undo redo stack there could be situation that + // m_volume point to unexisting volume so One need also objectID + ObjectID m_volume_id; + + // cancel for previous update of volume to cancel finalize part + std::shared_ptr> m_job_cancel = nullptr; + + // Rotation gizmo + GLGizmoRotate m_rotate_gizmo; + std::optional m_angle; + std::optional m_distance; + + // Value is set only when dragging rotation to calculate actual angle + std::optional m_rotate_start_angle; + + // TODO: it should be accessible by other gizmo too. + // May be move to plater? + RaycastManager m_raycast_manager; + + // When true keep up vector otherwise relative rotation + bool m_keep_up = true; + + // Keep size aspect ratio when True. + bool m_keep_ratio = true; + + // setted only when wanted to use - not all the time + std::optional m_set_window_offset; + + // Keep data about dragging only during drag&drop + std::optional m_surface_drag; + + // For volume on scaled objects + std::optional m_scale_width; + std::optional m_scale_height; + std::optional m_scale_depth; + void calculate_scale(); + float get_scale_for_tolerance(); + + // keep SVG data rendered on GPU + Texture m_texture; + + // bounding box of shape + // Note: Scaled mm to int value by m_volume_shape.scale + BoundingBox m_shape_bb; + + std::string m_filename_preview; + + IconManager m_icon_manager; + IconManager::Icons m_icons; + + // only temporary solution + static const std::string M_ICON_FILENAME; +}; +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoSVG_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index 0b083a4..c686de3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -37,7 +37,7 @@ static void call_after_if_active(std::function fn, GUI_App* app = &wxGet }); } -static std::set get_volume_ids(const Selection &selection) +static std::set get_selected_volume_ids(const Selection &selection) { const Selection::IndicesList &volume_ids = selection.get_volume_idxs(); const ModelObjectPtrs &model_objects = selection.get_model()->objects; @@ -64,19 +64,6 @@ static std::set get_volume_ids(const Selection &selection) return result; } -// return ModelVolume from selection by object id -static ModelVolume *get_volume(const ObjectID &id, const Selection &selection) { - const Selection::IndicesList &volume_ids = selection.get_volume_idxs(); - const ModelObjectPtrs &model_objects = selection.get_model()->objects; - for (auto volume_id : volume_ids) { - const GLVolume *selected_volume = selection.get_volume(volume_id); - const GLVolume::CompositeID &cid = selected_volume->composite_id; - ModelObject *obj = model_objects[cid.object_id]; - ModelVolume *volume = obj->volumes[cid.volume_id]; - if (id == volume->id()) return volume; - } - return nullptr; -} static std::string create_volumes_name(const std::set& ids, const Selection &selection){ assert(!ids.empty()); @@ -88,7 +75,7 @@ static std::string create_volumes_name(const std::set& ids, const Sele else name += " + "; - const ModelVolume *volume = get_volume(id, selection); + const ModelVolume *volume = get_selected_volume(id, selection); assert(volume != nullptr); name += volume->name; } @@ -181,11 +168,11 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi { create_gui_cfg(); const Selection &selection = m_parent.get_selection(); - auto act_volume_ids = get_volume_ids(selection); + auto act_volume_ids = get_selected_volume_ids(selection); if (act_volume_ids.empty()) { stop_worker_thread_request(); close(); - if (! m_parent.get_selection().is_single_volume()) { + if (m_parent.get_selection().volumes_count() != 1) { MessageDialog msg((wxWindow*)wxGetApp().mainframe, _L("Simplification is currently only allowed when a single part is selected"), _L("Error")); @@ -471,7 +458,7 @@ void GLGizmoSimplify::process() const Selection& selection = m_parent.get_selection(); State::Data its; for (const auto &id : m_volume_ids) { - const ModelVolume *volume = get_volume(id, selection); + const ModelVolume *volume = get_selected_volume(id, selection); its[id] = std::make_unique(volume->mesh().its); // copy } @@ -549,7 +536,7 @@ void GLGizmoSimplify::apply_simplify() { for (const auto &item: m_state.result) { const ObjectID &id = item.first; const indexed_triangle_set &its = *item.second; - ModelVolume *volume = get_volume(id, selection); + ModelVolume *volume = get_selected_volume(id, selection); assert(volume != nullptr); ModelObject *obj = volume->get_object(); @@ -725,7 +712,7 @@ void GLGizmoSimplify::on_render() const Selection & selection = m_parent.get_selection(); // Check that the GLVolume still belongs to the ModelObject we work on. - if (m_volume_ids != get_volume_ids(selection)) return; + if (m_volume_ids != get_selected_volume_ids(selection)) return; const ModelObjectPtrs &model_objects = selection.get_model()->objects; const Selection::IndicesList &volume_idxs = selection.get_volume_idxs(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 3103cd9..794747b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -22,6 +22,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMeasure.hpp" #include "libslic3r/format.hpp" @@ -109,6 +110,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "mmu_segmentation.svg", 9)); m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 10)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent)); + m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); m_gizmos.emplace_back(new GLGizmoSimplify(m_parent)); m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 8c98596..509a433 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -79,6 +79,7 @@ public: MmuSegmentation, Measure, Emboss, + Svg, Simplify, Undefined }; diff --git a/src/slic3r/GUI/IconManager.cpp b/src/slic3r/GUI/IconManager.cpp index 45c7688..e85e710 100644 --- a/src/slic3r/GUI/IconManager.cpp +++ b/src/slic3r/GUI/IconManager.cpp @@ -1,6 +1,20 @@ #include "IconManager.hpp" +#include #include +#include #include +#include +#include +#include +#include "nanosvg/nanosvg.h" +#include "nanosvg/nanosvgrast.h" +#include "libslic3r/Utils.hpp" // ScopeGuard + +#include "3DScene.hpp" // glsafe +#include "GL/glew.h" + +#define STB_RECT_PACK_IMPLEMENTATION +#include "imgui/imstb_rectpack.h" // distribute rectangles using namespace Slic3r::GUI; @@ -14,16 +28,195 @@ static void draw_transparent_icon(const IconManager::Icon &icon); // only help f IconManager::~IconManager() { priv::clear(m_icons); // release opengl texture is made in ~GLTexture() + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); } -std::vector IconManager::init(const InitTypes &input) +namespace { +NSVGimage *parse_file(const char * filepath) { + FILE *fp = boost::nowide::fopen(filepath, "rb"); + assert(fp != nullptr); + if (fp == nullptr) + return nullptr; + + Slic3r::ScopeGuard sg([fp]() { fclose(fp); }); + + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + // Note: +1 is for null termination + auto data_ptr = std::make_unique(size+1); + data_ptr[size] = '\0'; // Must be null terminated. + + size_t readed_size = fread(data_ptr.get(), 1, size, fp); + assert(readed_size == size); + if (readed_size != size) + return nullptr; + + return nsvgParse(data_ptr.get(), "px", 96.0f); +} + +void subdata(unsigned char *data, size_t data_stride, const std::vector &data2, size_t data2_row) { + assert(data_stride >= data2_row); + for (size_t data2_offset = 0, data_offset = 0; + data2_offset < data2.size(); + data2_offset += data2_row, data_offset += data_stride) + ::memcpy((void *)(data + data_offset), (const void *)(data2.data() + data2_offset), data2_row); +} +} + +IconManager::Icons IconManager::init(const InitTypes &input) { - BOOST_LOG_TRIVIAL(error) << "Not implemented yet"; + assert(!input.empty()); + if (input.empty()) + return {}; + + // TODO: remove in future + if (m_id != 0) { + glsafe(::glDeleteTextures(1, &m_id)); + m_id = 0; + } + + int total_surface = 0; + for (const InitType &i : input) + total_surface += i.size.x * i.size.y; + const int surface_sqrt = (int)sqrt((float)total_surface) + 1; + + // Start packing + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + const int TEX_HEIGHT_MAX = 1024 * 32; + int width = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; + + int num_nodes = width; + std::vector nodes(num_nodes); + stbrp_context context; + stbrp_init_target(&context, width, TEX_HEIGHT_MAX, nodes.data(), num_nodes); + + ImVector pack_rects; + pack_rects.resize(input.size()); + memset(pack_rects.Data, 0, (size_t) pack_rects.size_in_bytes()); + for (size_t i = 0; i < input.size(); i++) { + const ImVec2 &size = input[i].size; + assert(size.x > 1); + assert(size.y > 1); + pack_rects[i].w = size.x; + pack_rects[i].h = size.y; + } + int pack_rects_res = stbrp_pack_rects(&context, &pack_rects[0], pack_rects.Size); + assert(pack_rects_res == 1); + if (pack_rects_res != 1) + return {}; + + ImVec2 tex_size(width, width); + for (const stbrp_rect &rect : pack_rects) { + float x = rect.x + rect.w; + float y = rect.y + rect.h; + if(x > tex_size.x) tex_size.x = x; + if(y > tex_size.y) tex_size.y = y; + } + + Icons result(input.size()); + for (int i = 0; i < pack_rects.Size; i++) { + const stbrp_rect &rect = pack_rects[i]; + assert(rect.was_packed); + if (!rect.was_packed) + return {}; + + ImVec2 tl(rect.x / tex_size.x, rect.y / tex_size.y); + ImVec2 br((rect.x + rect.w) / tex_size.x, (rect.y + rect.h) / tex_size.y); + + assert(input[i].size.x == rect.w); + assert(input[i].size.y == rect.h); + Icon icon = {input[i].size, tl, br}; + result[i] = std::make_shared(std::move(icon)); + } + + NSVGrasterizer *rast = nsvgCreateRasterizer(); + assert(rast != nullptr); + if (rast == nullptr) + return {}; + ScopeGuard sg_rast([rast]() { ::nsvgDeleteRasterizer(rast); }); + + int channels = 4; + int n_pixels = tex_size.x * tex_size.y; + // store data for whole texture + std::vector data(n_pixels * channels, {0}); + + // initialize original index locations + std::vector idx(input.size()); + std::iota(idx.begin(), idx.end(), 0); + + // Group same filename by sort inputs + // sort indexes based on comparing values in input + std::sort(idx.begin(), idx.end(), [&input](size_t i1, size_t i2) { return input[i1].filepath < input[i2].filepath; }); + for (size_t j: idx) { + const InitType &i = input[j]; + if (i.filepath.empty()) + continue; // no file path only reservation of space for texture + assert(boost::filesystem::exists(i.filepath)); + if (!boost::filesystem::exists(i.filepath)) + continue; + assert(boost::algorithm::iends_with(i.filepath, ".svg")); + if (!boost::algorithm::iends_with(i.filepath, ".svg")) + continue; + + NSVGimage *image = parse_file(i.filepath.c_str()); + assert(image != nullptr); + if (image == nullptr) return {}; + ScopeGuard sg_image([image]() { ::nsvgDelete(image); }); + + float svg_scale = i.size.y / image->height; + // scale should be same in both directions + assert(is_approx(svg_scale, i.size.y / image->width)); + + const stbrp_rect &rect = pack_rects[j]; + int n_pixels = rect.w * rect.h; + std::vector icon_data(n_pixels * channels, {0}); + ::nsvgRasterize(rast, image, 0, 0, svg_scale, icon_data.data(), i.size.x, i.size.y, i.size.x * channels); + + // makes white or gray only data in icon + if (i.type == RasterType::white_only_data || + i.type == RasterType::gray_only_data) { + unsigned char value = (i.type == RasterType::white_only_data) ? 255 : 127; + for (size_t k = 0; k < icon_data.size(); k += channels) + if (icon_data[k] != 0 || icon_data[k + 1] != 0 || icon_data[k + 2] != 0) { + icon_data[k] = value; + icon_data[k + 1] = value; + icon_data[k + 2] = value; + } + } + + int start_offset = (rect.y*tex_size.x + rect.x) * channels; + int data_stride = tex_size.x * channels; + subdata(data.data() + start_offset, data_stride, icon_data, rect.w * channels); + } + + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); + + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(::glGenTextures(1, &m_id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint) m_id)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) tex_size.x, (GLsizei) tex_size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*) data.data())); + + // bind no texture + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + + for (const auto &i : result) + i->tex_id = m_id; + return result; } std::vector IconManager::init(const std::vector &file_paths, const ImVec2 &size, RasterType type) { + assert(!file_paths.empty()); + assert(size.x >= 1); + assert(size.x < 256*16); // TODO: remove in future if (!m_icons.empty()) { // not first initialization diff --git a/src/slic3r/GUI/IconManager.hpp b/src/slic3r/GUI/IconManager.hpp index aa7afda..72331a8 100644 --- a/src/slic3r/GUI/IconManager.hpp +++ b/src/slic3r/GUI/IconManager.hpp @@ -70,10 +70,10 @@ public: /// Initialize raster texture on GPU with given images /// NOTE: Have to be called after OpenGL initialization ///
- /// Define files and its + /// Define files and its size with rasterization /// Rasterized icons stored on GPU, /// Same size and order as input, each item of vector is set of texture in order by RasterType - VIcons init(const InitTypes &input); + Icons init(const InitTypes &input); /// /// Initialize multiple icons with same settings for size and type @@ -96,6 +96,7 @@ public: private: // keep data stored on GPU GLTexture m_icons_texture; + unsigned int m_id{ 0 }; Icons m_icons; }; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 8fb8ce4..eb4bb4e 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -671,8 +671,9 @@ bool ImGuiWrapper::slider_float(const char* label, float* v, float v_min, float ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.4f, 0.4f, 0.4f, 1.0f }); ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0.4f, 0.4f, 0.4f, 1.0f }); + int frame_padding = style.ItemSpacing.y / 2; // keep same line height for input and slider const ImTextureID tex_id = io.Fonts->TexID; - if (image_button(tex_id, size, uv0, uv1, -1, ImVec4(0.0, 0.0, 0.0, 0.0), ImVec4(1.0, 1.0, 1.0, 1.0), ImGuiButtonFlags_PressedOnClick)) { + if (image_button(tex_id, size, uv0, uv1, frame_padding, ImVec4(0.0, 0.0, 0.0, 0.0), ImVec4(1.0, 1.0, 1.0, 1.0), ImGuiButtonFlags_PressedOnClick)) { if (!slider_editing) ImGui::SetKeyboardFocusHere(-1); else @@ -788,7 +789,8 @@ bool ImGuiWrapper::combo(const wxString& label, const std::vector& bool ImGuiWrapper::combo(const std::string& label, const std::vector& options, int& selection, ImGuiComboFlags flags/* = 0*/, float label_width/* = 0.0f*/, float item_width/* = 0.0f*/) { // this is to force the label to the left of the widget: - if (!label.empty()) { + const bool hidden_label = boost::starts_with(label, "##"); + if (!label.empty() && !hidden_label) { text(label); ImGui::SameLine(label_width); } @@ -798,7 +800,7 @@ bool ImGuiWrapper::combo(const std::string& label, const std::vector= 0 ? options[selection].c_str() : ""; - if (ImGui::BeginCombo(("##" + label).c_str(), selection_str, flags)) { + if (ImGui::BeginCombo(hidden_label ? label.c_str() : ("##" + label).c_str(), selection_str, flags)) { for (int i = 0; i < (int)options.size(); i++) { if (ImGui::Selectable(options[i].c_str(), i == selection)) { selection_out = i; @@ -1395,6 +1397,47 @@ bool ImGuiWrapper::slider_optional_int(const char *label, } else return false; } +std::optional ImGuiWrapper::change_window_position(const char *window_name, bool try_to_fix) { + ImGuiWindow *window = ImGui::FindWindowByName(window_name); + // is window just created + if (window == NULL) + return {}; + + // position of window on screen + ImVec2 position = window->Pos; + ImVec2 size = window->SizeFull; + + // screen size + ImVec2 screen = ImGui::GetMainViewport()->Size; + + std::optional output_window_offset; + if (position.x < 0) { + if (position.y < 0) + // top left + output_window_offset = ImVec2(0, 0); + else + // only left + output_window_offset = ImVec2(0, position.y); + } else if (position.y < 0) { + // only top + output_window_offset = ImVec2(position.x, 0); + } else if (screen.x < (position.x + size.x)) { + if (screen.y < (position.y + size.y)) + // right bottom + output_window_offset = ImVec2(screen.x - size.x, screen.y - size.y); + else + // only right + output_window_offset = ImVec2(screen.x - size.x, position.y); + } else if (screen.y < (position.y + size.y)) { + // only bottom + output_window_offset = ImVec2(position.x, screen.y - size.y); + } + + if (!try_to_fix && output_window_offset.has_value()) + output_window_offset = ImVec2(-1, -1); // Put on default position + + return output_window_offset; +} void ImGuiWrapper::left_inputs() { ImGui::ClearActiveID(); } diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index a18fec2..1fefa63 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -154,6 +154,15 @@ public: // Extended function ImGuiWrapper::slider_float to work with std::optional, when value == def_val than optional release its value bool slider_optional_int(const char* label, std::optional &v, int v_min, int v_max, const char* format = "%.3f", float power = 1.0f, bool clamp = true, const wxString& tooltip = {}, bool show_edit_btn = true, int def_val = 0); + /// + /// Change position of imgui window + /// + /// ImGui identifier of window + /// [output] optional + /// When True Only move to be full visible otherwise reset position + /// New offset of window for function ImGui::SetNextWindowPos + static std::optional change_window_position(const char *window_name, bool try_to_fix); + /// /// Use ImGui internals to unactivate (lose focus) in input. /// When input is activ it can't change value by application. diff --git a/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp index a4f22be..57c9a62 100644 --- a/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp @@ -172,10 +172,11 @@ bool BoostThreadWorker::wait_for_idle(unsigned timeout_ms) bool BoostThreadWorker::push(std::unique_ptr job) { - if (job) - m_input_queue.push(JobEntry{std::move(job)}); + if (!job) + return false; - return bool{job}; + m_input_queue.push(JobEntry{std::move(job)}); + return true; } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp index 4057b73..417eb0e 100644 --- a/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp +++ b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp @@ -74,7 +74,7 @@ void CreateFontImageJob::process(Ctl &ctl) // normalize height of font BoundingBox bounding_box; - for (ExPolygon &shape : shapes) + for (const ExPolygon &shape : shapes) bounding_box.merge(BoundingBox(shape.contour.points)); if (bounding_box.size().x() < 1 || bounding_box.size().y() < 1) { m_input.cancel->store(true); diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp index 4bafd96..28edc38 100644 --- a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp @@ -47,8 +47,7 @@ void CreateFontStyleImagesJob::process(Ctl &ctl) for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); // calculate conversion from FontPoint to screen pixels by size of font - const FontFile::Info &info = get_font_info(*item.font.font_file, item.prop); - double scale = item.prop.size_in_mm / info.unit_per_em * SHAPE_SCALE * m_input.ppm; + double scale = get_text_shape_scale(item.prop, *item.font.font_file); scales[index] = scale; //double scale = font_prop.size_in_mm * SCALING_FACTOR; diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index 3b631b8..be45703 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -2,10 +2,13 @@ #include #include +#include #include #include // load_obj for default mesh #include // use surface cuts +#include // create object +#include #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/NotificationManager.hpp" @@ -15,29 +18,143 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/Selection.hpp" #include "slic3r/GUI/CameraUtils.hpp" #include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/3DScene.hpp" +#include "slic3r/GUI/Jobs/Worker.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "slic3r/Utils/RaycastManager.hpp" + +// #define EXECUTE_UPDATE_ON_MAIN_THREAD // debug execution on main thread using namespace Slic3r; using namespace Slic3r::Emboss; using namespace Slic3r::GUI; using namespace Slic3r::GUI::Emboss; -// private namespace -namespace priv{ -// create sure that emboss object is bigger than source object [in mm] -constexpr float safe_extension = 1.0f; +// Private implementation for create volume and objects jobs +namespace { +/// +/// Hold neccessary data to create ModelVolume in job +/// Volume is created on the surface of existing volume in object. +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +struct DataCreateVolume +{ + // Hold data about shape + DataBasePtr base; + + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // new created volume transformation + std::optional trmat; + + // Define which gizmo open on the success + GLGizmosManager::EType gizmo; +}; // Offset of clossed side to model constexpr float SAFE_SURFACE_OFFSET = 0.015f; // [in mm] +/// +/// Create new TextVolume on the surface of ModelObject +/// Should not be stopped +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +class CreateVolumeJob : public Job +{ + DataCreateVolume m_input; + TriangleMesh m_result; + +public: + explicit CreateVolumeJob(DataCreateVolume &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +/// +/// Hold neccessary data to create ModelObject in job +/// Object is placed on bed under screen coor +/// OR to center of scene when it is out of bed shape +/// +struct DataCreateObject +{ + // Hold data about shape + DataBasePtr base; + + // define position on screen where to create object + Vec2d screen_coor; + + // projection property + Camera camera; + + // shape of bed in case of create volume on bed + std::vector bed_shape; + + // Define which gizmo open on the success + GLGizmosManager::EType gizmo; +}; + +/// +/// Create new TextObject on the platter +/// Should not be stopped +/// +class CreateObjectJob : public Job +{ + DataCreateObject m_input; + TriangleMesh m_result; + Transform3d m_transformation; + +public: + explicit CreateObjectJob(DataCreateObject &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +/// +/// Hold neccessary data to create(cut) volume from surface object in job +/// +struct CreateSurfaceVolumeData : public SurfaceVolumeData +{ + // Hold data about shape + DataBasePtr base; + + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // Define which gizmo open on the success + GLGizmosManager::EType gizmo; +}; + +/// +/// Cut surface from object and create cutted volume +/// Should not be stopped +/// +class CreateSurfaceVolumeJob : public Job +{ + CreateSurfaceVolumeData m_input; + TriangleMesh m_result; + +public: + explicit CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + /// /// Assert check of inputs data /// -/// -/// bool check(const DataBase &input, bool check_fontfile = true, bool use_surface = false); +bool check(GLGizmosManager::EType gizmo); +bool check(const CreateVolumeParams& input); bool check(const DataCreateVolume &input, bool is_main_thread = false); bool check(const DataCreateObject &input); bool check(const DataUpdate &input, bool is_main_thread = false, bool use_surface = false); @@ -45,7 +162,8 @@ bool check(const CreateSurfaceVolumeData &input, bool is_main_thread = false); bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread = false); template static ExPolygons create_shape(DataBase &input, Fnc was_canceled); -template static std::vector create_shapes(DataBase &input, Fnc was_canceled); +// create sure that emboss object is bigger than source object [in mm] +constexpr float safe_extension = 1.0f; // /// Try to create mesh from text @@ -56,14 +174,14 @@ template static std::vector create_shapes(DataBase &in /// NOTE: Cache glyphs is changed /// To check if process was canceled /// Triangle mesh model -template static TriangleMesh try_create_mesh(DataBase &input, Fnc was_canceled); -template static TriangleMesh create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl &ctl); +template TriangleMesh try_create_mesh(DataBase &input, const Fnc& was_canceled); +template TriangleMesh create_mesh(DataBase &input, const Fnc& was_canceled, Job::Ctl &ctl); /// /// Create default mesh for embossed text /// /// Not empty model(index trinagle set - its) -static TriangleMesh create_default_mesh(); +TriangleMesh create_default_mesh(); /// /// Must be called on main thread @@ -71,7 +189,14 @@ static TriangleMesh create_default_mesh(); /// New mesh data /// Text configuration, ... /// Transformation of volume -static void update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3d *tr = nullptr); +void update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3d *tr = nullptr); + +/// +/// Update name in right panel +/// +/// Right panel data +/// Volume with just changed name +void update_name_in_list(const ObjectList &object_list, const ModelVolume &volume); /// /// Add new volume to object @@ -81,16 +206,9 @@ static void update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform /// Type of new volume /// Transformation of volume inside of object /// Text configuration and New VolumeName -static void create_volume(TriangleMesh &&mesh, const ObjectID& object_id, - const ModelVolumeType type, const Transform3d trmat, const DataBase &data); - -/// -/// Select Volume from objects -/// -/// All objects in scene -/// Identifier of volume in object -/// Pointer to volume when exist otherwise nullptr -static ModelVolume *get_volume(ModelObjectPtrs &objects, const ObjectID &volume_id); +/// Gizmo to open +void create_volume(TriangleMesh &&mesh, const ObjectID& object_id, const ModelVolumeType type, + const std::optional& trmat, const DataBase &data, GLGizmosManager::EType gizmo); /// /// Create projection for cut surface from mesh @@ -99,7 +217,7 @@ static ModelVolume *get_volume(ModelObjectPtrs &objects, const ObjectID &volume_ /// Convert shape to milimeters /// Bounding box 3d of model volume for projection ranges /// Orthogonal cut_projection -static OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range); +OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range); /// /// Create tranformation for emboss Cutted surface @@ -109,65 +227,82 @@ static OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale /// Text voliume transformation inside object /// Cutted surface from model /// Projection -static OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut); +OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut); /// /// Cut surface into triangle mesh /// -/// (can't be const - cache of font) +/// (can't be const - cache of font) /// SurfaceVolume data /// Check to interupt execution /// Extruded object from cuted surace -static TriangleMesh cut_surface(/*const*/ DataBase &input1, const SurfaceVolumeData &input2, std::function was_canceled); +template +TriangleMesh cut_surface(/*const*/ DataBase &input1, const SurfaceVolumeData &input2, const Fnc& was_canceled); -static void create_message(const std::string &message); // only in finalize -static bool process(std::exception_ptr &eptr); -static bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input); +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Source object volumes for cut surface from +/// Source volume id +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id = {}); + +void create_message(const std::string &message); // only in finalize +bool process(std::exception_ptr &eptr); +bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input); class JobException : public std::runtime_error { -public: JobException(const char* message):runtime_error(message){}}; +public: using std::runtime_error::runtime_error;}; +auto was_canceled(const Job::Ctl &ctl, const DataBase &base){ + return [&ctl, &cancel = base.cancel]() { + if (cancel->load()) + return true; + return ctl.was_canceled(); + }; +} -}// namespace priv +} // namespace + +void Slic3r::GUI::Emboss::DataBase::write(ModelVolume &volume) const{ + volume.name = volume_name; + volume.emboss_shape = shape; + volume.emboss_shape->fix_3mf_tr.reset(); +} ///////////////// /// Create Volume -CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input) - : m_input(std::move(input)) -{ - assert(priv::check(m_input, true)); -} +CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input): m_input(std::move(input)){ assert(check(m_input, true)); } void CreateVolumeJob::process(Ctl &ctl) { - if (!priv::check(m_input)) throw std::runtime_error("Bad input data for EmbossCreateVolumeJob."); - auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); }; - m_result = priv::create_mesh(m_input, was_canceled, ctl); + if (!check(m_input)) + throw std::runtime_error("Bad input data for EmbossCreateVolumeJob."); + m_result = create_mesh(*m_input.base, was_canceled(ctl, *m_input.base), ctl); } void CreateVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; if (m_result.its.empty()) - return priv::create_message("Can't create empty volume."); - - priv::create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, m_input); + return create_message("Can't create empty volume."); + create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, *m_input.base, m_input.gizmo); } ///////////////// /// Create Object -CreateObjectJob::CreateObjectJob(DataCreateObject &&input) - : m_input(std::move(input)) -{ - assert(priv::check(m_input)); -} +CreateObjectJob::CreateObjectJob(DataCreateObject &&input): m_input(std::move(input)){ assert(check(m_input)); } void CreateObjectJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for EmbossCreateObjectJob."); + if (!check(m_input)) + throw JobException("Bad input data for EmbossCreateObjectJob."); - auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); }; - m_result = priv::create_mesh(m_input, was_canceled, ctl); + // can't create new object with using surface + if (m_input.base->shape.projection.use_surface) + m_input.base->shape.projection.use_surface = false; + + auto was_canceled = ::was_canceled(ctl, *m_input.base); + m_result = create_mesh(*m_input.base, was_canceled, ctl); if (was_canceled()) return; // Create new object @@ -181,12 +316,12 @@ void CreateObjectJob::process(Ctl &ctl) bed_shape_.reserve(m_input.bed_shape.size()); for (const Vec2d &p : m_input.bed_shape) bed_shape_.emplace_back(p.cast()); - Polygon bed(bed_shape_); + Slic3r::Polygon bed(bed_shape_); if (!bed.contains(bed_coor.cast())) // mouse pose is out of build plate so create object in center of plate bed_coor = bed.centroid().cast(); - double z = m_input.text_configuration.style.prop.emboss / 2; + double z = m_input.base->shape.projection.depth / 2; Vec3d offset(bed_coor.x(), bed_coor.y(), z); offset -= m_result.center(); Transform3d::TranslationType tt(offset.x(), offset.y(), offset.z()); @@ -195,32 +330,53 @@ void CreateObjectJob::process(Ctl &ctl) void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; // only for sure if (m_result.empty()) - return priv::create_message("Can't create empty object."); + return create_message("Can't create empty object."); GUI_App &app = wxGetApp(); Plater *plater = app.plater(); - ObjectList *obj_list = app.obj_list(); - GLCanvas3D *canvas = plater->canvas3D(); plater->take_snapshot(_L("Add Emboss text object")); - // Create new object and change selection - bool center = false; - obj_list->load_mesh_object(std::move(m_result), m_input.volume_name, - center, &m_input.text_configuration, - &m_transformation); + Model& model = plater->model(); +#ifdef _DEBUG + check_model_ids_validity(model); +#endif /* _DEBUG */ + { + // INFO: inspiration for create object is from ObjectList::load_mesh_object() + ModelObject *new_object = model.add_object(); + new_object->name = m_input.base->volume_name; + new_object->add_instance(); // each object should have at list one instance + + ModelVolume *new_volume = new_object->add_volume(std::move(m_result)); + // set a default extruder value, since user can't add it manually + new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); + // write emboss data into volume + m_input.base->write(*new_volume); + + // set transformation + Slic3r::Geometry::Transformation tr(m_transformation); + new_object->instances.front()->set_transformation(tr); + new_object->ensure_on_bed(); + + // Actualize right panel and set inside of selection + app.obj_list()->paste_objects_into_list({model.objects.size() - 1}); + } +#ifdef _DEBUG + check_model_ids_validity(model); +#endif /* _DEBUG */ // When add new object selection is empty. // When cursor move and no one object is selected than // Manager::reset_all() So Gizmo could be closed before end of creation object + GLCanvas3D *canvas = plater->canvas3D(); GLGizmosManager &manager = canvas->get_gizmos_manager(); - if (manager.get_current_type() != GLGizmosManager::Emboss) - manager.open_gizmo(GLGizmosManager::Emboss); + if (manager.get_current_type() != m_input.gizmo) + manager.open_gizmo(m_input.gizmo); // redraw scene canvas->reload_scene(true); @@ -228,87 +384,83 @@ void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) ///////////////// /// Update Volume -UpdateJob::UpdateJob(DataUpdate&& input) - : m_input(std::move(input)) -{ - assert(priv::check(m_input, true)); -} +UpdateJob::UpdateJob(DataUpdate&& input): m_input(std::move(input)){ assert(check(m_input, true)); } void UpdateJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for EmbossUpdateJob."); + if (!check(m_input)) + throw JobException("Bad input data for EmbossUpdateJob."); - auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool { - if (cancel->load()) return true; - return ctl.was_canceled(); - }; - m_result = priv::try_create_mesh(m_input, was_canceled); + auto was_canceled = ::was_canceled(ctl, *m_input.base); + m_result = ::try_create_mesh(*m_input.base, was_canceled); if (was_canceled()) return; if (m_result.its.empty()) - throw priv::JobException("Created text volume is empty. Change text or font."); + throw JobException("Created text volume is empty. Change text or font."); } void UpdateJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; - priv::update_volume(std::move(m_result), m_input); + ::update_volume(std::move(m_result), m_input); } -namespace Slic3r::GUI::Emboss { - -SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id) +void UpdateJob::update_volume(ModelVolume *volume, TriangleMesh &&mesh, const DataBase &base) { - SurfaceVolumeData::ModelSources result; - result.reserve(volumes.size() - 1); - for (const ModelVolume *v : volumes) { - if (text_volume_id.has_value() && v->id().id == *text_volume_id) continue; - // skip modifiers and negative volumes, ... - if (!v->is_model_part()) continue; - const TriangleMesh &tm = v->mesh(); - if (tm.empty()) continue; - if (tm.its.empty()) continue; - result.push_back({v->get_mesh_shared_ptr(), v->get_matrix()}); - } - return result; + // check inputs + bool is_valid_input = volume != nullptr && !mesh.empty() && !base.volume_name.empty(); + assert(is_valid_input); + if (!is_valid_input) + return; + + // update volume + volume->set_mesh(std::move(mesh)); + volume->set_new_unique_id(); + volume->calculate_convex_hull(); + + // write data from base into volume + base.write(*volume); + + GUI_App &app = wxGetApp(); // may be move to input + if (volume->name != base.volume_name) { + volume->name = base.volume_name; + + const ObjectList *obj_list = app.obj_list(); + if (obj_list != nullptr) + update_name_in_list(*obj_list, *volume); } -SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume *text_volume) -{ - if (text_volume == nullptr) return {}; - if (!text_volume->text_configuration.has_value()) return {}; - const ModelVolumePtrs &volumes = text_volume->get_object()->volumes; - // no other volume in object - if (volumes.size() <= 1) return {}; - return create_sources(volumes, text_volume->id().id); + ModelObject *object = volume->get_object(); + assert(object != nullptr); + if (object == nullptr) + return; + + + Plater *plater = app.plater(); + if (plater->printer_technology() == ptSLA) + sla::reproject_points_and_holes(object); + plater->changed_object(*object); } - - -} // namespace Slic3r::GUI::Emboss - ///////////////// /// Create Surface volume CreateSurfaceVolumeJob::CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input) : m_input(std::move(input)) { - assert(priv::check(m_input, true)); + assert(check(m_input, true)); } void CreateSurfaceVolumeJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for CreateSurfaceVolumeJob."); - // check cancelation of process - auto was_canceled = [&ctl]() -> bool { return ctl.was_canceled(); }; - m_result = priv::cut_surface(m_input, m_input, was_canceled); + if (!check(m_input)) + throw JobException("Bad input data for CreateSurfaceVolumeJob."); + m_result = cut_surface(*m_input.base, m_input, was_canceled(ctl, *m_input.base)); } void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; - priv::create_volume(std::move(m_result), m_input.object_id, - m_input.volume_type, m_input.text_tr, m_input); + create_volume(std::move(m_result), m_input.object_id, + m_input.volume_type, m_input.transform, *m_input.base, m_input.gizmo); } ///////////////// @@ -316,173 +468,350 @@ void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { UpdateSurfaceVolumeJob::UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input) : m_input(std::move(input)) { - assert(priv::check(m_input, true)); + assert(check(m_input, true)); } void UpdateSurfaceVolumeJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for UseSurfaceJob."); - - // check cancelation of process - auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool { - if (cancel->load()) return true; - return ctl.was_canceled(); - }; - m_result = priv::cut_surface(m_input, m_input, was_canceled); + if (!check(m_input)) + throw JobException("Bad input data for UseSurfaceJob."); + m_result = cut_surface(*m_input.base, m_input, was_canceled(ctl, *m_input.base)); } void UpdateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; // when start using surface it is wanted to move text origin on surface of model // also when repeteadly move above surface result position should match - Transform3d *tr = &m_input.text_tr; - priv::update_volume(std::move(m_result), m_input, tr); + ::update_volume(std::move(m_result), m_input, &m_input.transform); } +namespace { +/// +/// Check if volume type is possible use for new text volume +/// +/// Type +/// True when allowed otherwise false +bool is_valid(ModelVolumeType volume_type); + +/// +/// Start job for add new volume to object with given transformation +/// +/// Define where to queue the job. e.g. wxGetApp().plater()->get_ui_job_worker() +/// Define where to add +/// Wanted volume transformation, when not set will be calculated after creation to be near the object +/// Define what to emboss - shape +/// Type of volume: Part, negative, modifier +/// Define which gizmo open on the success +/// Nullptr when job is sucessfully add to worker otherwise return data to be processed different way +bool start_create_volume_job(Worker &worker, + const ModelObject &object, + const std::optional &volume_tr, + DataBasePtr data, + ModelVolumeType volume_type, + GLGizmosManager::EType gizmo); + +/// +/// Find volume in selected objects with closest convex hull to screen center. +/// +/// Define where to search for closest +/// Canvas center(dependent on camera settings) +/// Actual objects +/// OUT: coordinate of controid of closest volume +/// closest volume when exists otherwise nullptr +const GLVolume *find_closest( + const Selection &selection, const Vec2d &screen_center, const Camera &camera, const ModelObjectPtrs &objects, Vec2d *closest_center); + +/// +/// Start job for add object with text into scene +/// +/// Contain worker, build shape, gizmo +/// Define params for create volume +/// Screen coordinat, where to create new object laying on bed +/// True when can add job to worker otherwise FALSE +bool start_create_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor); + +/// +/// Start job to create volume on the surface of object +/// +/// Variabless needed to create volume +/// Describe what to emboss - shape +/// Where to add +/// True .. try to create volume without screen_coor, +/// False .. +/// Nullptr when job is sucessfully add to worker otherwise return data to be processed different way +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &screen_coor, bool try_no_coor); + +} // namespace + +namespace Slic3r::GUI::Emboss { + +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume &text_volume) +{ + const ModelVolumePtrs &volumes = text_volume.get_object()->volumes; + // no other volume in object + if (volumes.size() <= 1) + return {}; + return ::create_sources(volumes, text_volume.id().id); +} + +bool start_create_volume(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos) +{ + if (data == nullptr) + return false; + if (!check(input)) + return false; + + if (input.gl_volume == nullptr) + // object is not under mouse position soo create object on plater + return ::start_create_object_job(input, std::move(data), mouse_pos); + + bool try_no_coor = true; + return ::start_create_volume_on_surface_job(input, std::move(data), mouse_pos, try_no_coor); +} + +bool start_create_volume_without_position(CreateVolumeParams &input, DataBasePtr data) +{ + assert(data != nullptr); + if (data == nullptr) + return false; + if (!check(input)) + return false; + + // select position by camera position and view direction + const Selection &selection = input.canvas.get_selection(); + int object_idx = selection.get_object_idx(); + + Size s = input.canvas.get_canvas_size(); + Vec2d screen_center(s.get_width() / 2., s.get_height() / 2.); + const ModelObjectPtrs &objects = selection.get_model()->objects; + + // No selected object so create new object + if (selection.is_empty() || object_idx < 0 || + static_cast(object_idx) >= objects.size()) + // create Object on center of screen + // when ray throw center of screen not hit bed it create object on center of bed + return ::start_create_object_job(input, std::move(data), screen_center); + + // create volume inside of selected object + Vec2d coor; + const Camera &camera = wxGetApp().plater()->get_camera(); + input.gl_volume = ::find_closest(selection, screen_center, camera, objects, &coor); + if (input.gl_volume == nullptr) + return ::start_create_object_job(input, std::move(data), screen_center); + + bool try_no_coor = false; + return ::start_create_volume_on_surface_job(input, std::move(data), coor, try_no_coor); +} + +#ifdef EXECUTE_UPDATE_ON_MAIN_THREAD +namespace { +// Run Job on main thread (blocking) - ONLY DEBUG +static inline bool execute_job(std::shared_ptr j) +{ + struct MyCtl : public Job::Ctl + { + void update_status(int st, const std::string &msg = "") override{}; + bool was_canceled() const override { return false; } + std::future call_on_main_thread(std::function fn) override { return std::future{}; } + } ctl; + j->process(ctl); + wxGetApp().plater()->CallAfter([j]() { + std::exception_ptr e_ptr = nullptr; + j->finalize(false, e_ptr); + }); + return true; +} +} // namespace +#endif + +bool start_update_volume(DataUpdate &&data, const ModelVolume &volume, const Selection &selection, RaycastManager& raycaster) +{ + assert(data.volume_id == volume.id()); + + // check cutting from source mesh + bool &use_surface = data.base->shape.projection.use_surface; + if (use_surface && volume.is_the_only_one_part()) + use_surface = false; + + std::unique_ptr job = nullptr; + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_volume_sources(volume); + if (sources.empty()) + return false; + + Transform3d volume_tr = volume.get_matrix(); + const std::optional &fix_3mf = volume.emboss_shape->fix_3mf_tr; + if (fix_3mf.has_value()) + volume_tr = volume_tr * fix_3mf->inverse(); + + // when it is new applying of use surface than move origin onto surfaca + if (!volume.emboss_shape->projection.use_surface) { + auto offset = calc_surface_offset(selection, raycaster); + if (offset.has_value()) + volume_tr *= Eigen::Translation(*offset); + } + + UpdateSurfaceVolumeData surface_data{std::move(data), {volume_tr, std::move(sources)}}; + job = std::make_unique(std::move(surface_data)); + } else { + job = std::make_unique(std::move(data)); + } + +#ifndef EXECUTE_UPDATE_ON_MAIN_THREAD + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + return queue_job(worker, std::move(job)); +#else + // Run Job on main thread (blocking) - ONLY DEBUG + return execute_job(std::move(job)); +#endif // EXECUTE_UPDATE_ON_MAIN_THREAD +} + +} // namespace Slic3r::GUI::Emboss //////////////////////////// /// private namespace implementation -bool priv::check(const DataBase &input, bool check_fontfile, bool use_surface) +namespace { +bool check(const DataBase &input, bool check_fontfile, bool use_surface) { bool res = true; - if (check_fontfile) { - assert(input.font_file.has_value()); - res &= input.font_file.has_value(); - } - assert(!input.text_configuration.fix_3mf_tr.has_value()); - res &= !input.text_configuration.fix_3mf_tr.has_value(); - assert(!input.text_configuration.text.empty()); - res &= !input.text_configuration.text.empty(); + // if (check_fontfile) { + // assert(input.font_file.has_value()); + // res &= input.font_file.has_value(); + // } + // assert(!input.text_configuration.fix_3mf_tr.has_value()); + // res &= !input.text_configuration.fix_3mf_tr.has_value(); + // assert(!input.text_configuration.text.empty()); + // res &= !input.text_configuration.text.empty(); assert(!input.volume_name.empty()); res &= !input.volume_name.empty(); - const FontProp& prop = input.text_configuration.style.prop; - assert(prop.use_surface == use_surface); - res &= prop.use_surface == use_surface; - assert(prop.per_glyph == !input.text_lines.empty()); - res &= prop.per_glyph == !input.text_lines.empty(); - if (prop.per_glyph) { - assert(get_count_lines(input.text_configuration.text) == input.text_lines.size()); - res &= get_count_lines(input.text_configuration.text) == input.text_lines.size(); + //const FontProp& prop = input.text_configuration.style.prop; + //assert(prop.per_glyph == !input.text_lines.empty()); + //res &= prop.per_glyph == !input.text_lines.empty(); + //if (prop.per_glyph) { + // assert(get_count_lines(input.text_configuration.text) == input.text_lines.size()); + // res &= get_count_lines(input.text_configuration.text) == input.text_lines.size(); + //} + return res; +} + +bool check(GLGizmosManager::EType gizmo) +{ + assert(gizmo == GLGizmosManager::Emboss || gizmo == GLGizmosManager::Svg); + return gizmo == GLGizmosManager::Emboss || gizmo == GLGizmosManager::Svg; } +bool check(const CreateVolumeParams &input) +{ + bool res = is_valid(input.volume_type); + auto gizmo_type = static_cast(input.gizmo); + res &= ::check(gizmo_type); return res; } -bool priv::check(const DataCreateVolume &input, bool is_main_thread) { +bool check(const DataCreateVolume &input, bool is_main_thread) +{ bool check_fontfile = false; - bool res = check((DataBase) input, check_fontfile); - assert(input.volume_type != ModelVolumeType::INVALID); - res &= input.volume_type != ModelVolumeType::INVALID; - assert(input.object_id.id >= 0); - res &= input.object_id.id >= 0; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile); + res &= is_valid(input.volume_type); + res &= check(input.gizmo); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; return res; } -bool priv::check(const DataCreateObject &input) { +bool check(const DataCreateObject &input) +{ bool check_fontfile = false; - bool res = check((DataBase) input, check_fontfile); + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile); assert(input.screen_coor.x() >= 0.); res &= input.screen_coor.x() >= 0.; assert(input.screen_coor.y() >= 0.); res &= input.screen_coor.y() >= 0.; assert(input.bed_shape.size() >= 3); // at least triangle res &= input.bed_shape.size() >= 3; + res &= check(input.gizmo); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; return res; } -bool priv::check(const DataUpdate &input, bool is_main_thread, bool use_surface){ +bool check(const DataUpdate &input, bool is_main_thread, bool use_surface) +{ bool check_fontfile = true; - bool res = check((DataBase) input, check_fontfile, use_surface); - assert(input.volume_id.id >= 0); - res &= input.volume_id.id >= 0; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile, use_surface); if (is_main_thread) - assert(get_volume(wxGetApp().model().objects, input.volume_id) != nullptr); - assert(input.cancel != nullptr); - res &= input.cancel != nullptr; + assert(get_model_volume(input.volume_id, wxGetApp().model().objects) != nullptr); + assert(input.base->cancel != nullptr); + res &= input.base->cancel != nullptr; if (is_main_thread) - assert(!input.cancel->load()); + assert(!input.base->cancel->load()); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; return res; } -bool priv::check(const CreateSurfaceVolumeData &input, bool is_main_thread) +bool check(const CreateSurfaceVolumeData &input, bool is_main_thread) { bool use_surface = true; - bool res = check((DataBase)input, is_main_thread, use_surface); + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, is_main_thread, use_surface); assert(!input.sources.empty()); res &= !input.sources.empty(); + res &= check(input.gizmo); + assert(input.base->shape.projection.use_surface); + res &= input.base->shape.projection.use_surface; return res; } -bool priv::check(const UpdateSurfaceVolumeData &input, bool is_main_thread){ +bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread) +{ bool use_surface = true; - bool res = check((DataUpdate)input, is_main_thread, use_surface); + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, is_main_thread, use_surface); assert(!input.sources.empty()); res &= !input.sources.empty(); + assert(input.base->shape.projection.use_surface); + res &= input.base->shape.projection.use_surface; return res; } template -ExPolygons priv::create_shape(DataBase &input, Fnc was_canceled) { - FontFileWithCache &font = input.font_file; - const TextConfiguration &tc = input.text_configuration; - const char *text = tc.text.c_str(); - const FontProp &prop = tc.style.prop; - assert(!prop.per_glyph); - assert(font.has_value()); - if (!font.has_value()) - return {}; - - ExPolygons shapes = text2shapes(font, text, prop, was_canceled); - if (shapes.empty()) - return {}; - - return shapes; -} - -template -std::vector priv::create_shapes(DataBase &input, Fnc was_canceled) { - FontFileWithCache &font = input.font_file; - const TextConfiguration &tc = input.text_configuration; - const char *text = tc.text.c_str(); - const FontProp &prop = tc.style.prop; - assert(prop.per_glyph); - assert(font.has_value()); - if (!font.has_value()) - return {}; - - std::wstring ws = boost::nowide::widen(text); - std::vector shapes = text2vshapes(font, ws, prop, was_canceled); - if (shapes.empty()) - return {}; - - if (was_canceled()) - return {}; - - return shapes; +ExPolygons create_shape(DataBase &input, Fnc was_canceled) { + EmbossShape &es = input.create_shape(); + // TODO: improve to use real size of volume + // ... need world matrix for volume + // ... printer resolution will be fine too + return union_with_delta(es, UNION_DELTA, UNION_MAX_ITERATIN); } //#define STORE_SAMPLING #ifdef STORE_SAMPLING #include "libslic3r/SVG.hpp" #endif // STORE_SAMPLING -namespace { -std::vector create_line_bounds(const std::vector &shapes, const std::wstring& text, size_t count_lines = 0) +std::vector create_line_bounds(const ExPolygonsWithIds &shapes, size_t count_lines = 0) { - assert(text.size() == shapes.size()); if (count_lines == 0) - count_lines = get_count_lines(text); - assert(count_lines == get_count_lines(text)); + count_lines = get_count_lines(shapes); + assert(count_lines == get_count_lines(shapes)); std::vector result(count_lines); size_t text_line_index = 0; // s_i .. shape index - for (size_t s_i = 0; s_i < shapes.size(); ++s_i) { - const ExPolygons &shape = shapes[s_i]; + for (const ExPolygonsWithId &shape_id: shapes) { + const ExPolygons &shape = shape_id.expoly; BoundingBox bb; if (!shape.empty()) { bb = get_extents(shape); } BoundingBoxes &line_bbs = result[text_line_index]; line_bbs.push_back(bb); - if (text[s_i] == '\n'){ + if (shape_id.id == ENTER_UNICODE) { // skip enters on beginig and tail ++text_line_index; } @@ -495,27 +824,22 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w // method use square of coord stored into int64_t static_assert(std::is_same()); - std::vector shapes = priv::create_shapes(input, was_canceled); - if (shapes.empty()) + const EmbossShape &shape = input.create_shape(); + if (shape.shapes_with_ids.empty()) return {}; // Precalculate bounding boxes of glyphs // Separate lines of text to vector of Bounds - const TextConfiguration &tc = input.text_configuration; - std::wstring ws = boost::nowide::widen(tc.text.c_str()); - assert(get_count_lines(ws) == input.text_lines.size()); + assert(get_count_lines(shape.shapes_with_ids) == input.text_lines.size()); size_t count_lines = input.text_lines.size(); - std::vector bbs = create_line_bounds(shapes, ws, count_lines); + std::vector bbs = create_line_bounds(shape.shapes_with_ids, count_lines); - const FontProp &prop = tc.style.prop; - FontFileWithCache &font = input.font_file; - double shape_scale = get_shape_scale(prop, *font.font_file); - double projec_scale = shape_scale / SHAPE_SCALE; - double depth = prop.emboss / projec_scale; - auto scale_tr = Eigen::Scaling(projec_scale); + double depth = shape.projection.depth / shape.scale; + auto scale_tr = Eigen::Scaling(shape.scale); // half of font em size for direction of letter emboss - double em_2_mm = prop.size_in_mm / 2.; + // double em_2_mm = prop.size_in_mm / 2.; // TODO: fix it + double em_2_mm = 5.; int32_t em_2_polygon = static_cast(std::round(scale_(em_2_mm))); size_t s_i_offset = 0; // shape index offset(for next lines) @@ -523,7 +847,7 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w for (size_t text_line_index = 0; text_line_index < input.text_lines.size(); ++text_line_index) { const BoundingBoxes &line_bbs = bbs[text_line_index]; const TextLine &line = input.text_lines[text_line_index]; - PolygonPoints samples = sample_slice(line, line_bbs, shape_scale); + PolygonPoints samples = sample_slice(line, line_bbs, shape.scale); std::vector angles = calculate_angles(em_2_polygon, samples, line.polygon); for (size_t i = 0; i < line_bbs.size(); ++i) { @@ -531,10 +855,11 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w if (!letter_bb.defined) continue; - Vec2d to_zero_vec = letter_bb.center().cast() * shape_scale; // [in mm] - float surface_offset = input.is_outside ? -priv::SAFE_SURFACE_OFFSET : (-prop.emboss + priv::SAFE_SURFACE_OFFSET); - if (prop.distance.has_value()) - surface_offset += *prop.distance; + Vec2d to_zero_vec = letter_bb.center().cast() * shape.scale; // [in mm] + float surface_offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (-shape.projection.depth + SAFE_SURFACE_OFFSET); + + if (input.from_surface.has_value()) + surface_offset += *input.from_surface; Eigen::Translation to_zero(-to_zero_vec.x(), 0., static_cast(surface_offset)); @@ -546,7 +871,7 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w Eigen::Translation offset_tr(offset_vec.x(), 0., -offset_vec.y()); Transform3d tr = offset_tr * rotate * to_zero * scale_tr; - const ExPolygons &letter_shape = shapes[s_i_offset + i]; + const ExPolygons &letter_shape = shape.shapes_with_ids[s_i_offset + i].expoly; assert(get_extents(letter_shape) == letter_bb); auto projectZ = std::make_unique(depth); ProjectTransform project(std::move(projectZ), tr); @@ -585,11 +910,10 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w } return TriangleMesh(std::move(result)); } -} // namespace template -TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) +TriangleMesh try_create_mesh(DataBase &input, const Fnc& was_canceled) { if (!input.text_lines.empty()) { TriangleMesh tm = create_mesh_per_glyph(input, was_canceled); @@ -597,17 +921,17 @@ TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) if (!tm.empty()) return tm; } - ExPolygons shapes = priv::create_shape(input, was_canceled); + ExPolygons shapes = create_shape(input, was_canceled); if (shapes.empty()) return {}; if (was_canceled()) return {}; - const FontProp &prop = input.text_configuration.style.prop; - const FontFile &ff = *input.font_file.font_file; // NOTE: SHAPE_SCALE is applied in ProjectZ - double scale = get_shape_scale(prop, ff) / SHAPE_SCALE; - double depth = prop.emboss / scale; + double scale = input.shape.scale; + double depth = input.shape.projection.depth / scale; auto projectZ = std::make_unique(depth); - float offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (SAFE_SURFACE_OFFSET - prop.emboss); + float offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (SAFE_SURFACE_OFFSET - input.shape.projection.depth); + if (input.from_surface.has_value()) + offset += *input.from_surface; Transform3d tr = Eigen::Translation(0., 0.,static_cast(offset)) * Eigen::Scaling(scale); ProjectTransform project(std::move(projectZ), tr); if (was_canceled()) return {}; @@ -615,23 +939,21 @@ TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) } template -TriangleMesh priv::create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl& ctl) +TriangleMesh create_mesh(DataBase &input, const Fnc& was_canceled, Job::Ctl& ctl) { // It is neccessary to create some shape // Emboss text window is opened by creation new emboss text object - TriangleMesh result; - if (input.font_file.has_value()) { - result = try_create_mesh(input, was_canceled); - if (was_canceled()) return {}; - } + TriangleMesh result = try_create_mesh(input, was_canceled); + if (was_canceled()) + return {}; if (result.its.empty()) { - result = priv::create_default_mesh(); - if (was_canceled()) return {}; + result = create_default_mesh(); + if (was_canceled()) + return {}; // only info ctl.call_on_main_thread([]() { - create_message("It is used default volume for embossed " - "text, try to change text or font to fix it."); + create_message("It is used default volume for embossed text, try to change text or font to fix it."); }); } @@ -639,7 +961,7 @@ TriangleMesh priv::create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl& ctl) return result; } -TriangleMesh priv::create_default_mesh() +TriangleMesh create_default_mesh() { // When cant load any font use default object loaded from file std::string path = Slic3r::resources_dir() + "/data/embossed_text.obj"; @@ -651,105 +973,56 @@ TriangleMesh priv::create_default_mesh() return triangle_mesh; } -namespace{ -void update_volume_name(const ModelVolume &volume, const ObjectList *obj_list) +void update_name_in_list(const ObjectList& object_list, const ModelVolume& volume) { - if (obj_list == nullptr) + const ModelObjectPtrs *objects_ptr = object_list.objects(); + if (objects_ptr == nullptr) return; - const std::vector* objects = obj_list->objects(); - if (objects == nullptr) - return; + const ModelObjectPtrs &objects = *objects_ptr; + const ModelObject *object = volume.get_object(); + const ObjectID &object_id = object->id(); - int object_idx = -1; - int volume_idx = -1; - for (size_t oi = 0; oi < objects->size(); ++oi) { - const ModelObject *mo = objects->at(oi); - if (mo == nullptr) - continue; - if (volume.get_object()->id() != mo->id()) - continue; - const ModelVolumePtrs& volumes = mo->volumes; - for (size_t vi = 0; vi < volumes.size(); ++vi) { - const ModelVolume *mv = volumes[vi]; - if (mv == nullptr) - continue; - if (mv->id() == volume.id()){ - object_idx = static_cast(oi); - volume_idx = static_cast(vi); - break; - } - } - if (volume_idx > 0) + // search for index of object + int object_index = -1; + for (size_t i = 0; i < objects.size(); ++i) + if (objects[i]->id() == object_id) { + object_index = static_cast(i); break; } - obj_list->update_name_in_list(object_idx, volume_idx); -} -} -void UpdateJob::update_volume(ModelVolume *volume, - TriangleMesh &&mesh, - const TextConfiguration &text_configuration, - std::string_view volume_name) -{ - // check inputs - bool is_valid_input = - volume != nullptr && - !mesh.empty() && - !volume_name.empty(); - assert(is_valid_input); - if (!is_valid_input) return; - - // update volume - volume->set_mesh(std::move(mesh)); - volume->set_new_unique_id(); - volume->calculate_convex_hull(); - volume->get_object()->invalidate_bounding_box(); - volume->text_configuration = text_configuration; - - // discard information about rotation, should not be stored in volume - volume->text_configuration->style.prop.angle.reset(); + const ModelVolumePtrs volumes = object->volumes; + const ObjectID &volume_id = volume.id(); - GUI_App &app = wxGetApp(); // may be move ObjectList and Plater to input? - - // update volume name in right panel( volume / object name) - if (volume->name != volume_name) { - volume->name = volume_name; - update_volume_name(*volume, app.obj_list()); + // search for index of volume + int volume_index = -1; + for (size_t i = 0; i < volumes.size(); ++i) + if (volumes[i]->id() == volume_id) { + volume_index = static_cast(i); + break; } - // When text is object. - // When text positive volume is lowest part of object than modification of text - // have to move object on bed. - if (volume->type() == ModelVolumeType::MODEL_PART) - volume->get_object()->ensure_on_bed(); - - // redraw scene - Plater *plater = app.plater(); - if (plater == nullptr) + if (object_index < 0 || volume_index < 0) return; - // Update Model and redraw scene - plater->update(); + object_list.update_name_in_list(object_index, volume_index); } -void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3d* tr) +void update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3d *tr) { // for sure that some object will be created if (mesh.its.empty()) return create_message("Empty mesh can't be created."); Plater *plater = wxGetApp().plater(); - GLCanvas3D *canvas = plater->canvas3D(); + // Check gizmo is still open otherwise job should be canceled + assert(plater->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss || + plater->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Svg); - // Check emboss gizmo is still open - GLGizmosManager &manager = canvas->get_gizmos_manager(); - if (manager.get_current_type() != GLGizmosManager::Emboss) - return; - - std::string snap_name = GUI::format(_L("Text: %1%"), data.text_configuration.text); + // TRN: This is the name of the action appearing in undo/redo stack. + std::string snap_name = _u8L("Text/SVG attribute change"); Plater::TakeSnapshot snapshot(plater, snap_name, UndoRedo::SnapshotType::GizmoAction); - ModelVolume *volume = get_volume(plater->model().objects, data.volume_id); + ModelVolume *volume = get_model_volume(data.volume_id, plater->model().objects); // could appear when user delete edited volume if (volume == nullptr) @@ -759,18 +1032,21 @@ void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3 volume->set_transformation(*tr); } else { // apply fix matrix made by store to .3mf - const auto &tc = volume->text_configuration; - assert(tc.has_value()); - if (tc.has_value() && tc->fix_3mf_tr.has_value()) - volume->set_transformation(volume->get_matrix() * tc->fix_3mf_tr->inverse()); + const std::optional &emboss_shape = volume->emboss_shape; + assert(emboss_shape.has_value()); + if (emboss_shape.has_value() && emboss_shape->fix_3mf_tr.has_value()) + volume->set_transformation(volume->get_matrix() * emboss_shape->fix_3mf_tr->inverse()); } - UpdateJob::update_volume(volume, std::move(mesh), data.text_configuration, data.volume_name); + UpdateJob::update_volume(volume, std::move(mesh), *data.base); } -void priv::create_volume( - TriangleMesh &&mesh, const ObjectID& object_id, - const ModelVolumeType type, const Transform3d trmat, const DataBase &data) +void create_volume(TriangleMesh &&mesh, + const ObjectID &object_id, + const ModelVolumeType type, + const std::optional &trmat, + const DataBase &data, + GLGizmosManager::EType gizmo) { GUI_App &app = wxGetApp(); Plater *plater = app.plater(); @@ -791,13 +1067,19 @@ void priv::create_volume( // Parent object for text volume was propably removed. // Assumption: User know what he does, so text volume is no more needed. if (obj == nullptr) - return priv::create_message("Bad object to create volume."); + return create_message("Bad object to create volume."); if (mesh.its.empty()) - return priv::create_message("Can't create empty volume."); + return create_message("Can't create empty volume."); plater->take_snapshot(_L("Add Emboss text Volume")); + BoundingBoxf3 instance_bb; + if (!trmat.has_value()) { + // used for align to instance + size_t instance_index = 0; // must exist + instance_bb = obj->instance_bounding_box(instance_index); + } // NOTE: be carefull add volume also center mesh !!! // So first add simple shape(convex hull is also calculated) ModelVolume *volume = obj->add_volume(make_cube(1., 1., 1.), type); @@ -815,12 +1097,25 @@ void priv::create_volume( volume->source.is_from_builtin_objects = true; volume->name = data.volume_name; // copy - volume->text_configuration = data.text_configuration; // copy - // discard information about rotation, should not be stored in volume - volume->text_configuration->style.prop.angle.reset(); + if (trmat.has_value()) { + volume->set_transformation(*trmat); + } else { + assert(!data.shape.projection.use_surface); + // Create transformation for volume near from object(defined by glVolume) + // Transformation is inspired add generic volumes in ObjectList::load_generic_subobject + Vec3d volume_size = volume->mesh().bounding_box().size(); + // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. + Vec3d offset_tr(0, // center of instance - Can't suggest width of text before it will be created + -instance_bb.size().y() / 2 - volume_size.y() / 2, // under + volume_size.z() / 2 - instance_bb.size().z() / 2); // lay on bed + // use same instance as for calculation of instance_bounding_box + Transform3d tr = obj->instances.front()->get_transformation().get_matrix_no_offset().inverse(); + Transform3d volume_trmat = tr * Eigen::Translation3d(offset_tr); + volume->set_transformation(volume_trmat); + } - volume->set_transformation(trmat); + data.write(*volume); // update printable state on canvas if (type == ModelVolumeType::MODEL_PART) { @@ -835,36 +1130,25 @@ void priv::create_volume( // when new volume is created change selection to this volume auto add_to_selection = [volume](const ModelVolume *vol) { return vol == volume; }; wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(object_idx, add_to_selection); - if (!sel.IsEmpty()) obj_list->select_item(sel.front()); + if (!sel.IsEmpty()) + obj_list->select_item(sel.front()); obj_list->selection_changed(); // Now is valid text volume selected open emboss gizmo GLGizmosManager &manager = canvas->get_gizmos_manager(); - if (manager.get_current_type() != GLGizmosManager::Emboss) - manager.open_gizmo(GLGizmosManager::Emboss); + if (manager.get_current_type() != gizmo) + manager.open_gizmo(gizmo); // update model and redraw scene //canvas->reload_scene(true); plater->update(); } -ModelVolume *priv::get_volume(ModelObjectPtrs &objects, - const ObjectID &volume_id) +OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range) { - for (ModelObject *obj : objects) - for (ModelVolume *vol : obj->volumes) - if (vol->id() == volume_id) return vol; - return nullptr; -}; - -OrthoProject priv::create_projection_for_cut( - Transform3d tr, - double shape_scale, - const std::pair &z_range) -{ - double min_z = z_range.first - priv::safe_extension; - double max_z = z_range.second + priv::safe_extension; + double min_z = z_range.first - safe_extension; + double max_z = z_range.second + safe_extension; assert(min_z < max_z); // range between min and max value double projection_size = max_z - min_z; @@ -883,8 +1167,7 @@ OrthoProject priv::create_projection_for_cut( return OrthoProject(tr, project_direction); } -OrthoProject3d priv::create_emboss_projection( - bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut) +OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut) { float front_move = (is_outside) ? emboss : SAFE_SURFACE_OFFSET, @@ -894,14 +1177,11 @@ OrthoProject3d priv::create_emboss_projection( return OrthoProject3d(from_front_to_back); } -namespace { indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transform3d& tr,const SurfaceVolumeData::ModelSources &sources, DataBase& input, std::function was_canceled) { assert(!sources.empty()); BoundingBox bb = get_extents(shapes); - const FontFile &ff = *input.font_file.font_file; - const FontProp &fp = input.text_configuration.style.prop; - double shape_scale = get_shape_scale(fp, ff); + double shape_scale = input.shape.scale; const SurfaceVolumeData::ModelSource *biggest = &sources.front(); @@ -914,10 +1194,11 @@ indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transfor Transform3d mesh_tr_inv = s.tr.inverse(); Transform3d cut_projection_tr = mesh_tr_inv * tr; std::pair z_range{0., 1.}; - OrthoProject cut_projection = priv::create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); // copy only part of source model indexed_triangle_set its = its_cut_AoI(s.mesh->its, bb, cut_projection); - if (its.indices.empty()) continue; + if (its.indices.empty()) + continue; if (biggest_count < its.vertices.size()) { biggest_count = its.vertices.size(); biggest = &s; @@ -936,8 +1217,9 @@ indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transfor size_t itss_index = s_to_itss[biggest - &sources.front()]; BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]); for (const SurfaceVolumeData::ModelSource &s : sources) { - size_t itss_index = s_to_itss[&s - &sources.front()]; - if (itss_index == std::numeric_limits::max()) continue; + itss_index = s_to_itss[&s - &sources.front()]; + if (itss_index == std::numeric_limits::max()) + continue; if (&s == biggest) continue; @@ -945,17 +1227,17 @@ indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transfor bool fix_reflected = true; indexed_triangle_set &its = itss[itss_index]; its_transform(its, tr, fix_reflected); - BoundingBoxf3 bb = bounding_box(its); - mesh_bb.merge(bb); + BoundingBoxf3 its_bb = bounding_box(its); + mesh_bb.merge(its_bb); } // tr_inv = transformation of mesh inverted Transform3d emboss_tr = cut_projection_tr.inverse(); BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr); std::pair z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()}; - OrthoProject cut_projection = priv::create_projection_for_cut(cut_projection_tr, shape_scale, z_range); - float projection_ratio = (-z_range.first + priv::safe_extension) / - (z_range.second - z_range.first + 2 * priv::safe_extension); + OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + float projection_ratio = (-z_range.first + safe_extension) / + (z_range.second - z_range.first + 2 * safe_extension); ExPolygons shapes_data; // is used only when text is reflected to reverse polygon points order const ExPolygons *shapes_ptr = &shapes; @@ -986,31 +1268,26 @@ indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transfor if (was_canceled()) return {}; // !! Projection needs to transform cut - OrthoProject3d projection = priv::create_emboss_projection(input.is_outside, fp.emboss, emboss_tr, cut); + OrthoProject3d projection = create_emboss_projection(input.is_outside, input.shape.projection.depth, emboss_tr, cut); return cut2model(cut, projection); } TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &input2, std::function was_canceled) { - std::vector shapes = priv::create_shapes(input1, was_canceled); - if (was_canceled()) return {}; - if (shapes.empty()) - throw priv::JobException(_u8L("Font doesn't have any shape for given text.").c_str()); // Precalculate bounding boxes of glyphs // Separate lines of text to vector of Bounds - const TextConfiguration &tc = input1.text_configuration; - std::wstring ws = boost::nowide::widen(tc.text.c_str()); - assert(get_count_lines(ws) == input1.text_lines.size()); - size_t count_lines = input1.text_lines.size(); - std::vector bbs = create_line_bounds(shapes, ws, count_lines); + const EmbossShape &es = input1.create_shape(); + if (was_canceled()) return {}; + if (es.shapes_with_ids.empty()) + throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); - const FontProp &prop = tc.style.prop; - FontFileWithCache &font = input1.font_file; - double shape_scale = get_shape_scale(prop, *font.font_file); + assert(get_count_lines(es.shapes_with_ids) == input1.text_lines.size()); + size_t count_lines = input1.text_lines.size(); + std::vector bbs = create_line_bounds(es.shapes_with_ids, count_lines); // half of font em size for direction of letter emboss - double em_2_mm = prop.size_in_mm / 2.; + double em_2_mm = 5.; // TODO: fix it int32_t em_2_polygon = static_cast(std::round(scale_(em_2_mm))); size_t s_i_offset = 0; // shape index offset(for next lines) @@ -1018,7 +1295,7 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in for (size_t text_line_index = 0; text_line_index < input1.text_lines.size(); ++text_line_index) { const BoundingBoxes &line_bbs = bbs[text_line_index]; const TextLine &line = input1.text_lines[text_line_index]; - PolygonPoints samples = sample_slice(line, line_bbs, shape_scale); + PolygonPoints samples = sample_slice(line, line_bbs, es.scale); std::vector angles = calculate_angles(em_2_polygon, samples, line.polygon); for (size_t i = 0; i < line_bbs.size(); ++i) { @@ -1033,7 +1310,7 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in Vec2d offset_vec = unscale(sample.point); // [in mm] auto offset_tr = Eigen::Translation(offset_vec.x(), 0., -offset_vec.y()); - ExPolygons &glyph_shape = shapes[s_i_offset + i]; + ExPolygons glyph_shape = es.shapes_with_ids[s_i_offset + i].expoly; assert(get_extents(glyph_shape) == glyph_bb); Point offset(-glyph_bb.center().x(), 0); @@ -1041,7 +1318,7 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in s.translate(offset); Transform3d modify = offset_tr * rotate; - Transform3d tr = input2.text_tr * modify; + Transform3d tr = input2.transform * modify; indexed_triangle_set glyph_its = cut_surface_to_its(glyph_shape, tr, input2.sources, input1, was_canceled); // move letter in volume on the right position its_transform(glyph_its, modify); @@ -1057,17 +1334,16 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in if (was_canceled()) return {}; if (result.empty()) - throw priv::JobException(_u8L("There is no valid surface for text projection.").c_str()); + throw JobException(_u8L("There is no valid surface for text projection.").c_str()); return TriangleMesh(std::move(result)); } -} // namespace // input can't be const - cache of font -TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2, std::function was_canceled) +template +TriangleMesh cut_surface(DataBase& input1, const SurfaceVolumeData& input2, const Fnc& was_canceled) { - const FontProp &fp = input1.text_configuration.style.prop; - if (fp.per_glyph) + if (!input1.text_lines.empty()) return cut_per_glyph_surface(input1, input2, was_canceled); ExPolygons shapes = create_shape(input1, was_canceled); @@ -1075,7 +1351,7 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 if (shapes.empty()) throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); - indexed_triangle_set its = cut_surface_to_its(shapes, input2.text_tr, input2.sources, input1, was_canceled); + indexed_triangle_set its = cut_surface_to_its(shapes, input2.transform, input2.sources, input1, was_canceled); if (was_canceled()) return {}; if (its.empty()) throw JobException(_u8L("There is no valid surface for text projection.").c_str()); @@ -1083,18 +1359,40 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 return TriangleMesh(std::move(its)); } -bool priv::process(std::exception_ptr &eptr) { - if (!eptr) return false; +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id) +{ + SurfaceVolumeData::ModelSources result; + result.reserve(volumes.size() - 1); + for (const ModelVolume *v : volumes) { + if (text_volume_id.has_value() && v->id().id == *text_volume_id) + continue; + // skip modifiers and negative volumes, ... + if (!v->is_model_part()) + continue; + const TriangleMesh &tm = v->mesh(); + if (tm.empty()) + continue; + if (tm.its.empty()) + continue; + result.push_back({v->get_mesh_shared_ptr(), v->get_matrix()}); + } + return result; +} + +bool process(std::exception_ptr &eptr) +{ + if (!eptr) + return false; try { std::rethrow_exception(eptr); - } catch (priv::JobException &e) { + } catch (JobException &e) { create_message(e.what()); eptr = nullptr; } return true; } -bool priv::finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input) +bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input) { // doesn't care about exception when process was canceled by user if (canceled || input.cancel->load()) { @@ -1104,6 +1402,165 @@ bool priv::finalize(bool canceled, std::exception_ptr &eptr, const DataBase &inp return !process(eptr); } -void priv::create_message(const std::string &message) { +bool is_valid(ModelVolumeType volume_type) +{ + assert(volume_type != ModelVolumeType::INVALID); + assert(volume_type == ModelVolumeType::MODEL_PART || + volume_type == ModelVolumeType::NEGATIVE_VOLUME || + volume_type == ModelVolumeType::PARAMETER_MODIFIER); + if (volume_type == ModelVolumeType::MODEL_PART || + volume_type == ModelVolumeType::NEGATIVE_VOLUME || + volume_type == ModelVolumeType::PARAMETER_MODIFIER) + return true; + + BOOST_LOG_TRIVIAL(error) << "Can't create embossed volume with this type: " << (int) volume_type; + return false; +} + +bool start_create_volume_job(Worker &worker, + const ModelObject &object, + const std::optional &volume_tr, + DataBasePtr data, + ModelVolumeType volume_type, + GLGizmosManager::EType gizmo) +{ + bool &use_surface = data->shape.projection.use_surface; + std::unique_ptr job; + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_sources(object.volumes); + if (sources.empty() || !volume_tr.has_value()) { + use_surface = false; + } else { + SurfaceVolumeData sfvd{*volume_tr, std::move(sources)}; + CreateSurfaceVolumeData surface_data{std::move(sfvd), std::move(data), volume_type, object.id(), gizmo}; + job = std::make_unique(std::move(surface_data)); + } + } + if (!use_surface) { + // create volume + DataCreateVolume create_volume_data{std::move(data), volume_type, object.id(), volume_tr, gizmo}; + job = std::make_unique(std::move(create_volume_data)); + } + return queue_job(worker, std::move(job)); +} + +const GLVolume *find_closest( + const Selection &selection, const Vec2d &screen_center, const Camera &camera, const ModelObjectPtrs &objects, Vec2d *closest_center) +{ + assert(closest_center != nullptr); + const GLVolume *closest = nullptr; + const Selection::IndicesList &indices = selection.get_volume_idxs(); + assert(!indices.empty()); // no selected volume + if (indices.empty()) + return closest; + + double center_sq_distance = std::numeric_limits::max(); + for (unsigned int id : indices) { + const GLVolume *gl_volume = selection.get_volume(id); + if (const ModelVolume *volume = get_model_volume(*gl_volume, objects); + volume == nullptr || !volume->is_model_part()) + continue; + Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *gl_volume); + Vec2d c = hull.centroid().cast(); + Vec2d d = c - screen_center; + bool is_bigger_x = std::fabs(d.x()) > std::fabs(d.y()); + if ((is_bigger_x && d.x() * d.x() > center_sq_distance) || + (!is_bigger_x && d.y() * d.y() > center_sq_distance)) + continue; + + double distance = d.squaredNorm(); + if (center_sq_distance < distance) + continue; + center_sq_distance = distance; + + *closest_center = c; + closest = gl_volume; + } + return closest; +} + +bool start_create_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor) +{ + const Pointfs &bed_shape = input.build_volume.bed_shape(); + auto gizmo_type = static_cast(input.gizmo); + DataCreateObject data{std::move(emboss_data), coor, input.camera, bed_shape, gizmo_type}; + auto job = std::make_unique(std::move(data)); + return queue_job(input.worker, std::move(job)); +} + +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &screen_coor, bool try_no_coor) +{ + auto on_bad_state = [&input, try_no_coor](DataBasePtr data_, const ModelObject *object = nullptr) { + if (try_no_coor) { + // Can't create on coordinate try to create somewhere + return start_create_volume_without_position(input, std::move(data_)); + } else { + // In centroid of convex hull is not hit with object. e.g. torid + // soo create transfomation on border of object + + // there is no point on surface so no use of surface will be applied + if (data_->shape.projection.use_surface) + data_->shape.projection.use_surface = false; + + if (object == nullptr) + return false; + + auto gizmo_type = static_cast(input.gizmo); + return start_create_volume_job(input.worker, *object, {}, std::move(data_), input.volume_type, gizmo_type); + } + }; + + assert(input.gl_volume != nullptr); + if (input.gl_volume == nullptr) + return on_bad_state(std::move(data)); + + const Model *model = input.canvas.get_model(); + + assert(model != nullptr); + if (model == nullptr) + return on_bad_state(std::move(data)); + + const ModelObjectPtrs &objects = model->objects; + const ModelVolume *volume = get_model_volume(*input.gl_volume, objects); + assert(volume != nullptr); + if (volume == nullptr) + return on_bad_state(std::move(data)); + + const ModelInstance *instance = get_model_instance(*input.gl_volume, objects); + assert(instance != nullptr); + if (instance == nullptr) + return on_bad_state(std::move(data)); + + const ModelObject *object = volume->get_object(); + assert(object != nullptr); + if (object == nullptr) + return on_bad_state(std::move(data)); + + auto cond = RaycastManager::AllowVolumes({volume->id().id}); + RaycastManager::Meshes meshes = create_meshes(input.canvas, cond); + input.raycaster.actualize(*instance, &cond, &meshes); + std::optional hit = ray_from_camera(input.raycaster, screen_coor, input.camera, &cond); + + // context menu for add text could be open only by right click on an + // object. After right click, object is selected and object_idx is set + // also hit must exist. But there is options to add text by object list + if (!hit.has_value()) + // When model is broken. It could appear that hit miss the object. + // So add part near by in simmilar manner as right panel do + return on_bad_state(std::move(data), object); + + // Create result volume transformation + Transform3d surface_trmat = create_transformation_onto_surface(hit->position, hit->normal, UP_LIMIT); + apply_transformation(input.angle, input.distance, surface_trmat); + Transform3d transform = instance->get_matrix().inverse() * surface_trmat; + auto gizmo_type = static_cast(input.gizmo); + // Try to cast ray into scene and find object for add volume + return start_create_volume_job(input.worker, *object, transform, std::move(data), input.volume_type, gizmo_type); +} + +void create_message(const std::string &message) { show_error(nullptr, message.c_str()); } + +} // namespace \ No newline at end of file diff --git a/src/slic3r/GUI/Jobs/EmbossJob.hpp b/src/slic3r/GUI/Jobs/EmbossJob.hpp index c153814..1b355b4 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.hpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.hpp @@ -5,42 +5,78 @@ #include #include #include -#include "slic3r/Utils/RaycastManager.hpp" +#include // ExPolygonsWithIds +#include "libslic3r/Point.hpp" // Transform3d +#include "libslic3r/ObjectID.hpp" #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/TextLines.hpp" #include "Job.hpp" +// forward declarations namespace Slic3r { -class ModelVolume; class TriangleMesh; -} +class ModelVolume; +enum class ModelVolumeType : int; +class BuildVolume; +namespace GUI { +class RaycastManager; +class Plater; +class GLCanvas3D; +class Worker; +class Selection; +}} namespace Slic3r::GUI::Emboss { /// -/// Base data holder for embossing +/// Base data hold data for create emboss shape /// -struct DataBase +class DataBase { - // Keep pointer on Data of font (glyph shapes) - Slic3r::Emboss::FontFileWithCache font_file; - // font item is not used for create object - TextConfiguration text_configuration; - // new volume name created from text - std::string volume_name; +public: + DataBase(const std::string& volume_name, std::shared_ptr> cancel) + : volume_name(volume_name), cancel(std::move(cancel)) {} + DataBase(const std::string& volume_name, std::shared_ptr> cancel, EmbossShape&& shape) + : volume_name(volume_name), cancel(std::move(cancel)), shape(std::move(shape)){} + DataBase(DataBase &&) = default; + virtual ~DataBase() = default; + + /// + /// Create shape + /// e.g. Text extract glyphs from font + /// Not 'const' function because it could modify shape + /// + virtual EmbossShape& create_shape() { return shape; }; + + /// + /// Write data how to reconstruct shape to volume + /// + /// Data object for store emboss params + virtual void write(ModelVolume &volume) const; // Define projection move - // True (raised) .. move outside from surface - // False (engraved).. move into object - bool is_outside; + // True (raised) .. move outside from surface (MODEL_PART) + // False (engraved).. move into object (NEGATIVE_VOLUME) + bool is_outside = true; + + // Define per letter projection on one text line + // [optional] It is not used when empty + Slic3r::Emboss::TextLines text_lines = {}; + + // [optional] Define distance for surface + // It is used only for flat surface (not cutted) + // Position of Zero(not set value) differ for MODEL_PART and NEGATIVE_VOLUME + std::optional from_surface; + + // new volume name + std::string volume_name; // flag that job is canceled // for time after process. std::shared_ptr> cancel; - // Define per letter projection on one text line - // [optional] It is not used when empty - Slic3r::Emboss::TextLines text_lines; + // shape to emboss + EmbossShape shape; }; /// @@ -60,59 +96,15 @@ struct DataCreateVolume : public DataBase Transform3d trmat; }; -/// -/// Create new TextVolume on the surface of ModelObject -/// Should not be stopped -/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! -/// -class CreateVolumeJob : public Job -{ - DataCreateVolume m_input; - TriangleMesh m_result; - -public: - CreateVolumeJob(DataCreateVolume&& input); - void process(Ctl &ctl) override; - void finalize(bool canceled, std::exception_ptr &eptr) override; -}; - -/// -/// Hold neccessary data to create ModelObject in job -/// Object is placed on bed under screen coor -/// OR to center of scene when it is out of bed shape -/// -struct DataCreateObject : public DataBase -{ - // define position on screen where to create object - Vec2d screen_coor; - - // projection property - Camera camera; - - // shape of bed in case of create volume on bed - std::vector bed_shape; -}; - -/// -/// Create new TextObject on the platter -/// Should not be stopped -/// -class CreateObjectJob : public Job -{ - DataCreateObject m_input; - TriangleMesh m_result; - Transform3d m_transformation; -public: - CreateObjectJob(DataCreateObject&& input); - void process(Ctl &ctl) override; - void finalize(bool canceled, std::exception_ptr &eptr) override; -}; +using DataBasePtr = std::unique_ptr; /// /// Hold neccessary data to update embossed text object in job /// -struct DataUpdate : public DataBase +struct DataUpdate { + // Hold data about shape + DataBasePtr base; // unique identifier of volume to change ObjectID volume_id; }; @@ -128,7 +120,7 @@ class UpdateJob : public Job public: // move params to private variable - UpdateJob(DataUpdate &&input); + explicit UpdateJob(DataUpdate &&input); /// /// Create new embossed volume by m_input data and store to m_result @@ -150,18 +142,14 @@ public: /// /// Volume to be updated /// New Triangle mesh for volume - /// Parametric description of volume - /// Name of volume - static void update_volume(ModelVolume *volume, - TriangleMesh &&mesh, - const TextConfiguration &text_configuration, - std::string_view volume_name); + /// Data to write into volume + static void update_volume(ModelVolume *volume, TriangleMesh &&mesh, const DataBase &base); }; struct SurfaceVolumeData { - // Transformation of text volume inside of object - Transform3d text_tr; + // Transformation of volume inside of object + Transform3d transform; struct ModelSource { @@ -174,66 +162,97 @@ struct SurfaceVolumeData ModelSources sources; }; -/// -/// Hold neccessary data to create(cut) volume from surface object in job -/// -struct CreateSurfaceVolumeData : public DataBase, public SurfaceVolumeData{ - // define embossed volume type - ModelVolumeType volume_type; - - // parent ModelObject index where to create volume - ObjectID object_id; -}; - -/// -/// Cut surface from object and create cutted volume -/// Should not be stopped -/// -class CreateSurfaceVolumeJob : public Job -{ - CreateSurfaceVolumeData m_input; - TriangleMesh m_result; - -public: - CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input); - void process(Ctl &ctl) override; - void finalize(bool canceled, std::exception_ptr &eptr) override; -}; - /// /// Hold neccessary data to update embossed text object in job /// struct UpdateSurfaceVolumeData : public DataUpdate, public SurfaceVolumeData{}; -/// -/// Copied triangles from object to be able create mesh for cut surface from -/// -/// Source object volumes for cut surface from -/// Source volume id -/// Source data for cut surface from -SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id = {}); - -/// -/// Copied triangles from object to be able create mesh for cut surface from -/// -/// Define text in object -/// Source data for cut surface from -SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume *text_volume); - /// /// Update text volume to use surface from object /// class UpdateSurfaceVolumeJob : public Job { UpdateSurfaceVolumeData m_input; - TriangleMesh m_result; + TriangleMesh m_result; public: // move params to private variable - UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input); + explicit UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input); void process(Ctl &ctl) override; void finalize(bool canceled, std::exception_ptr &eptr) override; }; + +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Define embossed volume +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume &volume); + +/// +/// shorten params for start_crate_volume functions +/// +struct CreateVolumeParams +{ + GLCanvas3D &canvas; + + // Direction of ray into scene + const Camera &camera; + + // To put new object on the build volume + const BuildVolume &build_volume; + + // used to emplace job for execution + Worker &worker; + + // New created volume type + ModelVolumeType volume_type; + + // Contain AABB trees from scene + RaycastManager &raycaster; + + // Define which gizmo open on the success + unsigned char gizmo; // GLGizmosManager::EType + + // Volume define object to add new volume + const GLVolume *gl_volume; + + // Wanted additionl move in Z(emboss) direction of new created volume + std::optional distance = {}; + + // Wanted additionl rotation around Z of new created volume + std::optional angle = {}; +}; + +/// +/// Create new volume on position of mouse cursor +/// +/// canvas + camera + bed shape + +/// Shape of emboss +/// New created volume type +/// Knows object in scene +/// Define which gizmo open on the success - enum GLGizmosManager::EType +/// Define position where to create volume +/// Wanted additionl move in Z(emboss) direction of new created volume +/// Wanted additionl rotation around Z of new created volume +/// True on success otherwise False +bool start_create_volume(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos); + +/// +/// Same as previous function but without mouse position +/// Need to suggest position or put near the selection +/// +bool start_create_volume_without_position(CreateVolumeParams &input, DataBasePtr data); + +/// +/// Start job for update embossed volume +/// +/// define update data +/// Volume to be updated +/// Keep model and gl_volumes - when start use surface volume must be selected +/// Could cast ray to scene +/// True when start job otherwise false +bool start_update_volume(DataUpdate &&data, const ModelVolume &volume, const Selection &selection, RaycastManager &raycaster); } // namespace Slic3r::GUI #endif // slic3r_EmbossJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index 0b3c4cb..9e60afa 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -157,7 +157,7 @@ void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr) if (Preset::printer_technology(config) == ptSLA) { wxGetApp().preset_bundle->load_config_model(name, std::move(config)); - p->plater->check_selected_presets_visibility(ptSLA); + p->plater->notify_about_installed_presets();; wxGetApp().load_current_presets(); } else { p->plater->get_notification_manager()->push_notification( diff --git a/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp index d400490..3ee1672 100644 --- a/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp +++ b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp @@ -114,7 +114,8 @@ public: void clear() { std::lock_guard lk{m_mutex}; - while (!m_queue.empty()) m_queue.pop(); + while (!m_queue.empty()) + m_queue.pop(); } }; diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 66961b2..30a5e47 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -46,7 +46,8 @@ KBShortcutsDialog::KBShortcutsDialog() } wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK); - wxGetApp().UpdateDarkUI(static_cast(this->FindWindowById(wxID_OK, this))); + wxGetApp().SetWindowVariantForButton(buttons->GetAffirmativeButton()); + wxGetApp().UpdateDarkUI(buttons->GetAffirmativeButton()); this->SetEscapeId(wxID_OK); main_sizer->Add(buttons, 0, wxEXPAND | wxALL, 5); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 311aa57..c4e2dcb 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -211,7 +211,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S sizer->SetSizeHints(this); Fit(); - const wxSize min_size = wxGetApp().get_min_size(); //wxSize(76*wxGetApp().em_unit(), 49*wxGetApp().em_unit()); + const wxSize min_size = wxGetApp().get_min_size(this); #ifdef __APPLE__ // Using SetMinSize() on Mac messes up the window position in some cases // cf. https://groups.google.com/forum/#!topic/wx-users/yUKPBBfXWO0 @@ -732,7 +732,7 @@ void MainFrame::init_tabpanel() #ifdef _MSW_DARK_MODE if (wxGetApp().tabs_as_menu()) { m_tabpanel = new wxSimplebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); - wxGetApp().UpdateDarkUI(m_tabpanel); +// wxGetApp().UpdateDarkUI(m_tabpanel); } else m_tabpanel = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME, true); @@ -740,9 +740,8 @@ void MainFrame::init_tabpanel() m_tabpanel = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); #endif -#ifndef __WXOSX__ // Don't call SetFont under OSX to avoid name cutting in ObjectList + wxGetApp().UpdateDarkUI(m_tabpanel); m_tabpanel->SetFont(Slic3r::GUI::wxGetApp().normal_font()); -#endif m_tabpanel->Hide(); m_settings_dialog.set_tabpanel(m_tabpanel); @@ -1169,7 +1168,6 @@ void MainFrame::on_sys_color_changed() wxGetApp().update_ui_colours_from_appconfig(); #ifdef __WXMSW__ wxGetApp().UpdateDarkUI(m_tabpanel); - // m_statusbar->update_dark_ui(); #ifdef _MSW_DARK_MODE // update common mode sizer if (!wxGetApp().tabs_as_menu()) @@ -1217,6 +1215,29 @@ static const wxString sep = " - "; static const wxString sep_space = ""; #endif +static void append_about_menu_item(wxMenu* target_menu, int insert_pos = wxNOT_FOUND) +{ + if (wxGetApp().is_editor()) + append_menu_item(target_menu, wxID_ANY, wxString::Format(_L("&About %s"), SLIC3R_APP_NAME), _L("Show about dialog"), + [](wxCommandEvent&) { Slic3r::GUI::about(); }, nullptr, nullptr, []() {return true; }, nullptr, insert_pos); + else + append_menu_item(target_menu, wxID_ANY, wxString::Format(_L("&About %s"), GCODEVIEWER_APP_NAME), _L("Show about dialog"), + [](wxCommandEvent&) { Slic3r::GUI::about(); }, nullptr, nullptr, []() {return true; }, nullptr, insert_pos); +} + +#ifdef __APPLE__ +static void init_macos_application_menu(wxMenuBar* menu_bar, MainFrame* main_frame) +{ + wxMenu* apple_menu = menu_bar->OSXGetAppleMenu(); + if (apple_menu != nullptr) { + append_about_menu_item(apple_menu, 0); + + // This fixes a bug on macOS where the quit command doesn't emit window close events. + // wx bug: https://trac.wxwidgets.org/ticket/18328 + apple_menu->Bind(wxEVT_MENU, [main_frame](wxCommandEvent&) { main_frame->Close(); }, wxID_EXIT); + } +} +#endif // __APPLE__ static wxMenu* generate_help_menu() { wxMenu* helpMenu = new wxMenu(); @@ -1434,34 +1455,20 @@ void MainFrame::init_menubar_as_editor() []() {return true; }, this); append_submenu(fileMenu, export_menu, wxID_ANY, _L("&Export"), ""); + wxMenu* convert_menu = new wxMenu(); + append_menu_item(convert_menu, wxID_ANY, _L("Convert ASCII G-code to &binary") + dots, _L("Convert a G-code file from ASCII to binary format"), + [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->convert_gcode_to_binary(); }, "convert_file", nullptr, + []() { return true; }, this); + append_menu_item(convert_menu, wxID_ANY, _L("Convert binary G-code to &ASCII") + dots, _L("Convert a G-code file from binary to ASCII format"), + [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->convert_gcode_to_ascii(); }, "convert_file", nullptr, + []() { return true; }, this); + append_submenu(fileMenu, convert_menu, wxID_ANY, _L("&Convert"), ""); append_menu_item(fileMenu, wxID_ANY, _L("Ejec&t SD Card / Flash Drive") + dots + "\tCtrl+T", _L("Eject SD card / Flash drive after the G-code was exported to it."), [this](wxCommandEvent&) { if (m_plater) m_plater->eject_drive(); }, "eject_sd", nullptr, [this]() {return can_eject(); }, this); fileMenu->AppendSeparator(); -#if 0 - m_menu_item_repeat = nullptr; - append_menu_item(fileMenu, wxID_ANY, _L("Quick Slice") +dots+ "\tCtrl+U", _L("Slice a file into a G-code"), - [this](wxCommandEvent&) { - wxTheApp->CallAfter([this]() { - quick_slice(); - m_menu_item_repeat->Enable(is_last_input_file()); - }); }, "cog_go.png"); - append_menu_item(fileMenu, wxID_ANY, _L("Quick Slice and Save As") +dots +"\tCtrl+Alt+U", _L("Slice a file into a G-code, save as"), - [this](wxCommandEvent&) { - wxTheApp->CallAfter([this]() { - quick_slice(qsSaveAs); - m_menu_item_repeat->Enable(is_last_input_file()); - }); }, "cog_go.png"); - m_menu_item_repeat = append_menu_item(fileMenu, wxID_ANY, _L("Repeat Last Quick Slice") +"\tCtrl+Shift+U", _L("Repeat last quick slice"), - [this](wxCommandEvent&) { - wxTheApp->CallAfter([this]() { - quick_slice(qsReslice); - }); }, "cog_go.png"); - m_menu_item_repeat->Enable(false); - fileMenu->AppendSeparator(); -#endif m_menu_item_reslice_now = append_menu_item(fileMenu, wxID_ANY, _L("(Re)Slice No&w") + "\tCtrl+R", _L("Start new slicing process"), [this](wxCommandEvent&) { reslice_now(); }, "re_slice", nullptr, [this]() { return m_plater != nullptr && can_reslice(); }, this); @@ -1680,14 +1687,7 @@ void MainFrame::init_menubar_as_editor() #endif #ifdef __APPLE__ - // This fixes a bug on Mac OS where the quit command doesn't emit window close events - // wx bug: https://trac.wxwidgets.org/ticket/18328 - wxMenu* apple_menu = m_menubar->OSXGetAppleMenu(); - if (apple_menu != nullptr) { - apple_menu->Bind(wxEVT_MENU, [this](wxCommandEvent &) { - Close(); - }, wxID_EXIT); - } + init_macos_application_menu(m_menubar, this); #endif // __APPLE__ if (plater()->printer_technology() == ptSLA) @@ -1738,6 +1738,13 @@ void MainFrame::init_menubar_as_gcodeviewer() "", nullptr, [this]() { return !m_plater->get_last_loaded_gcode().empty(); }, this); #endif // __APPLE__ fileMenu->AppendSeparator(); + append_menu_item(fileMenu, wxID_ANY, _L("Convert ASCII G-code to &binary") + dots, _L("Convert a G-code file from ASCII to binary format"), + [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->convert_gcode_to_binary(); }, "convert_file", nullptr, + []() { return true; }, this); + append_menu_item(fileMenu, wxID_ANY, _L("Convert binary G-code to &ASCII") + dots, _L("Convert a G-code file from binary to ASCII format"), + [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->convert_gcode_to_ascii(); }, "convert_file", nullptr, + []() { return true; }, this); + fileMenu->AppendSeparator(); append_menu_item(fileMenu, wxID_ANY, _L("Export &Toolpaths as OBJ") + dots, _L("Export toolpaths as OBJ"), [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->export_toolpaths_to_obj(); }, "export_plater", nullptr, [this]() {return can_export_toolpaths(); }, this); @@ -1772,14 +1779,7 @@ void MainFrame::init_menubar_as_gcodeviewer() SetMenuBar(m_menubar); #ifdef __APPLE__ - // This fixes a bug on Mac OS where the quit command doesn't emit window close events - // wx bug: https://trac.wxwidgets.org/ticket/18328 - wxMenu* apple_menu = m_menubar->OSXGetAppleMenu(); - if (apple_menu != nullptr) { - apple_menu->Bind(wxEVT_MENU, [this](wxCommandEvent&) { - Close(); - }, wxID_EXIT); - } + init_macos_application_menu(m_menubar, this); #endif // __APPLE__ } @@ -1799,133 +1799,6 @@ void MainFrame::update_menubar() m_changeable_menu_items[miPrinterTab] ->SetBitmap(*get_bmp_bundle(is_fff ? "printer" : "sla_printer")); } -#if 0 -// To perform the "Quck Slice", "Quick Slice and Save As", "Repeat last Quick Slice" and "Slice to SVG". -void MainFrame::quick_slice(const int qs) -{ -// my $progress_dialog; - wxString input_file; -// eval -// { - // validate configuration - auto config = wxGetApp().preset_bundle->full_config(); - auto valid = config.validate(); - if (! valid.empty()) { - show_error(this, valid); - return; - } - - // select input file - if (!(qs & qsReslice)) { - wxFileDialog dlg(this, _L("Choose a file to slice (STL/OBJ/AMF/3MF/QIDI):"), - wxGetApp().app_config->get_last_dir(), "", - file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dlg.ShowModal() != wxID_OK) - return; - input_file = dlg.GetPath(); - if (!(qs & qsExportSVG)) - m_qs_last_input_file = input_file; - } - else { - if (m_qs_last_input_file.IsEmpty()) { - //wxMessageDialog dlg(this, _L("No previously sliced file."), - MessageDialog dlg(this, _L("No previously sliced file."), - _L("Error"), wxICON_ERROR | wxOK); - dlg.ShowModal(); - return; - } - if (std::ifstream(m_qs_last_input_file.ToUTF8().data())) { - //wxMessageDialog dlg(this, _L("Previously sliced file (")+m_qs_last_input_file+_L(") not found."), - MessageDialog dlg(this, _L("Previously sliced file (")+m_qs_last_input_file+_L(") not found."), - _L("File Not Found"), wxICON_ERROR | wxOK); - dlg.ShowModal(); - return; - } - input_file = m_qs_last_input_file; - } - auto input_file_basename = get_base_name(input_file); - wxGetApp().app_config->update_skein_dir(get_dir_name(input_file)); - - auto bed_shape = Slic3r::Polygon::new_scale(config.option("bed_shape")->values); -// auto print_center = Slic3r::Pointf->new_unscale(bed_shape.bounding_box().center()); -// -// auto sprint = new Slic3r::Print::Simple( -// print_center = > print_center, -// status_cb = > [](int percent, const wxString& msg) { -// m_progress_dialog->Update(percent, msg+"…"); -// }); - - // keep model around - auto model = Slic3r::Model::read_from_file(input_file.ToUTF8().data()); - -// sprint->apply_config(config); -// sprint->set_model(model); - - // Copy the names of active presets into the placeholder parser. -// wxGetApp().preset_bundle->export_selections(sprint->placeholder_parser); - - // select output file - wxString output_file; - if (qs & qsReslice) { - if (!m_qs_last_output_file.IsEmpty()) - output_file = m_qs_last_output_file; - } - else if (qs & qsSaveAs) { - // The following line may die if the output_filename_format template substitution fails. - wxFileDialog dlg(this, format_wxstr(_L("Save %s file as:"), ((qs & qsExportSVG) ? _L("SVG") : _L("G-code"))), - wxGetApp().app_config->get_last_output_dir(get_dir_name(output_file)), get_base_name(input_file), - qs & qsExportSVG ? file_wildcards(FT_SVG) : file_wildcards(FT_GCODE), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (dlg.ShowModal() != wxID_OK) - return; - output_file = dlg.GetPath(); - if (!(qs & qsExportSVG)) - m_qs_last_output_file = output_file; - wxGetApp().app_config->update_last_output_dir(get_dir_name(output_file)); - } - else if (qs & qsExportPNG) { - wxFileDialog dlg(this, _L("Save ZIP file as:"), - wxGetApp().app_config->get_last_output_dir(get_dir_name(output_file)), - get_base_name(output_file), "*.sl1", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (dlg.ShowModal() != wxID_OK) - return; - output_file = dlg.GetPath(); - } - - // show processbar dialog - m_progress_dialog = new wxProgressDialog(_L("Slicing") + dots, - // TRN ProgressDialog on reslicing: "input file basename" - format_wxstr(_L("Processing %s"), (input_file_basename + dots)), - 100, nullptr, wxPD_AUTO_HIDE); - m_progress_dialog->Pulse(); - { -// my @warnings = (); -// local $SIG{ __WARN__ } = sub{ push @warnings, $_[0] }; - -// sprint->output_file(output_file); -// if (export_svg) { -// sprint->export_svg(); -// } -// else if(export_png) { -// sprint->export_png(); -// } -// else { -// sprint->export_gcode(); -// } -// sprint->status_cb(undef); -// Slic3r::GUI::warning_catcher($self)->($_) for @warnings; - } - m_progress_dialog->Destroy(); - m_progress_dialog = nullptr; - - auto message = format(_L("%1% was successfully sliced."), input_file_basename); -// wxTheApp->notify(message); - //wxMessageDialog(this, message, _L("Slicing Done!"), wxOK | wxICON_INFORMATION).ShowModal(); - MessageDialog(this, message, _L("Slicing Done!"), wxOK | wxICON_INFORMATION).ShowModal(); -// }; -// Slic3r::GUI::catch_error(this, []() { if (m_progress_dialog) m_progress_dialog->Destroy(); }); -} -#endif void MainFrame::reslice_now() { @@ -1993,11 +1866,24 @@ void MainFrame::load_config_file() return; wxFileDialog dlg(this, _L("Select configuration to load:"), !m_last_config.IsEmpty() ? get_dir_name(m_last_config) : wxGetApp().app_config->get_last_dir(), - "config.ini", "INI files (*.ini, *.gcode)|*.ini;*.INI;*.gcode;*.g", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + "config.ini", "INI files (*.ini, *.gcode, *.bgcode)|*.ini;*.INI;*.gcode;*.g;*.bgcode;*.bgc", wxFD_OPEN | wxFD_FILE_MUST_EXIST); wxString file; if (dlg.ShowModal() == wxID_OK) file = dlg.GetPath(); if (! file.IsEmpty() && this->load_config_file(file.ToUTF8().data())) { + DynamicPrintConfig config = wxGetApp().preset_bundle->full_config(); + const auto* post_process = config.opt("post_process"); + if (post_process != nullptr && !post_process->values.empty()) { + const wxString msg = _L("The selected config file contains a post-processing script.\nPlease review the script carefully before exporting G-code."); + std::string text; + for (const std::string& s : post_process->values) { + text += s; + } + + InfoDialog msg_dlg(nullptr, msg, from_u8(text), true, wxOK | wxICON_WARNING); + msg_dlg.set_caption(wxString(SLIC3R_APP_NAME " - ") + _L("Attention!")); + msg_dlg.ShowModal(); + } wxGetApp().app_config->update_config_dir(get_dir_name(file)); m_last_config = file; } @@ -2015,7 +1901,7 @@ bool MainFrame::load_config_file(const std::string &path) return false; } - m_plater->check_selected_presets_visibility(ptFFF); + m_plater->notify_about_installed_presets(); wxGetApp().load_current_presets(); return true; } @@ -2186,6 +2072,7 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) else model_id = preset->config.opt_string("printer_model"); } + preset_data.push_back({wxString::FromUTF8(it->get_full_name(preset_name)).Lower(), wxString::FromUTF8(preset_name), wxString::FromUTF8(it->get_full_name(preset_name)), ph_printers.is_selected(it, preset_name), model_id}); @@ -2519,15 +2406,6 @@ SettingsDialog::SettingsDialog(MainFrame* mainframe) if (wxGetApp().is_gcode_viewer()) return; -#if defined(__WXMSW__) - // ys_FIXME! temporary workaround for correct font scaling - // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, - // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT - this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); -#else - this->SetFont(wxGetApp().normal_font()); - this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -#endif // __WXMSW__ // Load the icon either from the exe, or from the ico file. #if _WIN32 diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 03a66b1..f50b708 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -181,7 +181,6 @@ public: bool is_last_input_file() const { return !m_qs_last_input_file.IsEmpty(); } bool is_dlg_layout() const { return m_layout == ESettingsLayout::Dlg; } -// void quick_slice(const int qs = qsUndef); void reslice_now(); void repair_stl(); void export_config(); diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index f04c51a..1203d92 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -22,6 +22,9 @@ #include "wxExtensions.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "GUI_App.hpp" + +#include "Widgets/CheckBox.hpp" + //Y #include "libslic3r/AppConfig.cpp" @@ -87,6 +90,7 @@ void MsgDialog::SetButtonLabel(wxWindowID btn_id, const wxString& label, bool se wxButton* MsgDialog::add_button(wxWindowID btn_id, bool set_focus /*= false*/, const wxString& label/* = wxString()*/) { wxButton* btn = new wxButton(this, btn_id, label); + wxGetApp().SetWindowVariantForButton(btn); if (set_focus) { btn->SetFocus(); // For non-MSW platforms SetFocus is not enought to use it as default, when the dialog is closed by ENTER @@ -106,6 +110,7 @@ wxButton* MsgDialog::get_button(wxWindowID btn_id){ //B44 void MsgDialog::apply_style(long style) { + if (style & wxOK) add_button(wxID_OK, true); if (style & wxYES) add_button(wxID_YES, !(style & wxNO_DEFAULT)); if (style & wxNO) add_button(wxID_NO, (style & wxNO_DEFAULT)); if (style & wxCANCEL) add_button(wxID_CANCEL, (style & wxCANCEL_DEFAULT)); @@ -125,19 +130,19 @@ void MsgDialog::finalize() // Text shown as HTML, so that mouse selection and Ctrl-V to copy will work. -static void add_msg_content(wxWindow* parent, wxBoxSizer* content_sizer, wxString msg, bool monospaced_font = false, bool is_marked_msg = false) +static void add_msg_content(MsgDialog* parent, wxBoxSizer* content_sizer, const HtmlContent& content) { wxHtmlWindow* html = new wxHtmlWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO); // count lines in the message int msg_lines = 0; - if (!monospaced_font) { + if (!content.is_monospaced_font) { int line_len = 55;// count of symbols in one line int start_line = 0; - for (auto i = msg.begin(); i != msg.end(); ++i) { + for (auto i = content.msg.begin(); i != content.msg.end(); ++i) { if (*i == '\n') { - int cur_line_len = i - msg.begin() - start_line; - start_line = i - msg.begin(); + int cur_line_len = i - content.msg.begin() - start_line; + start_line = i - content.msg.begin(); if (cur_line_len == 0 || line_len > cur_line_len) msg_lines++; else @@ -176,11 +181,11 @@ static void add_msg_content(wxWindow* parent, wxBoxSizer* content_sizer, wxStrin } // if message containes the table - if (msg.Contains("")) { - int lines = msg.Freq('\n') + 1; + if (content.msg.Contains("")) { + int lines = content.msg.Freq('\n') + 1; int pos = 0; - while (pos < (int)msg.Len() && pos != wxNOT_FOUND) { - pos = msg.find("", pos + 1); + while (pos < (int)content.msg.Len() && pos != wxNOT_FOUND) { + pos = content.msg.find("", pos + 1); lines += 2; } int page_height = std::min(int(font.GetPixelSize().y+2) * lines, 68 * em); @@ -188,16 +193,16 @@ static void add_msg_content(wxWindow* parent, wxBoxSizer* content_sizer, wxStrin } else { wxClientDC dc(parent); - wxSize msg_sz = dc.GetMultiLineTextExtent(msg); + wxSize msg_sz = dc.GetMultiLineTextExtent(content.msg); page_size = wxSize(std::min(msg_sz.GetX() + 2 * em, 68 * em), - std::min(msg_sz.GetY() + 8 * em, 68 * em)); + std::min(msg_sz.GetY() + 2 * em, 68 * em)); } html->SetMinSize(page_size); - std::string msg_escaped = xml_escape(into_u8(msg), is_marked_msg); + std::string msg_escaped = xml_escape(into_u8(content.msg), content.is_marked_msg || content.on_link_clicked); boost::replace_all(msg_escaped, "\r\n", "
"); boost::replace_all(msg_escaped, "\n", "
"); - if (monospaced_font) + if (content.is_monospaced_font) // Code formatting will be preserved. This is useful for reporting errors from the placeholder parser. msg_escaped = std::string("
") + msg_escaped + "
"; //Y @@ -211,13 +216,17 @@ static void add_msg_content(wxWindow* parent, wxBoxSizer* content_sizer, wxStrin "" "" "%3%" - "%4%" "" "" "", - bgr_clr_str, text_clr_str, from_u8(msg_escaped), is_that_msg ? "
You can get the latest version of the software through the link below:\nhttps://qidi3d.com/pages/software-firmware" : "")); + bgr_clr_str, text_clr_str, from_u8(msg_escaped))); - html->Bind(wxEVT_HTML_LINK_CLICKED, [parent](wxHtmlLinkEvent& event) { + html->Bind(wxEVT_HTML_LINK_CLICKED, [parent, &content](wxHtmlLinkEvent& event) { + if (content.on_link_clicked) { + parent->EndModal(wxID_CLOSE); + content.on_link_clicked(into_u8(event.GetLinkInfo().GetHref())); + } + else wxGetApp().open_browser_with_warning_dialog(event.GetLinkInfo().GetHref(), parent, false); event.Skip(false); }); @@ -228,21 +237,33 @@ static void add_msg_content(wxWindow* parent, wxBoxSizer* content_sizer, wxStrin // ErrorDialog -ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &msg, bool monospaced_font) - : MsgDialog(parent, wxString::Format(_(L("%s error")), SLIC3R_APP_NAME), - wxString::Format(_(L("%s has encountered an error")), SLIC3R_APP_NAME), wxOK) - , msg(msg) +void ErrorDialog::create(const HtmlContent& content, int icon_width) { - add_msg_content(this, content_sizer, msg, monospaced_font); + add_msg_content(this, content_sizer, content); // Use a small bitmap with monospaced font, as the error text will not be wrapped. - logo->SetBitmap(*get_bmp_bundle("QIDISlicer_192px_grayscale.png", monospaced_font ? 48 : /*1*/84)); + logo->SetBitmap(*get_bmp_bundle("QIDISlicer_192px_grayscale.png", icon_width)); SetMaxSize(wxSize(-1, CONTENT_MAX_HEIGHT*wxGetApp().em_unit())); finalize(); } +ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &msg, bool monospaced_font) + : MsgDialog(parent, wxString::Format(_L("%s error"), SLIC3R_APP_NAME), + wxString::Format(_L("%s has encountered an error"), SLIC3R_APP_NAME), wxOK) + , m_content(HtmlContent{ msg, monospaced_font, true }) +{ + create(m_content, monospaced_font ? 48 : 84); +} + +ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &msg, const std::function &on_link_clicked) + : MsgDialog(parent, wxString::Format(_L("%s error"), SLIC3R_APP_NAME), + wxString::Format(_L("%s has encountered an error"), SLIC3R_APP_NAME), wxOK) + , m_content(HtmlContent{ msg, false, true, on_link_clicked }) +{ + create(m_content, 84); +} // WarningDialog WarningDialog::WarningDialog(wxWindow *parent, @@ -252,7 +273,7 @@ WarningDialog::WarningDialog(wxWindow *parent, : MsgDialog(parent, caption.IsEmpty() ? wxString::Format(_L("%s warning"), SLIC3R_APP_NAME) : caption, wxString::Format(_L("%s has a warning")+":", SLIC3R_APP_NAME), style) { - add_msg_content(this, content_sizer, message); + add_msg_content(this, content_sizer, HtmlContent{ message }); finalize(); } @@ -265,7 +286,7 @@ MessageDialog::MessageDialog(wxWindow* parent, long style/* = wxOK*/) : MsgDialog(parent, caption.IsEmpty() ? wxString::Format(_L("%s info"), SLIC3R_APP_NAME) : caption, wxEmptyString, style) { - add_msg_content(this, content_sizer, get_wraped_wxString(message)); + add_msg_content(this, content_sizer, HtmlContent{ get_wraped_wxString(message) }); finalize(); } @@ -278,9 +299,9 @@ RichMessageDialog::RichMessageDialog(wxWindow* parent, long style/* = wxOK*/) : MsgDialog(parent, caption.IsEmpty() ? wxString::Format(_L("%s info"), SLIC3R_APP_NAME) : caption, wxEmptyString, style) { - add_msg_content(this, content_sizer, get_wraped_wxString(message)); + add_msg_content(this, content_sizer, HtmlContent{ get_wraped_wxString(message) }); - m_checkBox = new wxCheckBox(this, wxID_ANY, m_checkBoxText); + m_checkBox = new ::CheckBox(this, m_checkBoxText); wxGetApp().UpdateDarkUI(m_checkBox); m_checkBox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { m_checkBoxValue = m_checkBox->GetValue(); }); @@ -293,8 +314,10 @@ int RichMessageDialog::ShowModal() { if (m_checkBoxText.IsEmpty()) m_checkBox->Hide(); - else + else { m_checkBox->SetLabelText(m_checkBoxText); + m_checkBox->Update(); + } Layout(); return wxDialog::ShowModal(); @@ -307,7 +330,7 @@ InfoDialog::InfoDialog(wxWindow* parent, const wxString &title, const wxString& : MsgDialog(parent, wxString::Format(_L("%s information"), SLIC3R_APP_NAME), title, style) , msg(msg) { - add_msg_content(this, content_sizer, msg, false, is_marked_msg); + add_msg_content(this, content_sizer, HtmlContent{ msg, false, is_marked_msg }); finalize(); } diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index 4ccc907..308fd92 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -13,13 +13,20 @@ #include class wxBoxSizer; -class wxCheckBox; +class CheckBox; class wxStaticBitmap; namespace Slic3r { namespace GUI { +struct HtmlContent +{ + wxString msg{ wxEmptyString }; + bool is_monospaced_font{ false }; + bool is_marked_msg{ false }; + std::function on_link_clicked{ nullptr }; +}; // A message / query dialog with a bitmap on the left and any content on the right // with buttons underneath. struct MsgDialog : wxDialog @@ -63,6 +70,7 @@ public: // If monospaced_font is true, the error message is displayed using html
tags, // so that the code formatting will be preserved. This is useful for reporting errors from the placeholder parser. ErrorDialog(wxWindow *parent, const wxString &msg, bool courier_font); + ErrorDialog(wxWindow *parent, const wxString &msg, const std::function& on_link_clicked); ErrorDialog(ErrorDialog &&) = delete; ErrorDialog(const ErrorDialog &) = delete; ErrorDialog &operator=(ErrorDialog &&) = delete; @@ -70,7 +78,9 @@ public: virtual ~ErrorDialog() = default; private: - wxString msg; + void create(const HtmlContent& content, int icon_width); + + HtmlContent m_content; }; @@ -128,7 +138,7 @@ public: // Generic rich message dialog, used intead of wxRichMessageDialog class RichMessageDialog : public MsgDialog { - wxCheckBox* m_checkBox{ nullptr }; + CheckBox* m_checkBox{ nullptr }; wxString m_checkBoxText; bool m_checkBoxValue{ false }; @@ -316,6 +326,7 @@ public: InfoDialog&operator=(const InfoDialog&) = delete; virtual ~InfoDialog() = default; + void set_caption(const wxString& caption) { this->SetTitle(caption); } private: wxString msg; }; diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 1fa2200..0358fe8 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -60,6 +60,7 @@ enum class NotificationType // Notification on the start of QIDISlicer, when updates of system profiles are detected. // Contains a hyperlink to execute installation of the new system profiles. PresetUpdateAvailable, + PresetUpdateAvailableNewPrinter, // LoadingFailed, // Errors emmited by Print::validate // difference from Slicing error is that they disappear not grey out at update_background_process @@ -115,14 +116,16 @@ enum class NotificationType SimplifySuggestion, // Change of text will change font to similar one on. UnknownFont, - // information about netfabb is finished repairing model (blocking proccess) - NetfabbFinished, + // information that repairing model finished (blocking proccess) + RepairFinished, // Short meesage to fill space between start and finish of export ExportOngoing, // Progressbar of download from qidislicer:// url URLDownload, // MacOS specific - PS comes forward even when downloader is not allowed URLNotRegistered, + // Config file was detected during startup, open wifi config dialog via hypertext + WifiConfigFileDetected }; class NotificationManager diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index f3fdbf7..6fd6c4d 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -280,6 +280,11 @@ void OG_CustomCtrl::OnMotion(wxMouseEvent& event) tooltip = *field->undo_to_sys_tooltip(); break; } + if (opt_idx < line.rects_edit_icon.size() && is_point_in_rect(pos, line.rects_edit_icon[opt_idx])) { + if (Field* field = opt_group->get_field(opt_key); field && field->has_edit_ui()) + tooltip = *field->edit_tooltip(); + break; + } } if (!tooltip.IsEmpty()) break; @@ -329,6 +334,12 @@ void OG_CustomCtrl::OnLeftDown(wxMouseEvent& event) event.Skip(); return; } + if (opt_idx < line.rects_edit_icon.size() && is_point_in_rect(pos, line.rects_edit_icon[opt_idx])) { + if (Field* field = opt_group->get_field(opt_key)) + field->on_edit_value(); + event.Skip(); + return; + } } } @@ -567,18 +578,22 @@ void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord v_pos) return; } + wxCoord h_pos = draw_mode_bmp(dc, v_pos); Field* field = ctrl->opt_group->get_field(og_line.get_options().front().opt_id); //B21 //const bool suppress_hyperlinks = get_app_config()->get_bool("suppress_hyperlinks"); const bool suppress_hyperlinks = true; if (draw_just_act_buttons) { - if (field) - draw_act_bmps(dc, wxPoint(0, v_pos), field->undo_to_sys_bitmap(), field->undo_bitmap(), field->blink()); + if (field) { + const wxPoint pos = draw_act_bmps(dc, wxPoint(h_pos, v_pos), field->undo_to_sys_bitmap(), field->undo_bitmap(), field->blink()); + // Add edit button, if it exists + if (field->has_edit_ui()) + draw_edit_bmp(dc, pos, field->edit_bitmap()); + } return; } - wxCoord h_pos = draw_mode_bmp(dc, v_pos); if (og_line.near_label_widget_win) h_pos += og_line.near_label_widget_win->GetSize().x + ctrl->m_h_gap; @@ -610,7 +625,7 @@ void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord v_pos) option_set.front().side_widget == nullptr && og_line.get_extra_widgets().size() == 0) { if (field && field->has_undo_ui()) - h_pos = draw_act_bmps(dc, wxPoint(h_pos, v_pos), field->undo_to_sys_bitmap(), field->undo_bitmap(), field->blink()) + ctrl->m_h_gap; + h_pos = draw_act_bmps(dc, wxPoint(h_pos, v_pos), field->undo_to_sys_bitmap(), field->undo_bitmap(), field->blink()).x + ctrl->m_h_gap; else if (field && !field->has_undo_ui() && field->blink()) draw_blinking_bmp(dc, wxPoint(h_pos, v_pos), field->blink()); // update width for full_width fields @@ -639,7 +654,7 @@ void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord v_pos) } if (field && field->has_undo_ui()) { - h_pos = draw_act_bmps(dc, wxPoint(h_pos, v_pos), field->undo_to_sys_bitmap(), field->undo_bitmap(), field->blink(), bmp_rect_id++); + h_pos = draw_act_bmps(dc, wxPoint(h_pos, v_pos), field->undo_to_sys_bitmap(), field->undo_bitmap(), field->blink(), bmp_rect_id++).x; if (field->getSizer()) { auto children = field->getSizer()->GetChildren(); @@ -670,7 +685,8 @@ wxCoord OG_CustomCtrl::CtrlLine::draw_mode_bmp(wxDC& dc, wxCoord v_pos) return ctrl->m_h_gap; ConfigOptionMode mode = og_line.get_options()[0].opt.mode; - wxBitmapBundle* bmp = get_bmp_bundle("mode", wxOSX ? 10 : 12, wxGetApp().get_mode_btn_color(mode)); + int pix_cnt = wxOSX ? 10 : 12; + wxBitmapBundle* bmp = get_bmp_bundle("mode", pix_cnt, pix_cnt, wxGetApp().get_mode_btn_color(mode)); wxCoord y_draw = v_pos + lround((height - get_bitmap_size(bmp, ctrl).GetHeight()) / 2); if (og_line.get_options().front().opt.gui_type != ConfigOptionDef::GUIType::legend) @@ -750,7 +766,7 @@ wxPoint OG_CustomCtrl::CtrlLine::draw_blinking_bmp(wxDC& dc, wxPoint pos, bool i return wxPoint(h_pos, v_pos); } -wxCoord OG_CustomCtrl::CtrlLine::draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmapBundle& bmp_undo_to_sys, const wxBitmapBundle& bmp_undo, bool is_blinking, size_t rect_id) +wxPoint OG_CustomCtrl::CtrlLine::draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmapBundle& bmp_undo_to_sys, const wxBitmapBundle& bmp_undo, bool is_blinking, size_t rect_id) { pos = draw_blinking_bmp(dc, pos, is_blinking); wxCoord h_pos = pos.x; @@ -769,7 +785,19 @@ wxCoord OG_CustomCtrl::CtrlLine::draw_act_bmps(wxDC& dc, wxPoint pos, const wxBi h_pos += bmp_dim + ctrl->m_h_gap; - return h_pos; + return wxPoint(h_pos, v_pos); +} + +wxCoord OG_CustomCtrl::CtrlLine::draw_edit_bmp(wxDC &dc, wxPoint pos, const wxBitmapBundle *bmp_edit) +{ + const wxCoord h_pos = pos.x + ctrl->m_h_gap; + const wxCoord v_pos = pos.y; + const int bmp_w = get_bitmap_size(bmp_edit, ctrl).GetWidth(); + rects_edit_icon.emplace_back(wxRect(h_pos, v_pos, bmp_w, bmp_w)); + + dc.DrawBitmap(bmp_edit->GetBitmapFor(ctrl), h_pos, v_pos); + + return h_pos + bmp_w + ctrl->m_h_gap; } bool OG_CustomCtrl::CtrlLine::launch_browser() const diff --git a/src/slic3r/GUI/OG_CustomCtrl.hpp b/src/slic3r/GUI/OG_CustomCtrl.hpp index 269f847..0d7cb62 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.hpp +++ b/src/slic3r/GUI/OG_CustomCtrl.hpp @@ -62,12 +62,14 @@ class OG_CustomCtrl :public wxPanel wxCoord draw_mode_bmp(wxDC& dc, wxCoord v_pos); wxCoord draw_text (wxDC& dc, wxPoint pos, const wxString& text, const wxColour* color, int width, bool is_url = false); wxPoint draw_blinking_bmp(wxDC& dc, wxPoint pos, bool is_blinking); - wxCoord draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmapBundle& bmp_undo_to_sys, const wxBitmapBundle& bmp_undo, bool is_blinking, size_t rect_id = 0); + wxPoint draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmapBundle& bmp_undo_to_sys, const wxBitmapBundle& bmp_undo, bool is_blinking, size_t rect_id = 0); + wxCoord draw_edit_bmp(wxDC& dc, wxPoint pos, const wxBitmapBundle* bmp_edit); bool launch_browser() const; bool is_separator() const { return og_line.is_separator(); } std::vector rects_undo_icon; std::vector rects_undo_to_sys_icon; + std::vector rects_edit_icon; wxRect rect_label; }; diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index ca0ac61..7296b18 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -75,6 +75,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* pare const wxString& sub_obj_name, Slic3r::ModelVolumeType type, const bool is_text_volume, + const bool is_svg_volume, const wxString& extruder, const int idx/* = -1*/) : m_parent(parent), @@ -82,6 +83,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* pare m_type(itVolume), m_volume_type(type), m_is_text_volume(is_text_volume), + m_is_svg_volume(is_svg_volume), m_idx(idx), m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : "") { @@ -333,6 +335,7 @@ ObjectDataViewModel::ObjectDataViewModel() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); + m_svg_volume_bmps = MenuFactory::get_svg_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); @@ -355,7 +358,10 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) bool is_volume_node = vol_type >= 0; if (!node->has_warning_icon() && !node->has_lock()) { - node->SetBitmap(is_volume_node ? (node->is_text_volume() ? *m_text_volume_bmps.at(vol_type) : *m_volume_bmps.at(vol_type)) : m_empty_bmp); + node->SetBitmap(is_volume_node ? ( + node->is_text_volume() ? *m_text_volume_bmps.at(vol_type) : + node->is_svg_volume() ? *m_svg_volume_bmps.at(vol_type) : + *m_volume_bmps.at(vol_type)) : m_empty_bmp); return; } @@ -376,7 +382,10 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) if (node->has_lock()) bmps.emplace_back(&m_lock_bmp); if (is_volume_node) - bmps.emplace_back(node->is_text_volume() ? m_text_volume_bmps[vol_type] : m_volume_bmps[vol_type]); + bmps.emplace_back( + node->is_text_volume() ? m_text_volume_bmps[vol_type] : + node->is_svg_volume() ? m_svg_volume_bmps[vol_type] : + m_volume_bmps[vol_type]); bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); } @@ -413,6 +422,7 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent const int volume_idx, const Slic3r::ModelVolumeType volume_type, const bool is_text_volume, + const bool is_svg_volume, const std::string& warning_icon_name, const wxString& extruder) { @@ -424,7 +434,7 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent if (insert_position < 0) insert_position = get_root_idx(root, itInstanceRoot); - const auto node = new ObjectDataViewModelNode(root, name, volume_type, is_text_volume, extruder, volume_idx); + const auto node = new ObjectDataViewModelNode(root, name, volume_type, is_text_volume, is_svg_volume, extruder, volume_idx); UpdateBitmapForNode(node, warning_icon_name, root->has_lock() && volume_type < ModelVolumeType::PARAMETER_MODIFIER); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); @@ -1683,6 +1693,7 @@ void ObjectDataViewModel::UpdateBitmaps() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); + m_svg_volume_bmps = MenuFactory::get_svg_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 861db32..668c3b7 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -85,6 +85,7 @@ class ObjectDataViewModelNode std::string m_action_icon_name = ""; ModelVolumeType m_volume_type{ -1 }; bool m_is_text_volume{ false }; + bool m_is_svg_volume{false}; InfoItemType m_info_item_type {InfoItemType::Undef}; public: @@ -103,6 +104,7 @@ public: const wxString& sub_obj_name, Slic3r::ModelVolumeType type, const bool is_text_volume, + const bool is_svg_volume, const wxString& extruder, const int idx = -1 ); @@ -238,6 +240,7 @@ public: bool update_settings_digest(const std::vector& categories); int volume_type() const { return int(m_volume_type); } bool is_text_volume() const { return m_is_text_volume; } + bool is_svg_volume() const { return m_is_svg_volume; } void sys_color_changed(); #ifndef NDEBUG @@ -265,6 +268,7 @@ class ObjectDataViewModel :public wxDataViewModel std::vector m_objects; std::vector m_volume_bmps; std::vector m_text_volume_bmps; + std::vector m_svg_volume_bmps; std::map m_info_bmps; wxBitmapBundle m_empty_bmp; wxBitmapBundle m_warning_bmp; @@ -286,6 +290,7 @@ public: const int volume_idx, const Slic3r::ModelVolumeType volume_type, const bool is_text_volume, + const bool is_svg_volume, const std::string& warning_icon_name, const wxString& extruder); wxDataViewItem AddSettingsChild(const wxDataViewItem &parent_item); diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 30b7de4..a4fd5c2 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -96,6 +96,13 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co }; field->m_parent = parent(); + if (edit_custom_gcode && opt.is_code) { + field->m_fn_edit_value = [this](std::string opt_id) { + if (!m_disabled) + this->edit_custom_gcode(opt_id); + }; + field->set_edit_tooltip(_L("Edit Custom G-code")); + } field->m_back_to_initial_value = [this](std::string opt_id) { if (!m_disabled) this->back_to_initial_value(opt_id); @@ -268,6 +275,7 @@ void OptionsGroup::activate_line(Line& line) if (!custom_ctrl && m_use_custom_ctrl) { custom_ctrl = new OG_CustomCtrl(is_legend_line || !staticbox ? this->parent() : static_cast(this->stb), this); + wxGetApp().UpdateDarkUI(custom_ctrl); if (is_legend_line) sizer->Add(custom_ctrl, 0, wxEXPAND | wxLEFT, wxOSX ? 0 : 10); else @@ -904,9 +912,10 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config ret = double_to_string(val); } break; - case coString: + case coString: { ret = from_u8(config.opt_string(opt_key)); break; + } case coStrings: if (opt_key == "compatible_printers" || opt_key == "compatible_prints" || opt_key == "gcode_substitutions") { ret = config.option(opt_key)->values; @@ -921,8 +930,8 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config else if (opt->gui_flags == "serialized") { std::vector values = config.option(opt_key)->values; if (!values.empty() && !values[0].empty()) - for (auto el : values) - text_value += el + ";"; + for (const std::string& el : values) + text_value += from_u8(el) + ";"; ret = text_value; } else @@ -946,10 +955,6 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config case coPoints: if (opt_key == "bed_shape") ret = config.option(opt_key)->values; - else if (opt_key == "bed_exclude_area") - ret = get_thumbnails_string(config.option(opt_key)->values); - else if (opt_key == "thumbnails") - ret = get_thumbnails_string(config.option(opt_key)->values); else ret = config.option(opt_key)->get_at(idx); break; @@ -1004,7 +1009,7 @@ wxString OptionsGroup::get_url(const std::string& path_end) if (path_end.empty()) return wxEmptyString; - wxString language = get_app_config()->get("translation_language"); + wxString language = wxGetApp().current_language_code_safe(); wxString lang_marker = language.IsEmpty() ? "en" : language.BeforeFirst('_'); return wxString("https://help.qidi3d.com/") + lang_marker + wxString("/article/" + path_end); diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index f754505..d738c18 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -122,6 +122,7 @@ public: std::function rescale_extra_column_item { nullptr }; std::function rescale_near_label_widget { nullptr }; + std::function edit_custom_gcode { nullptr }; wxFont sidetext_font {wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) }; wxFont label_font {wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) }; int sidetext_width{ -1 }; diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 80dfba5..672138e 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -180,7 +180,7 @@ PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent, wxString printer_ m_add_preset_btn->SetToolTip(_L("Add preset for this printer device")); m_add_preset_btn->Bind(wxEVT_BUTTON, &PhysicalPrinterDialog::AddPreset, this); - m_printer_name = new wxTextCtrl(this, wxID_ANY, printer_name, wxDefaultPosition, wxDefaultSize); + m_printer_name = new ::TextInput(this,printer_name); wxGetApp().UpdateDarkUI(m_printer_name); m_printer_name->Bind(wxEVT_TEXT, [this](wxEvent&) { this->update_full_printer_names(); }); @@ -238,7 +238,7 @@ PhysicalPrinterDialog::PhysicalPrinterDialog(wxWindow* parent, wxString printer_ if (new_printer) { m_printer_name->SetFocus(); - m_printer_name->SelectAll(); + m_printer_name->GetTextCtrl()->SelectAll(); } this->Fit(); @@ -432,11 +432,14 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr m_optgroup->activate(); + const auto opt = m_config->option>("host_type"); + m_last_host_type = opt->value; + m_opened_as_connect = (m_last_host_type == htQIDIConnect); Field* printhost_field = m_optgroup->get_field("print_host"); if (printhost_field) { - wxTextCtrl* temp = dynamic_cast(printhost_field->getWindow()); - if (temp) + text_ctrl* temp = dynamic_cast(printhost_field->getWindow()); + if (temp) { temp->Bind(wxEVT_TEXT, ([printhost_field, temp](wxEvent& e) { #ifndef __WXGTK__ @@ -454,6 +457,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr field->propagate_value(); }), temp->GetId()); } + } // Always fill in the "printhost_port" combo box from the config and select it. { @@ -484,13 +488,7 @@ void PhysicalPrinterDialog::update(bool printer_change) const auto opt = m_config->option>("host_type"); m_optgroup->show_field("host_type"); - // hide QIDIConnect address - if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { - if (wxTextCtrl* temp = dynamic_cast(printhost_field->getWindow()); temp && temp->GetValue() == L"https://connect.qidi3d.com") { - temp->SetValue(wxString()); - } - } - if (opt->value == htQIDILink) { // QIDIConnect does NOT allow http digest + if (opt->value == htQIDILink) { // PrusaConnect does NOT allow http digest m_optgroup->show_field("printhost_authorization_type"); AuthorizationType auth_type = m_config->option>("printhost_authorization_type")->value; m_optgroup->show_field("printhost_apikey", auth_type == AuthorizationType::atKeyPassword); @@ -502,15 +500,30 @@ void PhysicalPrinterDialog::update(bool printer_change) for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) m_optgroup->hide_field(opt_key); supports_multiple_printers = opt && opt->value == htRepetier; - if (opt->value == htQIDIConnect) { // automatically show default qidiconnect address - if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { - if (wxTextCtrl* temp = dynamic_cast(printhost_field->getWindow()); temp && temp->GetValue().IsEmpty()) { - temp->SetValue(L"https://connect.qidi3d.com"); } + // Hide Browse and Test buttons for Connect + if (opt->value == htQIDIConnect) { + m_printhost_browse_btn->Hide(); + // hide show hostname and PrusaConnect address + Field* printhost_field = m_optgroup->get_field("print_host"); + text_ctrl* printhost_win = printhost_field ? dynamic_cast(printhost_field->getWindow()) : nullptr; + if (!m_opened_as_connect && printhost_win && m_last_host_type != htQIDIConnect){ + m_stored_host = printhost_win->GetValue(); + printhost_win->SetValue(L"https://connect.prusa3d.com"); } + } else { + m_printhost_browse_btn->Show(); + // hide PrusaConnect address and show hostname + Field* printhost_field = m_optgroup->get_field("print_host"); + text_ctrl* printhost_win = printhost_field ? dynamic_cast(printhost_field->getWindow()) : nullptr; + if (!m_opened_as_connect && printhost_win && m_last_host_type == htQIDIConnect) { + wxString temp_host = printhost_win->GetValue(); + printhost_win->SetValue(m_stored_host); + m_stored_host = temp_host; } } + m_last_host_type = opt->value; } else { m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false); @@ -554,16 +567,19 @@ void PhysicalPrinterDialog::update_host_type(bool printer_change) || boost::starts_with(model, "XL") ); }; - // allowed models are: all MK3/S and MK2.5/S + // allowed models are: all MK3/S and MK2.5/S. + // Since 2.6.2 also MINI, which makes list of supported printers same for both services. + // Lets keep these 2 functions separated for now. auto model_supports_qidiconnect = [](const std::string& model) { return model.size() >= 2 && ((boost::starts_with(model, "MK") && model[2] > '2' && model[2] <= '9') + || boost::starts_with(model, "MINI") || boost::starts_with(model, "MK2.5") || boost::starts_with(model, "XL") ); }; - // set all_presets_are_qidilink_supported + // set all_presets_are_prusalink_supported for (PresetForPrinter* prstft : m_presets) { std::string preset_name = prstft->get_preset_name(); if (Preset* preset = wxGetApp().preset_bundle->printers.find_preset(preset_name)) { @@ -667,7 +683,7 @@ void PhysicalPrinterDialog::update_full_printer_names() InfoDialog(this, format_wxstr("%1%: \"%2%\" ", _L("Unexpected character"), str), _L("The following characters are not allowed in the name") + ": " + unusable_symbols).ShowModal(); m_printer_name->SetValue(printer_name); - m_printer_name->SetInsertionPointEnd(); + m_printer_name->GetTextCtrl()->SetInsertionPointEnd(); return; } } @@ -724,6 +740,18 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event) return; } + Field* printhost_field = m_optgroup->get_field("print_host"); + text_ctrl* printhost_win = printhost_field ? dynamic_cast(printhost_field->getWindow()) : nullptr; + const auto opt = m_config->option>("host_type"); + if (opt->value == htQIDIConnect) { + if (printhost_win && printhost_win->GetValue() != L"https://connect.qidi3d.com"){ + InfoDialog msg(this, _L("Warning"), _L("URL of QIDIConnect is different from https://connect.qidi3d.com. Do you want to continue?"), true, wxYES_NO); + if(msg.ShowModal() != wxID_YES){ + printhost_win->SetValue(L"https://connect.qidi3d.com"); + return; + } + } + } PhysicalPrinterCollection& printers = wxGetApp().preset_bundle->physical_printers; const PhysicalPrinter* existing = printers.find_printer(into_u8(printer_name), false); if (existing && into_u8(printer_name) != printers.get_selected_printer_name()) diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index bef74c2..8e34087 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -6,10 +6,10 @@ #include #include "libslic3r/Preset.hpp" +#include "Widgets/TextInput.hpp" #include "GUI_Utils.hpp" class wxString; -class wxTextCtrl; class wxStaticText; class ScalableButton; class wxBoxSizer; @@ -63,7 +63,7 @@ class PhysicalPrinterDialog : public DPIDialog wxString m_default_name; DynamicPrintConfig* m_config { nullptr }; - wxTextCtrl* m_printer_name { nullptr }; + ::TextInput* m_printer_name { nullptr }; std::vector m_presets; ConfigOptionsGroup* m_optgroup { nullptr }; @@ -76,6 +76,9 @@ class PhysicalPrinterDialog : public DPIDialog wxBoxSizer* m_presets_sizer {nullptr}; + wxString m_stored_host; + PrintHostType m_last_host_type; + bool m_opened_as_connect {false}; void build_printhost_settings(ConfigOptionsGroup* optgroup); void OnOK(wxEvent& event); void AddPreset(wxEvent& event); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9f5a218..2a7cb2a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3,13 +3,13 @@ #include #include -#include #include #include #include #include #include #include +#include #include #include #include @@ -37,6 +37,7 @@ #include #endif +#include #include "libslic3r/libslic3r.h" #include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/AMF.hpp" @@ -106,6 +107,7 @@ #include "MsgDialog.hpp" #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification +#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GLGizmoCut.hpp" #include "FileArchiveDialog.hpp" @@ -123,6 +125,7 @@ #include "libslic3r/CustomGCode.hpp" #include "libslic3r/Platform.hpp" +#include "Widgets/CheckBox.hpp" using boost::optional; namespace fs = boost::filesystem; using Slic3r::_3DScene; @@ -191,11 +194,9 @@ public: wxStaticText *info_size; wxStaticText *info_volume; wxStaticText *info_facets; -// wxStaticText *info_materials; wxStaticText *info_manifold; wxStaticText *label_volume; -// wxStaticText *label_materials; // ysFIXME - delete after next release if anyone will not complain about this std::vector sla_hidden_items; bool showing_manifold_warning_icon; @@ -493,7 +494,7 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : auto wiping_dialog_btn = [this](wxWindow* parent) { m_wiping_dialog_button = new wxButton(parent, wxID_ANY, _L("Purging volumes") + dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); - m_wiping_dialog_button->SetFont(wxGetApp().normal_font()); + wxGetApp().SetWindowVariantForButton(m_wiping_dialog_button); wxGetApp().UpdateDarkUI(m_wiping_dialog_button, true); auto sizer = new wxBoxSizer(wxHORIZONTAL); @@ -771,6 +772,7 @@ void Sidebar::priv::hide_rich_tip(wxButton* btn) Sidebar::Sidebar(Plater *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(42 * wxGetApp().em_unit(), -1)), p(new priv(parent)) { + SetFont(wxGetApp().normal_font()); p->scrolled = new wxScrolledWindow(this); // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 from https://github.com/qidi3d/QIDISlicer/commit/8f019e5fa992eac2c9a1e84311c990a943f80b01, // but this cause the bad layout of the sidebar, when all infoboxes appear. @@ -778,13 +780,12 @@ Sidebar::Sidebar(Plater *parent) // But if we set this value to 5, layout will be better p->scrolled->SetScrollRate(0, 5); - SetFont(wxGetApp().normal_font()); #ifndef __APPLE__ #ifdef _WIN32 wxGetApp().UpdateDarkUI(this); wxGetApp().UpdateDarkUI(p->scrolled); #else - SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +// SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); #endif #endif @@ -920,8 +921,8 @@ Sidebar::Sidebar(Plater *parent) int bmp_px_cnt = 32; #endif //__APPLE__ ScalableBitmap bmp = ScalableBitmap(this, icon_name, bmp_px_cnt); - *btn = new ScalableButton(this, wxID_ANY, bmp, label, wxBU_EXACTFIT); - (*btn)->SetFont(wxGetApp().bold_font()); + *btn = new ScalableButton(this, wxID_ANY, bmp, "", wxBU_EXACTFIT); + wxGetApp().SetWindowVariantForButton((*btn)); #ifdef _WIN32 (*btn)->Bind(wxEVT_ENTER_WINDOW, [tooltip, btn, this](wxMouseEvent& event) { @@ -938,9 +939,9 @@ Sidebar::Sidebar(Plater *parent) (*btn)->Hide(); }; - init_scalable_btn(&p->btn_send_gcode, "export_gcode", _L("Send to printer"), _L("Send to printer") + " " + GUI::shortkey_ctrl_prefix() + "Shift+G"); + init_scalable_btn(&p->btn_send_gcode , "export_gcode", _L("Send to printer") + " " +GUI::shortkey_ctrl_prefix() + "Shift+G"); // init_scalable_btn(&p->btn_eject_device, "eject_sd" , _L("Remove device ") + GUI::shortkey_ctrl_prefix() + "T"); - init_scalable_btn(&p->btn_export_gcode_removable, "export_to_sd", _L("Export"), _L("Export to SD card / Flash drive") + " " + GUI::shortkey_ctrl_prefix() + "U"); + init_scalable_btn(&p->btn_export_gcode_removable, "export_to_sd", _L("Export to SD card / Flash drive") + " " + GUI::shortkey_ctrl_prefix() + "U"); //Y14 // regular buttons "Slice now" and "Export G-code" @@ -953,6 +954,7 @@ Sidebar::Sidebar(Plater *parent) auto init_btn = [this](wxButton **btn, wxString label, const int button_height) { *btn = new wxButton(this, wxID_ANY, label, wxDefaultPosition, wxSize(-1, button_height), wxBU_EXACTFIT); + wxGetApp().SetWindowVariantForButton((*btn)); (*btn)->SetFont(wxGetApp().bold_font()); wxGetApp().UpdateDarkUI((*btn), true); }; @@ -1098,10 +1100,14 @@ void Sidebar::update_presets(Preset::Type preset_type) case Preset::TYPE_PRINTER: { update_all_preset_comboboxes(); +#if 1 // #ysFIXME_delete_after_test_of >> it looks like CallAfter() is no need [issue with disapearing of comboboxes are not reproducible] + p->show_preset_comboboxes(); +#else // CallAfter is really needed here to correct layout of the preset comboboxes, // when printer technology is changed during a project loading AND/OR switching the application mode. // Otherwise, some of comboboxes are invisible CallAfter([this]() { p->show_preset_comboboxes(); }); +#endif break; } @@ -1140,7 +1146,7 @@ void Sidebar::update_reslice_btn_tooltip() const void Sidebar::msw_rescale() { - SetMinSize(wxSize(40 * wxGetApp().em_unit(), -1)); + SetMinSize(wxSize(42 * wxGetApp().em_unit(), -1)); for (PlaterPresetComboBox* combo : std::vector { p->combo_print, p->combo_sla_print, @@ -1220,6 +1226,28 @@ void Sidebar::search() p->searcher.search(); } +void Sidebar::jump_to_option(const std::string& composite_key) +{ + const auto separator_pos = composite_key.find(";"); + const std::string opt_key = composite_key.substr(0, separator_pos); + const std::string tab_name = composite_key.substr(separator_pos + 1, composite_key.length()); + + for (Tab* tab : wxGetApp().tabs_list) { + if (tab->name() == tab_name) { + check_and_update_searcher(true); + + // Regularly searcher is sorted in respect to the options labels, + // so resort searcher before get an option + p->searcher.sort_options_by_key(); + const Search::Option& opt = p->searcher.get_option(opt_key, tab->type()); + tab->activate_option(opt_key, boost::nowide::narrow(opt.category)); + + // Revert sort of searcher back + p->searcher.sort_options_by_label(); + break; + } + } +} void Sidebar::jump_to_option(const std::string& opt_key, Preset::Type type, const std::wstring& category) { //const Search::Option& opt = p->searcher.get_option(opt_key, type); @@ -1479,8 +1507,7 @@ void Sidebar::update_sliced_info_sizer() p->sliced_info->SetTextAndShow(siEstimatedTime, info_text, new_label); } - // if there is a wipe tower, insert number of toolchanges info into the array: - p->sliced_info->SetTextAndShow(siWTNumbetOfToolchanges, is_wipe_tower ? wxString::Format("%.d", ps.total_toolchanges) : "N/A"); + p->sliced_info->SetTextAndShow(siWTNumbetOfToolchanges, ps.total_toolchanges > 0 ? wxString::Format("%.d", ps.total_toolchanges) : "N/A"); // Hide non-FFF sliced info parameters p->sliced_info->SetTextAndShow(siMateril_unit, "N/A"); @@ -1632,6 +1659,28 @@ private: Plater& m_plater; }; +namespace { +bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position) +{ + std::string svg_file_str = into_u8(svg_file); + GLCanvas3D* canvas = plater.canvas3D(); + if (canvas == nullptr) + return false; + auto base_svg = canvas->get_gizmos_manager().get_gizmo(GLGizmosManager::Svg); + if (base_svg == nullptr) + return false; + GLGizmoSVG* svg = dynamic_cast(base_svg); + if (svg == nullptr) + return false; + + // Refresh hover state to find surface point under mouse + wxMouseEvent evt(wxEVT_MOTION); + evt.SetPosition(wxPoint(mouse_drop_position.x(), mouse_drop_position.y())); + canvas->on_mouse(evt); // call render where is call GLCanvas3D::_picking_pass() + + return svg->create_volume(svg_file_str, mouse_drop_position, ModelVolumeType::MODEL_PART); +} +} bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) { #ifdef WIN32 @@ -1643,6 +1692,19 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi m_mainframe.select_tab(size_t(0)); if (wxGetApp().is_editor()) m_plater.select_view_3D("3D"); + // When only one .svg file is dropped on scene + if (filenames.size() == 1) { + const wxString &filename = filenames.Last(); + const wxString file_extension = filename.substr(filename.length() - 4); + if (file_extension.CmpNoCase(".svg") == 0) { + const wxPoint offset = m_plater.GetPosition(); + Vec2d mouse_position(x - offset.x, y - offset.y); + // Scale for retina displays + const GLCanvas3D *canvas = m_plater.canvas3D(); + canvas->apply_retina_scale(mouse_position); + return emboss_svg(m_plater, filename, mouse_position); + } + } bool res = m_plater.load_files(filenames); m_mainframe.update_title(); return res; @@ -1956,7 +2018,7 @@ struct Plater::priv bool can_split_to_volumes() const; bool can_arrange() const; bool can_layers_editing() const; - bool can_fix_through_netfabb() const; + bool can_fix_through_winsdk() const; bool can_simplify() const; bool can_set_instance_to_object() const; bool can_mirror() const; @@ -2043,7 +2105,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) , collapse_toolbar(GLToolbar::Normal, "Collapse") , m_project_filename(wxEmptyString) { - this->q->SetFont(Slic3r::GUI::wxGetApp().normal_font()); background_process.set_fff_print(&fff_print); background_process.set_sla_print(&sla_print); @@ -2124,6 +2185,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_RESET_SKEW, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); }); + view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MIRRORED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { this->sidebar->enable_buttons(evt.data); }); //Y5 view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_EXPORT_BUTTONS, [this](Event& evt) { this->sidebar->enable_export_buttons(evt.data); }); @@ -2236,6 +2298,23 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // Close notification ExportingFinished but only if last export was to removable notification_manager->device_ejected(); }); + this->q->Bind(EVT_REMOVABLE_DRIVE_ADDED, [this](wxCommandEvent& evt) { + if (!fs::exists(fs::path(evt.GetString().utf8_string()) / "prusa_printer_settings.ini")) + return; + if (evt.GetInt() == 0) { // not at startup, show dialog + wxGetApp().open_wifi_config_dialog(false, evt.GetString()); + } else { // at startup, show only notification + notification_manager->push_notification(NotificationType::WifiConfigFileDetected + , NotificationManager::NotificationLevel::ImportantNotificationLevel + // TRN Text of notification when Slicer starts and usb stick with printer settings ini file is present + , _u8L("Printer configuration file detected on removable media.") + // TRN Text of hypertext of notification when Slicer starts and usb stick with printer settings ini file is present + , _u8L("Write Wi-Fi credentials."), [evt/*, CONFIG_FILE_NAME*/](wxEvtHandler* evt_hndlr){ + wxGetApp().open_wifi_config_dialog(true, evt.GetString()); + return true;}); + } + + }); // Start the background thread and register this window as a target for update events. wxGetApp().removable_drive_manager()->init(this->q); #ifdef _WIN32 @@ -2398,46 +2477,15 @@ bool Plater::priv::get_config_bool(const std::string &key) const return wxGetApp().app_config->get_bool(key); } -// After loading of the presets from project, check if they are visible. -// Set them to visible if they are not. -void Plater::check_selected_presets_visibility(PrinterTechnology loaded_printer_technology) +void Plater::notify_about_installed_presets() { - auto update_selected_preset_visibility = [](PresetCollection& presets, std::vector& names) { - if (!presets.get_selected_preset().is_visible) { - assert(presets.get_selected_preset().name == presets.get_edited_preset().name); - presets.get_selected_preset().is_visible = true; - presets.get_edited_preset().is_visible = true; - names.emplace_back(presets.get_selected_preset().name); - } - }; - - std::vector names; - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - if (loaded_printer_technology == ptFFF) { - update_selected_preset_visibility(preset_bundle->prints, names); - for (const auto& extruder_filaments : preset_bundle->extruders_filaments) { - Preset* preset = preset_bundle->filaments.find_preset(extruder_filaments.get_selected_preset_name()); - if (preset && !preset->is_visible) { - preset->is_visible = true; - names.emplace_back(preset->name); - if (preset->name == preset_bundle->filaments.get_edited_preset().name) - preset_bundle->filaments.get_selected_preset().is_visible = true; - } - } - } - else { - update_selected_preset_visibility(preset_bundle->sla_prints, names); - update_selected_preset_visibility(preset_bundle->sla_materials, names); - } - update_selected_preset_visibility(preset_bundle->printers, names); - - preset_bundle->update_compatible(PresetSelectCompatibleType::Never); + const auto& names = wxGetApp().preset_bundle->tmp_installed_presets; // show notification about temporarily installed presets if (!names.empty()) { - std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active instance of QIDISlicer", - "The presets below were temporarily installed on the active instance of QIDISlicer", names.size())) + ":"; - for (std::string& name : names) + std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active instance of PrusaSlicer", + "The presets below were temporarily installed on the active instance of PrusaSlicer", names.size())) + ":"; + for (const std::string& name : names) notif_text += "\n - " + name; get_notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::PrintInfoNotificationLevel, notif_text); @@ -2486,6 +2534,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ int answer_convert_from_meters = wxOK_DEFAULT; int answer_convert_from_imperial_units = wxOK_DEFAULT; + int answer_consider_as_multi_part_objects = wxOK_DEFAULT; bool in_temp = false; const fs::path temp_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); @@ -2565,10 +2614,23 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_config) { if (!config.empty()) { + const auto* post_process = config.opt("post_process"); + if (post_process != nullptr && !post_process->values.empty()) { + // TRN The placeholder is either "3MF" or "AMF" + wxString msg = GUI::format_wxstr(_L("The selected %1% file contains a post-processing script.\n" + "Please review the script carefully before exporting G-code."), type_3mf ? "3MF" : "AMF" ); + std::string text; + for (const std::string& s : post_process->values) + text += s; + + InfoDialog msg_dlg(nullptr, msg, from_u8(text), true, wxOK | wxICON_WARNING); + msg_dlg.set_caption(wxString(SLIC3R_APP_NAME " - ") + _L("Attention!")); + msg_dlg.ShowModal(); + } Preset::normalize(config); PresetBundle* preset_bundle = wxGetApp().preset_bundle; preset_bundle->load_config_model(filename.string(), std::move(config)); - q->check_selected_presets_visibility(loaded_printer_technology); + q->notify_about_installed_presets(); if (loaded_printer_technology == ptFFF) CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, &preset_bundle->project_config); @@ -2667,14 +2729,21 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (model.looks_like_multipart_object()) { - MessageDialog msg_dlg(q, _L( + if (answer_consider_as_multi_part_objects == wxOK_DEFAULT) { + RichMessageDialog dlg(q, _L( "This file contains several objects positioned at multiple heights.\n" "Instead of considering them as multiple objects, should \n" "the file be loaded as a single object having multiple parts?") + "\n", - _L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO); - if (msg_dlg.ShowModal() == wxID_YES) { - model.convert_multipart_object(nozzle_dmrs->values.size()); + _L("Multi-part object detected"), wxICON_QUESTION | wxYES_NO); + dlg.ShowCheckBox(_L("Apply to all objects being loaded.")); + int answer = dlg.ShowModal(); + if (dlg.IsCheckBoxChecked()) + answer_consider_as_multi_part_objects = answer; + if (answer == wxID_YES) + model.convert_multipart_object(nozzle_dmrs->size()); } + else if (answer_consider_as_multi_part_objects == wxID_YES) + model.convert_multipart_object(nozzle_dmrs->size()); } } if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { @@ -3333,8 +3402,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool GLCanvas3D::ContoursList contours; contours.contours = background_process.fff_print()->get_sequential_print_clearance_contours(); canvas->set_sequential_print_clearance_contours(contours, true); - canvas->set_as_dirty(); - canvas->request_extra_frame(); } } } @@ -3349,8 +3416,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool GLCanvas3D::ContoursList contours; contours.contours = background_process.fff_print()->get_sequential_print_clearance_contours(); canvas->set_sequential_print_clearance_contours(contours, true); - canvas->set_as_dirty(); - canvas->request_extra_frame(); } } std::vector warnings; @@ -4433,7 +4498,10 @@ void Plater::priv::on_action_layersediting(SimpleEvent&) void Plater::priv::on_object_select(SimpleEvent& evt) { - wxGetApp().obj_list()->update_selections(); + if (auto obj_list = wxGetApp().obj_list()) + obj_list->update_selections(); + else + return; selection_changed(); } @@ -4472,9 +4540,13 @@ void Plater::priv::on_right_click(RBtnEvent& evt) const bool is_part = selection.is_single_volume_or_modifier() && ! selection.is_any_connector(); if (is_some_full_instances) menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu(); - else if (is_part) - menu = selection.is_single_text() ? menus.text_part_menu() : menus.part_menu(); - else + else if (is_part) { + const GLVolume* gl_volume = selection.get_first_volume(); + const ModelVolume *model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); + menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() : + (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : + menus.part_menu(); + } else menu = menus.multi_selection_menu(); // } } @@ -4828,15 +4900,15 @@ bool Plater::priv::can_delete_all() const return !model.objects.empty() && !sidebar->obj_list()->is_editing(); } -bool Plater::priv::can_fix_through_netfabb() const +bool Plater::priv::can_fix_through_winsdk() const { std::vector obj_idxs, vol_idxs; sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs); -#if FIX_THROUGH_NETFABB_ALWAYS +#if FIX_THROUGH_WINSDK_ALWAYS // Fixing always. return ! obj_idxs.empty() || ! vol_idxs.empty(); -#else // FIX_THROUGH_NETFABB_ALWAYS +#else // FIX_THROUGH_WINSDK_ALWAYS // Fixing only if the model is not manifold. if (vol_idxs.empty()) { for (auto obj_idx : obj_idxs) @@ -4850,7 +4922,7 @@ bool Plater::priv::can_fix_through_netfabb() const if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0) return true; return false; -#endif // FIX_THROUGH_NETFABB_ALWAYS +#endif // FIX_THROUGH_WINSDK_ALWAYS } bool Plater::priv::can_simplify() const @@ -5257,7 +5329,7 @@ void Sidebar::set_btn_label(const ActionButtonType btn_type, const wxString& lab // Plater / Public Plater::Plater(wxWindow *parent, MainFrame *main_frame) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size()) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size(parent)) , p(new priv(this, main_frame)) { // Initialization performed in the private c-tor @@ -5795,7 +5867,7 @@ void Plater::add_num_text(std::string num, Vec2d posotion) if (volume_type == ModelVolumeType::INVALID) volume_type = ModelVolumeType::MODEL_PART; - emboss->create_volume(volume_type, posotion, num); + // emboss->create_volume(volume_type, posotion, num); } @@ -5906,6 +5978,189 @@ void Plater::reload_gcode_from_disk() load_gcode(filename); } +static std::string rename_file(const std::string& filename, const std::string& extension) +{ + const boost::filesystem::path src_path(filename); + std::string src_stem = src_path.stem().string(); + int value = 0; + if (src_stem.back() == ')') { + const size_t pos = src_stem.find_last_of('('); + if (pos != std::string::npos) { + const std::string value_str = src_stem.substr(pos + 1, src_stem.length() - pos); + try + { + value = std::stoi(value_str); + src_stem = src_stem.substr(0, pos); + } + catch (...) + { + // do nothing + } + } + } + + boost::filesystem::path dst_path(filename); + dst_path.remove_filename(); + dst_path /= src_stem + "(" + std::to_string(value + 1) + ")" + extension; + return dst_path.string(); +} + +void Plater::convert_gcode_to_ascii() +{ + // Ask user for a gcode file name. + wxString input_file; + wxGetApp().load_gcode(this, input_file); + if (input_file.empty()) + return; + + // Open source file + FilePtr in_file{ boost::nowide::fopen(into_u8(input_file).c_str(), "rb") }; + if (in_file.f == nullptr) { + MessageDialog msg_dlg(this, _L("Unable to open the selected file."), _L("Error"), wxICON_ERROR | wxOK); + msg_dlg.ShowModal(); + return; + } + + // Set out filename + const boost::filesystem::path input_path(into_u8(input_file)); + boost::filesystem::path output_path(into_u8(input_file)); + std::string output_file = output_path.replace_extension("gcode").string(); + + if (input_file == output_file) { + using namespace bgcode::core; + EResult res = is_valid_binary_gcode(*in_file.f); + if (res == EResult::InvalidMagicNumber) { + MessageDialog msg_dlg(this, _L("The selected file is already in ASCII format."), _L("Warning"), wxOK); + msg_dlg.ShowModal(); + return; + } + else { + output_file = rename_file(output_file, ".gcode"); + wxString msg = GUI::format_wxstr("The converted binary G-code file has '.gcode' extension.\n" + "The exported file will be renamed to:\n\n%1%\n\nDo you want to continue?", output_file); + MessageDialog msg_dlg(this, msg, _L("Warning"), wxYES_NO); + if (msg_dlg.ShowModal() != wxID_YES) + return; + } + } + + const bool exists = boost::filesystem::exists(output_file); + if (exists) { + MessageDialog msg_dlg(this, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), output_file), _L("Notice"), wxYES_NO); + if (msg_dlg.ShowModal() != wxID_YES) + return; + } + + // Open destination file + FilePtr out_file{ boost::nowide::fopen(output_file.c_str(), "wb") }; + if (out_file.f == nullptr) { + MessageDialog msg_dlg(this, _L("Unable to open output file."), _L("Error"), wxICON_ERROR | wxOK); + msg_dlg.ShowModal(); + return; + } + + // Perform conversion + { + wxBusyCursor busy; + using namespace bgcode::core; + EResult res = bgcode::convert::from_binary_to_ascii(*in_file.f, *out_file.f, true); + if (res == EResult::InvalidMagicNumber) { + in_file.close(); + out_file.close(); + boost::filesystem::copy_file(input_path, output_path, boost::filesystem::copy_option::overwrite_if_exists); + } + else if (res != EResult::Success) { + MessageDialog msg_dlg(this, _L(std::string(translate_result(res))), _L("Error converting G-code file"), wxICON_INFORMATION | wxOK); + msg_dlg.ShowModal(); + out_file.close(); + boost::nowide::remove(output_file.c_str()); + return; + } + } + + MessageDialog msg_dlg(this, Slic3r::GUI::format_wxstr("%1%\n%2%", _L("Successfully created G-code ASCII file"), output_file), + _L("Convert G-code file to ASCII format"), wxICON_ERROR | wxOK); + msg_dlg.ShowModal(); +} + +void Plater::convert_gcode_to_binary() +{ + // Ask user for a gcode file name. + wxString input_file; + wxGetApp().load_gcode(this, input_file); + if (input_file.empty()) + return; + + // Open source file + FilePtr in_file{ boost::nowide::fopen(into_u8(input_file).c_str(), "rb") }; + if (in_file.f == nullptr) { + MessageDialog msg_dlg(this, _L("Unable to open the selected file."), _L("Error"), wxICON_ERROR | wxOK); + msg_dlg.ShowModal(); + return; + } + + // Set out filename + const boost::filesystem::path input_path(into_u8(input_file)); + boost::filesystem::path output_path(into_u8(input_file)); + std::string output_file = output_path.replace_extension("bgcode").string(); + + if (input_file == output_file) { + using namespace bgcode::core; + EResult res = is_valid_binary_gcode(*in_file.f); + if (res == EResult::Success) { + MessageDialog msg_dlg(this, _L("The selected file is already in binary format."), _L("Warning"), wxOK); + msg_dlg.ShowModal(); + return; + } + else { + output_file = rename_file(output_file, ".bgcode"); + wxString msg = GUI::format_wxstr("The converted ASCII G-code file has '.bgcode' extension.\n" + "The exported file will be renamed to:\n\n%1%\n\nDo you want to continue?", output_file); + MessageDialog msg_dlg(this, msg, _L("Warning"), wxYES_NO); + if (msg_dlg.ShowModal() != wxID_YES) + return; + } + } + + const bool exists = boost::filesystem::exists(output_file); + if (exists) { + MessageDialog msg_dlg(this, GUI::format_wxstr(_L("File %1% already exists. Do you wish to overwrite it?"), output_file), _L("Notice"), wxYES_NO); + if (msg_dlg.ShowModal() != wxID_YES) + return; + } + + // Open destination file + FilePtr out_file{ boost::nowide::fopen(output_file.c_str(), "wb") }; + if (out_file.f == nullptr) { + MessageDialog msg_dlg(this, _L("Unable to open output file."), _L("Error"), wxICON_ERROR | wxOK); + msg_dlg.ShowModal(); + return; + } + + // Perform conversion + { + wxBusyCursor busy; + using namespace bgcode::core; + const bgcode::binarize::BinarizerConfig& binarizer_config = GCodeProcessor::get_binarizer_config(); + const EResult res = bgcode::convert::from_ascii_to_binary(*in_file.f, *out_file.f, binarizer_config); + if (res == EResult::AlreadyBinarized) { + in_file.close(); + out_file.close(); + boost::filesystem::copy_file(input_path, output_path, boost::filesystem::copy_option::overwrite_if_exists); + } + else if (res != EResult::Success) { + MessageDialog msg_dlg(this, _L(std::string(translate_result(res))), _L("Error converting G-code file"), wxICON_INFORMATION | wxOK); + msg_dlg.ShowModal(); + out_file.close(); + boost::nowide::remove(output_file.c_str()); + return; + } + } + + MessageDialog msg_dlg(this, Slic3r::GUI::format_wxstr("%1%\n%2%", _L("Successfully created G-code binary file"), output_file), + _L("Convert G-code file to binary format"), wxICON_ERROR | wxOK); + msg_dlg.ShowModal(); +} void Plater::refresh_print() { p->preview->refresh_print(); @@ -6139,6 +6394,7 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) // Decompress action. We already has correct file index in stat structure. mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); if (res == 0) { + // TRN: First argument = path to file, second argument = error description wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2% "), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive))); BOOST_LOG_TRIVIAL(error) << error_log; show_error(nullptr, error_log); @@ -6413,7 +6669,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/*=false*/) { const std::regex pattern_drop(".*[.](stl|obj|amf|3mf|qidi|step|stp|zip)", std::regex::icase); - const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase); + const std::regex pattern_gcode_drop(".*[.](gcode|g|bgcode|bgc)", std::regex::icase); std::vector paths; @@ -6861,6 +7117,55 @@ void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& ne w.wait_for_idle(); } +static wxString check_binary_vs_ascii_gcode_extension(PrinterTechnology pt, const std::string& ext, bool binary_output) +{ + wxString err_out; + if (pt == ptFFF) { + const bool binary_extension = (ext == ".bgcode" || ext == ".bgc"); + const bool ascii_extension = (ext == ".gcode" || ext == ".g" || ext == ".gco"); + if (binary_output && ascii_extension) { + // TRN The placeholder %1% is the file extension the user has selected. + err_out = format_wxstr(_L("Cannot save binary G-code with %1% extension.\n\n" + "Use a different extension or disable binary G-code export " + "in Print Settings."), ext, "output_filename_format;print", "gcode_binary;print"); + } + if (!binary_output && binary_extension) { + // TRN The placeholder %1% is the file extension the user has selected. + err_out = format_wxstr(_L("Cannot save ASCII G-code with %1% extension.\n\n" + "Use a different extension or enable binary G-code export " + "in Print Settings."), ext, "output_filename_format;print", "gcode_binary;print"); + } + } + return err_out; +} + + + +// This function should be deleted when binary G-codes become more common. The dialog is there to make the +// transition period easier for the users, because bgcode files are not recognized by older firmwares +// without any error message. +static void alert_when_exporting_binary_gcode(bool binary_output, const std::string& printer_notes) +{ + if (binary_output + && (boost::algorithm::contains(printer_notes, "PRINTER_MODEL_XL") + || boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MINI") + || boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MK4") + || boost::algorithm::contains(printer_notes, "PRINTER_MODEL_MK3.9"))) + { + AppConfig* app_config = wxGetApp().app_config; + wxWindow* parent = wxGetApp().mainframe; + const std::string option_key = "dont_warn_about_firmware_version_when_exporting_binary_gcode"; + + if (app_config->get(option_key) != "1") { + RichMessageDialog dialog(parent, _L("You are exporting binary G-code for a Prusa printer. Please, make sure that your printer " + "is running firmware version 5.1.0-alpha2 or later. Older firmwares are not able to handle binary G-codes."), + _L("Exporting binary G-code"), wxICON_WARNING | wxOK); + dialog.ShowCheckBox(_L("Don't show again")); + if (dialog.ShowModal() == wxID_OK && dialog.IsCheckBoxChecked()) + app_config->set(option_key, "1"); + } + } +} void Plater::export_gcode(bool prefer_removable) { if (p->model.objects.empty()) @@ -6916,16 +7221,25 @@ void Plater::export_gcode(bool prefer_removable) ); if (dlg.ShowModal() == wxID_OK) { output_path = into_path(dlg.GetPath()); - while (has_illegal_filename_characters(output_path.filename().string())) { - show_error(this, _L("The provided file name is not valid.") + "\n" + - _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""); - dlg.SetFilename(from_path(output_path.filename())); - if (dlg.ShowModal() == wxID_OK) - output_path = into_path(dlg.GetPath()); - else { - output_path.clear(); - break; + auto check_for_error = [this](const boost::filesystem::path& path, wxString& err_out) -> bool { + const std::string filename = path.filename().string(); + const std::string ext = boost::algorithm::to_lower_copy(path.extension().string()); + if (has_illegal_filename_characters(filename)) { + err_out = _L("The provided file name is not valid.") + "\n" + + _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""; + return true; } + err_out = check_binary_vs_ascii_gcode_extension(printer_technology(), ext, wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("gcode_binary")); + return !err_out.IsEmpty(); + }; + + wxString error_str; + if (check_for_error(output_path, error_str)) { + ErrorDialog(this, error_str, [this](const std::string& key) -> void { sidebar().jump_to_option(key); }).ShowModal(); + output_path.clear(); + } else { + alert_when_exporting_binary_gcode(wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("gcode_binary"), + wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_notes")); } } } @@ -7132,6 +7446,108 @@ void Plater::export_amf() } } +namespace { +std::string get_file_name(const std::string &file_path) +{ + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + size_t pos_point = file_path.find_last_of('.'); + size_t offset = pos_last_delimiter + 1; + size_t count = pos_point - pos_last_delimiter - 1; + return file_path.substr(offset, count); +} +using SvgFile = EmbossShape::SvgFile; +using SvgFiles = std::vector; +std::string create_unique_3mf_filepath(const std::string &file, const SvgFiles svgs) +{ + // const std::string MODEL_FOLDER = "3D/"; // copy from file 3mf.cpp + std::string path_in_3mf = "3D/" + file + ".svg"; + size_t suffix_number = 0; + bool is_unique = false; + do{ + is_unique = true; + path_in_3mf = "3D/" + file + ((suffix_number++)? ("_" + std::to_string(suffix_number)) : "") + ".svg"; + for (SvgFile *svgfile : svgs) { + if (svgfile->path_in_3mf.empty()) + continue; + if (svgfile->path_in_3mf.compare(path_in_3mf) == 0) { + is_unique = false; + break; + } + } + } while (!is_unique); + return path_in_3mf; +} + +bool set_by_local_path(SvgFile &svg, const SvgFiles& svgs) +{ + // Try to find already used svg file + for (SvgFile *svg_ : svgs) { + if (svg_->path_in_3mf.empty()) + continue; + if (svg.path.compare(svg_->path) == 0) { + svg.path_in_3mf = svg_->path_in_3mf; + return true; + } + } + return false; +} + +/// +/// Function to secure private data before store to 3mf +/// +/// Data(also private) to clean before publishing +void publish(Model &model) { + + // SVG file publishing + bool exist_new = false; + SvgFiles svgfiles; + for (ModelObject *object: model.objects){ + for (ModelVolume *volume : object->volumes) { + if (!volume->emboss_shape.has_value()) + continue; + if (volume->text_configuration.has_value()) + continue; // text dosen't have svg path + + assert(volume->emboss_shape->svg_file.has_value()); + if (!volume->emboss_shape->svg_file.has_value()) + continue; + + SvgFile* svg = &(*volume->emboss_shape->svg_file); + if (svg->path_in_3mf.empty()) + exist_new = true; + svgfiles.push_back(svg); + } + } + + if (exist_new){ + MessageDialog dialog(nullptr, + _L("Are you sure you want to store original SVGs with their local paths into the 3MF file?\n" + "If you hit 'NO', all SVGs in the project will not be editable any more."), + _L("Private protection"), wxYES_NO | wxICON_QUESTION); + if (dialog.ShowModal() == wxID_NO){ + for (ModelObject *object : model.objects) + for (ModelVolume *volume : object->volumes) + if (volume->emboss_shape.has_value()) + volume->emboss_shape.reset(); + } + } + + for (SvgFile* svgfile : svgfiles){ + if (!svgfile->path_in_3mf.empty()) + continue; // already suggested path (previous save) + + // create unique name for svgs, when local path differ + std::string filename = "unknown"; + if (!svgfile->path.empty()) { + if (set_by_local_path(*svgfile, svgfiles)) + continue; + // check whether original filename is already in: + filename = get_file_name(svgfile->path); + } + svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); + } +} +} bool Plater::export_3mf(const boost::filesystem::path& output_path) { if (p->model.objects.empty()) { @@ -7152,6 +7568,9 @@ bool Plater::export_3mf(const boost::filesystem::path& output_path) if (!path.Lower().EndsWith(".3mf")) return false; + // take care about private data stored into .3mf + // modify model + publish(p->model); DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); wxBusyCursor wait; @@ -7366,13 +7785,25 @@ void Plater::send_gcode() PrintHostSendDialog dlg(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage_paths, storage_names); if (dlg.ShowModal() == wxID_OK) { + { + const std::string ext = boost::algorithm::to_lower_copy(dlg.filename().extension().string()); + const bool binary_output = wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("gcode_binary"); + const wxString error_str = check_binary_vs_ascii_gcode_extension(printer_technology(), ext, binary_output); + if (! error_str.IsEmpty()) { + ErrorDialog(this, error_str, t_kill_focus([](const std::string& key) -> void { wxGetApp().sidebar().jump_to_option(key); })).ShowModal(); + return; + } + } + + alert_when_exporting_binary_gcode(wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("gcode_binary"), + wxGetApp().preset_bundle->printers.get_edited_preset().config.opt_string("printer_notes")); upload_job.upload_data.upload_path = dlg.filename(); upload_job.upload_data.post_action = dlg.post_action(); upload_job.upload_data.group = dlg.group(); upload_job.upload_data.storage = dlg.storage(); - // Show "Is printer clean" dialog for QIDIConnect - Upload and print. - if (std::string(upload_job.printhost->get_name()) == "QIDIConnect" && upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + // Show "Is printer clean" dialog for PrusaConnect - Upload and print. + if (std::string(upload_job.printhost->get_name()) == "PrusaConnect" && upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) { GUI::MessageDialog dlg(nullptr, _L("Is the printer ready? Is the print sheet in place, empty and clean?"), _L("Upload and Print"), wxOK | wxCANCEL); if (dlg.ShowModal() != wxID_OK) return; @@ -7896,28 +8327,41 @@ void Plater::clear_before_change_mesh(int obj_idx, const std::string ¬ificati void Plater::changed_mesh(int obj_idx) { ModelObject* mo = model().objects[obj_idx]; + if (p->printer_technology == ptSLA) sla::reproject_points_and_holes(mo); update(); p->object_list_changed(); p->schedule_background_process(); } +void Plater::changed_object(ModelObject &object){ + assert(object.get_model() == &p->model); // is object from same model? + object.invalidate_bounding_box(); + // recenter and re - align to Z = 0 + object.ensure_on_bed(p->printer_technology != ptSLA); + + if (p->printer_technology == ptSLA) { + // Update the SLAPrint from the current Model, so that the reload_scene() + // pulls the correct data, update the 3D scene. + p->update_restart_background_process(true, false); + } else + p->view3D->reload_scene(false); + + // update print + p->schedule_background_process(); + + // Check outside bed + get_current_canvas3D()->requires_check_outside_state(); +} + void Plater::changed_object(int obj_idx) { if (obj_idx < 0) return; - // recenter and re - align to Z = 0 - p->model.objects[obj_idx]->ensure_on_bed(p->printer_technology != ptSLA); - if (this->p->printer_technology == ptSLA) { - // Update the SLAPrint from the current Model, so that the reload_scene() - // pulls the correct data, update the 3D scene. - this->p->update_restart_background_process(true, false); - } - else - p->view3D->reload_scene(false); - - // update print - this->p->schedule_background_process(); + ModelObject *object = p->model.objects[obj_idx]; + if (object == nullptr) + return; + changed_object(*object); } void Plater::changed_objects(const std::vector& object_idxs) @@ -8161,7 +8605,7 @@ bool Plater::can_delete_all() const { return p->can_delete_all(); } bool Plater::can_increase_instances() const { return p->can_increase_instances(); } bool Plater::can_decrease_instances(int obj_idx/* = -1*/) const { return p->can_decrease_instances(obj_idx); } bool Plater::can_set_instance_to_object() const { return p->can_set_instance_to_object(); } -bool Plater::can_fix_through_netfabb() const { return p->can_fix_through_netfabb(); } +bool Plater::can_fix_through_winsdk() const { return p->can_fix_through_winsdk(); } bool Plater::can_simplify() const { return p->can_simplify(); } bool Plater::can_split_to_objects() const { return p->can_split_to_objects(); } bool Plater::can_split_to_volumes() const { return p->can_split_to_volumes(); } @@ -8261,6 +8705,7 @@ void Plater::bring_instance_forward() wxMenu* Plater::object_menu() { return p->menus.object_menu(); } wxMenu* Plater::part_menu() { return p->menus.part_menu(); } wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } +wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); } wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } wxMenu* Plater::default_menu() { return p->menus.default_menu(); } wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 704a550..ae751ee 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -19,7 +19,7 @@ #include "libslic3r/GCode.hpp" -#include "libslic3r/GCodeWriter.hpp" +#include "libslic3r/Gcode/GCodeWriter.hpp" #include "libslic3r/PrintConfig.hpp" class wxButton; @@ -32,8 +32,6 @@ namespace Slic3r { class BuildVolume; class Model; class ModelObject; -enum class ModelObjectCutAttribute : int; -using ModelObjectCutAttributes = enum_bitmask; class ModelInstance; class Print; class SLAPrint; @@ -58,7 +56,6 @@ class ObjectSettings; class ObjectLayers; class ObjectList; class GLCanvas3D; -class GLGizmoEmboss; class Mouse3DController; class NotificationManager; struct Camera; @@ -94,6 +91,8 @@ public: void search(); void jump_to_option(size_t selected); void jump_to_option(const std::string& opt_key, Preset::Type type, const std::wstring& category); + // jump to option which is represented by composite key : "opt_key;tab_name" + void jump_to_option(const std::string& composite_key); ObjectManipulation* obj_manipul(); ObjectList* obj_list(); @@ -195,6 +194,8 @@ public: void load_gcode(); void load_gcode(const wxString& filename); void reload_gcode_from_disk(); + void convert_gcode_to_ascii(); + void convert_gcode_to_binary(); void refresh_print(); std::vector load_files(const std::vector& input_files, bool load_model = true, bool load_config = true, bool imperial_units = false); @@ -202,7 +203,7 @@ public: std::vector load_files(const std::vector& input_files, bool load_model = true, bool load_config = true, bool imperial_units = false); // to be called on drag and drop bool load_files(const wxArrayString& filenames, bool delete_after_load = false); - void check_selected_presets_visibility(PrinterTechnology loaded_printer_technology); + void notify_about_installed_presets(); bool preview_zip_archive(const boost::filesystem::path& input_file); @@ -305,6 +306,7 @@ public: void clear_before_change_mesh(int obj_idx, const std::string ¬ification_msg); void changed_mesh(int obj_idx); + void changed_object(ModelObject &object); void changed_object(int obj_idx); void changed_objects(const std::vector& object_idxs); void schedule_background_process(bool schedule = true); @@ -382,7 +384,7 @@ public: bool can_increase_instances() const; bool can_decrease_instances(int obj_idx = -1) const; bool can_set_instance_to_object() const; - bool can_fix_through_netfabb() const; + bool can_fix_through_winsdk() const; bool can_simplify() const; bool can_split_to_objects() const; bool can_split_to_volumes() const; @@ -500,6 +502,7 @@ public: wxMenu* object_menu(); wxMenu* part_menu(); wxMenu* text_part_menu(); + wxMenu* svg_part_menu(); wxMenu* sla_object_menu(); wxMenu* default_menu(); wxMenu* instance_menu(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 8af4976..3bce1fa 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -13,6 +13,7 @@ #include "GLCanvas3D.hpp" #include "ConfigWizard.hpp" +#include "Widgets/SpinInput.hpp" #include #ifdef WIN32 @@ -56,13 +57,33 @@ namespace GUI { PreferencesDialog::PreferencesDialog(wxWindow* parent) : DPIDialog(parent, wxID_ANY, _L("Preferences"), wxDefaultPosition, - wxDefaultSize, wxDEFAULT_DIALOG_STYLE) + wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { #ifdef __WXOSX__ isOSX = true; #endif build(); + wxSize sz = GetSize(); + bool is_scrollbar_shown = false; + + const size_t pages_cnt = tabs->GetPageCount(); + for (size_t tab_id = 0; tab_id < pages_cnt; tab_id++) { + wxSizer* tab_sizer = tabs->GetPage(tab_id)->GetSizer(); + wxScrolledWindow* scrolled = static_cast(tab_sizer->GetItem(size_t(0))->GetWindow()); + scrolled->SetScrollRate(0, 5); + + is_scrollbar_shown |= scrolled->GetScrollLines(wxVERTICAL) > 0; + } + + if (is_scrollbar_shown) + sz.x += 2*em_unit(); +#ifdef __WXGTK__ + // To correct Layout of wxScrolledWindow we need at least small change of size + else + sz.x += 1; +#endif + SetSize(sz); m_highlighter.set_timer_owner(this, 0); } @@ -106,6 +127,11 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin for (const std::string& opt_key : {"suppress_hyperlinks", "downloader_url_registered"}) m_optgroup_other->set_value(opt_key, app_config->get_bool(opt_key)); + for (const std::string& opt_key : { "default_action_on_close_application" + ,"default_action_on_new_project" + ,"default_action_on_select_preset" }) + m_optgroup_general->set_value(opt_key, app_config->get(opt_key) == "none"); + m_optgroup_general->set_value("default_action_on_dirty_project", app_config->get("default_action_on_dirty_project").empty()); // update colors for color pickers of the labels update_color(m_sys_colour, wxGetApp().get_label_clr_sys()); update_color(m_mod_colour, wxGetApp().get_label_clr_modified()); @@ -126,11 +152,17 @@ static std::shared_ptrcreate_options_tab(const wxString& tit tabs->AddPage(tab, _(title)); tab->SetFont(wxGetApp().normal_font()); + auto scrolled = new wxScrolledWindow(tab); + + // Sizer in the scrolled area + auto* scrolled_sizer = new wxBoxSizer(wxVERTICAL); + scrolled->SetSizer(scrolled_sizer); wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(scrolled, 1, wxEXPAND); sizer->SetSizeHints(tab); tab->SetSizer(sizer); - std::shared_ptr optgroup = std::make_shared(tab); + std::shared_ptr optgroup = std::make_shared(scrolled); optgroup->label_width = 40; optgroup->set_config_category_and_type(title, int(Preset::TYPE_PREFERENCES)); return optgroup; @@ -143,6 +175,7 @@ static void activate_options_tab(std::shared_ptr optgroup) wxBoxSizer* sizer = static_cast(static_cast(optgroup->parent())->GetSizer()); sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10); + optgroup->parent()->Layout(); // apply sercher wxGetApp().sidebar().get_searcher().append_preferences_options(optgroup->get_lines()); } @@ -204,7 +237,7 @@ void PreferencesDialog::build() #ifdef _WIN32 wxGetApp().UpdateDarkUI(this); #else - SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + //SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); #endif const wxFont& font = wxGetApp().normal_font(); SetFont(font); @@ -215,7 +248,12 @@ void PreferencesDialog::build() tabs = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNB_DEFAULT); #else tabs = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL |wxNB_NOPAGETHEME | wxNB_DEFAULT ); - tabs->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#ifdef __linux__ + tabs->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, [this](wxBookCtrlEvent& e) { + e.Skip(); + CallAfter([this]() { tabs->GetCurrentPage()->Layout(); }); + }); +#endif #endif // Add "General" tab @@ -280,10 +318,6 @@ void PreferencesDialog::build() L("If enabled, sets QIDISlicer as default application to open .stl files."), app_config->get_bool("associate_stl")); - append_bool_option(m_optgroup_general, "associate_step", - L("Associate .stp files to QIDISlicer"), - L("If enabled, sets QIDISlicer as default application to open .stp files."), - app_config->get_bool("associate_step")); #endif // _WIN32 m_optgroup_general->append_separator(); @@ -364,6 +398,10 @@ void PreferencesDialog::build() L("Associate .gcode files to QIDISlicer G-code Viewer"), L("If enabled, sets QIDISlicer G-code Viewer as default application to open .gcode files."), app_config->get_bool("associate_gcode")); + append_bool_option(m_optgroup_general, "associate_bgcode", + L("Associate .bgcode files to PrusaSlicer G-code Viewer"), + L("If enabled, sets PrusaSlicer G-code Viewer as default application to open .bgcode files."), + app_config->get_bool("associate_bgcode")); } #endif // _WIN32 @@ -513,13 +551,20 @@ void PreferencesDialog::build() #ifdef _MSW_DARK_MODE append_bool_option(m_optgroup_gui, "tabs_as_menu", - L("Set settings tabs as menu items (experimental)"), + L("Set settings tabs as menu items"), L("If enabled, Settings Tabs will be placed as menu items. If disabled, old UI will be used."), app_config->get_bool("tabs_as_menu")); #endif m_optgroup_gui->append_separator(); +/* + append_bool_option(m_optgroup_gui, "suppress_round_corners", + L("Suppress round corners for controls (experimental)"), + L("If enabled, Settings Tabs will be placed as menu items. If disabled, old UI will be used."), + app_config->get("suppress_round_corners") == "1"); + m_optgroup_gui->append_separator(); +*/ append_bool_option(m_optgroup_gui, "show_hints", L("Show \"Tip of the day\" notification after start"), L("If enabled, useful hints are displayed at startup."), @@ -586,7 +631,7 @@ void PreferencesDialog::build() activate_options_tab(m_optgroup_other); create_downloader_path_sizer(); -// create_settings_font_widget(); + create_settings_font_widget(); #if ENABLE_ENVIRONMENT_MAP // Add "Render" tab @@ -606,10 +651,11 @@ void PreferencesDialog::build() activate_options_tab(m_optgroup_render); #endif // ENABLE_ENVIRONMENT_MAP + } #ifdef _WIN32 // Add "Dark Mode" tab - m_optgroup_dark_mode = create_options_tab(_L("Dark mode (experimental)"), tabs); + m_optgroup_dark_mode = create_options_tab(_L("Dark mode"), tabs); m_optgroup_dark_mode->m_on_change = [this](t_config_option_key opt_key, boost::any value) { if (auto it = m_values.find(opt_key); it != m_values.end()) { m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected @@ -635,7 +681,6 @@ void PreferencesDialog::build() activate_options_tab(m_optgroup_dark_mode); #endif //_WIN32 - } // update alignment of the controls for all tabs update_ctrls_alignment(); @@ -644,6 +689,8 @@ void PreferencesDialog::build() sizer->Add(tabs, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxGetApp().SetWindowVariantForButton(buttons->GetAffirmativeButton()); + wxGetApp().SetWindowVariantForButton(buttons->GetCancelButton()); this->Bind(wxEVT_BUTTON, &PreferencesDialog::accept, this, wxID_OK); this->Bind(wxEVT_BUTTON, &PreferencesDialog::revert, this, wxID_CANCEL); @@ -699,7 +746,7 @@ void PreferencesDialog::accept(wxEvent&) #endif // __linux__ } - std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled", "font_size" }; + std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled", "font_pt_size", "suppress_round_corners" }; for (const std::string& option : options_to_recreate_GUI) { if (m_values.find(option) != m_values.end()) { @@ -880,7 +927,7 @@ void PreferencesDialog::refresh_og(std::shared_ptr og) { og->parent()->Layout(); tabs->Layout(); - this->layout(); +// this->layout(); } void PreferencesDialog::create_icon_size_slider() @@ -1058,24 +1105,29 @@ void PreferencesDialog::create_settings_font_widget() wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); - const std::string opt_key = "font_size"; + const std::string opt_key = "font_pt_size"; m_blinkers[opt_key] = new BlinkingBitmap(parent); wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxHORIZONTAL); wxStaticText* font_example = new wxStaticText(parent, wxID_ANY, "Application text"); int val = wxGetApp().normal_font().GetPointSize(); - wxSpinCtrl* size_sc = new wxSpinCtrl(parent, wxID_ANY, format_wxstr("%1%", val), wxDefaultPosition, wxSize(15*em_unit(), -1), wxTE_PROCESS_ENTER | wxSP_ARROW_KEYS + SpinInput* size_sc = new SpinInput(parent, format_wxstr("%1%", val), "", wxDefaultPosition, wxSize(15 * em_unit(), -1), wxTE_PROCESS_ENTER | wxSP_ARROW_KEYS #ifdef _WIN32 | wxBORDER_SIMPLE #endif - , 8, 20); + , 8, wxGetApp().get_max_font_pt_size()); wxGetApp().UpdateDarkUI(size_sc); - auto apply_font = [this, font_example, opt_key](const int val, const wxFont& font) { + auto apply_font = [this, font_example, opt_key, stb_sizer](const int val, const wxFont& font) { font_example->SetFont(font); m_values[opt_key] = format("%1%", val); + stb_sizer->Layout(); +#ifdef __linux__ + CallAfter([this]() { refresh_og(m_optgroup_other); }); +#else refresh_og(m_optgroup_other); +#endif }; auto change_value = [size_sc, apply_font](wxCommandEvent& evt) { diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 72337d2..c016802 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -435,6 +435,7 @@ void PresetComboBox::update_from_bundle() void PresetComboBox::msw_rescale() { m_em_unit = em_unit(this); + ::ComboBox::Rescale(); } void PresetComboBox::sys_color_changed() @@ -788,7 +789,7 @@ void PlaterPresetComboBox::show_edit_menu() [this](wxCommandEvent&) { this->change_extruder_color(); }, "funnel", menu, []() { return true; }, wxGetApp().plater()); #endif //__linux__ append_menu_item(menu, wxID_ANY, _L("Show/Hide template presets"), "", - [this](wxCommandEvent&) { wxGetApp().open_preferences("no_templates", "General"); }, "spool", menu, []() { return true; }, wxGetApp().plater()); + [](wxCommandEvent&) { wxGetApp().open_preferences("no_templates", "General"); }, "spool", menu, []() { return true; }, wxGetApp().plater()); wxGetApp().plater()->PopupMenu(menu); return; diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index b588f98..605fb9a 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -21,6 +21,7 @@ #include "GUI.hpp" #include "GUI_App.hpp" +#include "Plater.hpp" #include "MsgDialog.hpp" #include "I18N.hpp" #include "MainFrame.hpp" @@ -103,8 +104,7 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo auto validate_path = [this](const wxString &path) -> bool { if (! path.Lower().EndsWith(m_valid_suffix.Lower())) { MessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), m_valid_suffix), wxString(SLIC3R_APP_NAME), wxYES | wxNO); - if (msg_wingow.ShowModal() == wxID_NO) - return false; + return msg_wingow.ShowModal() == wxID_YES; } return true; }; diff --git a/src/slic3r/GUI/RemovableDriveManager.cpp b/src/slic3r/GUI/RemovableDriveManager.cpp index dead18d..0d3737b 100644 --- a/src/slic3r/GUI/RemovableDriveManager.cpp +++ b/src/slic3r/GUI/RemovableDriveManager.cpp @@ -40,6 +40,7 @@ namespace GUI { wxDEFINE_EVENT(EVT_REMOVABLE_DRIVE_EJECTED, RemovableDriveEjectEvent); wxDEFINE_EVENT(EVT_REMOVABLE_DRIVES_CHANGED, RemovableDrivesChangedEvent); +wxDEFINE_EVENT(EVT_REMOVABLE_DRIVE_ADDED, wxCommandEvent); #if _WIN32 std::vector RemovableDriveManager::search_for_removable_drives() const @@ -1032,11 +1033,26 @@ void RemovableDriveManager::update() std::scoped_lock lock(m_drives_mutex); std::sort(current_drives.begin(), current_drives.end()); if (current_drives != m_current_drives) { + // event for writing / ejecting functions assert(m_callback_evt_handler); if (m_callback_evt_handler) wxPostEvent(m_callback_evt_handler, RemovableDrivesChangedEvent(EVT_REMOVABLE_DRIVES_CHANGED)); + // event for printer config file + std::vector new_drives; + std::set_difference(current_drives.begin(), current_drives.end(), m_current_drives.begin(), m_current_drives.end(), + std::inserter(new_drives, new_drives.begin())); + + for (const DriveData& data : new_drives) { + if (data.path.empty()) + continue; + wxCommandEvent* evt = new wxCommandEvent(EVT_REMOVABLE_DRIVE_ADDED); + evt->SetString(boost::nowide::widen(data.path)); + evt->SetInt((int)m_first_update); + m_callback_evt_handler->QueueEvent(evt); + } } m_current_drives = std::move(current_drives); + m_first_update = false; } else { // Acquiring the m_iniside_update lock failed, therefore another update is running. // Just block until the other instance of update() finishes. @@ -1091,4 +1107,11 @@ void RemovableDriveManager::eject_thread_finish() } #endif // __APPLE__ +std::vector RemovableDriveManager::get_drive_list() +{ + { + std::lock_guard guard(m_drives_mutex); + return m_current_drives; + } +} }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/RemovableDriveManager.hpp b/src/slic3r/GUI/RemovableDriveManager.hpp index 4ea25ea..5482f28 100644 --- a/src/slic3r/GUI/RemovableDriveManager.hpp +++ b/src/slic3r/GUI/RemovableDriveManager.hpp @@ -38,6 +38,7 @@ wxDECLARE_EVENT(EVT_REMOVABLE_DRIVE_EJECTED, RemovableDriveEjectEvent); using RemovableDrivesChangedEvent = SimpleEvent; wxDECLARE_EVENT(EVT_REMOVABLE_DRIVES_CHANGED, RemovableDrivesChangedEvent); +wxDECLARE_EVENT(EVT_REMOVABLE_DRIVE_ADDED, wxCommandEvent); #if __APPLE__ // Callbacks on device plug / unplug work reliably on OSX. #define REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS @@ -89,11 +90,14 @@ public: void volumes_changed(); #endif // _WIN32 + // returns copy of m_current_drives (protected by mutex) + std::vector get_drive_list(); private: bool m_initialized { false }; wxEvtHandler* m_callback_evt_handler { nullptr }; + bool m_first_update{ true }; #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS // Worker thread, worker thread synchronization and callbacks to the UI thread. void thread_proc(); diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index d63a46d..d2e9cba 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -483,7 +483,11 @@ SearchDialog::SearchDialog(OptionsSearcher* searcher) searcher(searcher) { SetFont(GUI::wxGetApp().normal_font()); +#if _WIN32 GUI::wxGetApp().UpdateDarkUI(this); +#elif __WXGTK__ + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif default_string = _L("Enter a search term"); int border = 10; @@ -521,9 +525,9 @@ SearchDialog::SearchDialog(OptionsSearcher* searcher) wxBoxSizer* check_sizer = new wxBoxSizer(wxHORIZONTAL); - check_category = new wxCheckBox(this, wxID_ANY, _L("Category")); + check_category = new ::CheckBox(this, _L("Category")); if (GUI::wxGetApp().is_localized()) - check_english = new wxCheckBox(this, wxID_ANY, _L("Search in English")); + check_english = new ::CheckBox(this, _L("Search in English")); wxStdDialogButtonSizer* cancel_btn = this->CreateStdDialogButtonSizer(wxCANCEL); GUI::wxGetApp().UpdateDarkUI(static_cast(this->FindWindowById(wxID_CANCEL, this))); @@ -571,6 +575,11 @@ SearchDialog::SearchDialog(OptionsSearcher* searcher) topSizer->SetSizeHints(this); } +SearchDialog::~SearchDialog() +{ + if (search_list_model) + search_list_model->DecRef(); +} void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/) { const std::string& line = searcher->search_string(); diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index e2fab87..8081ab8 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -20,6 +20,9 @@ #include "OptionsGroup.hpp" #include "libslic3r/Preset.hpp" +#include "Widgets/CheckBox.hpp" + +class CheckBox; namespace Slic3r { @@ -158,8 +161,8 @@ class SearchDialog : public GUI::DPIDialog wxTextCtrl* search_line { nullptr }; wxDataViewCtrl* search_list { nullptr }; SearchListModel* search_list_model { nullptr }; - wxCheckBox* check_category { nullptr }; - wxCheckBox* check_english { nullptr }; + CheckBox* check_category { nullptr }; + CheckBox* check_english { nullptr }; OptionsSearcher* searcher { nullptr }; @@ -178,7 +181,7 @@ class SearchDialog : public GUI::DPIDialog public: SearchDialog(OptionsSearcher* searcher); - ~SearchDialog() {} + ~SearchDialog(); void Popup(wxPoint position = wxDefaultPosition); void ProcessSelection(wxDataViewItem selection); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index f1cc437..ff22763 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -25,6 +25,10 @@ #include #include +#include +#include +#include + //B18 static const Slic3r::ColorRGBA UNIFORM_SCALE_COLOR = Slic3r::ColorRGBA::WHITE(); static const Slic3r::ColorRGBA SOLID_PLANE_COLOR = Slic3r::ColorRGBA::WHITE(); @@ -903,6 +907,40 @@ BoundingBoxf Selection::get_screen_space_bounding_box() return ss_box; } +const std::pair Selection::get_bounding_sphere() const +{ + if (!m_bounding_sphere.has_value()) { + std::optional>* sphere = const_cast>*>(&m_bounding_sphere); + *sphere = { Vec3d::Zero(), 0.0 }; + + using K = CGAL::Simple_cartesian; + using Traits = CGAL::Min_sphere_of_points_d_traits_3; + using Min_sphere = CGAL::Min_sphere_of_spheres_d; + using Point = K::Point_3; + + std::vector points; + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + const TriangleMesh* hull = volume.convex_hull(); + const indexed_triangle_set& its = (hull != nullptr) ? + hull->its : m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh().its; + const Transform3d& matrix = volume.world_matrix(); + for (const Vec3f& v : its.vertices) { + const Vec3d vv = matrix * v.cast(); + points.push_back(Point(vv.x(), vv.y(), vv.z())); + } + } + + Min_sphere ms(points.begin(), points.end()); + const float* center_x = ms.center_cartesian_begin(); + (*sphere)->first = { *center_x, *(center_x + 1), *(center_x + 2) }; + (*sphere)->second = ms.radius(); + } + } + + return *m_bounding_sphere; +} void Selection::setup_cache() { if (!m_valid) @@ -968,6 +1006,7 @@ void Selection::rotate(const Vec3d& rotation, TransformationType transformation_ assert(transformation_type.relative() || (transformation_type.absolute() && transformation_type.local())); + bool requires_general_synchronization = false; for (unsigned int i : m_list) { Transform3d rotation_matrix = Geometry::rotation_transform(rotation); GLVolume& v = *(*m_volumes)[i]; @@ -990,15 +1029,28 @@ void Selection::rotate(const Vec3d& rotation, TransformationType transformation_ rotation_matrix = inst_matrix_no_offset.inverse() * inst_rotation_matrix * rotation_matrix * inst_rotation_matrix.inverse() * inst_matrix_no_offset; // rotate around selection center - const Vec3d inst_pivot = inst_trafo.get_matrix_no_offset().inverse() * (m_cache.dragging_center - inst_trafo.get_offset()); + const Vec3d inst_pivot = inst_trafo.get_matrix_no_offset().inverse() * (m_cache.rotation_pivot - inst_trafo.get_offset()); rotation_matrix = Geometry::translation_transform(inst_pivot) * rotation_matrix * Geometry::translation_transform(-inst_pivot); + // Detects if the rotation is equivalent to a world rotation around the Z axis + // If not, force for a full synchronization of unselected instances + if (!requires_general_synchronization) { + const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); + const Transform3d old_world_rotation_matrix = (inst_trafo * vol_trafo).get_rotation_matrix(); + const Transform3d new_world_rotation_matrix = (inst_trafo * Geometry::Transformation(rotation_matrix) * vol_trafo).get_rotation_matrix(); + if (std::abs((old_world_rotation_matrix * Vec3d::UnitX()).z() - (new_world_rotation_matrix * Vec3d::UnitX()).z()) > EPSILON) + requires_general_synchronization = true; + else if (std::abs((old_world_rotation_matrix * Vec3d::UnitY()).z() - (new_world_rotation_matrix * Vec3d::UnitY()).z()) > EPSILON) + requires_general_synchronization = true; + else if (std::abs((old_world_rotation_matrix * Vec3d::UnitZ()).z() - (new_world_rotation_matrix * Vec3d::UnitZ()).z()) > EPSILON) + requires_general_synchronization = true; + } } - transform_instance_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.dragging_center); + transform_instance_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.rotation_pivot); } else { if (!is_single_volume_or_modifier()) { assert(transformation_type.world()); - transform_volume_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.dragging_center); + transform_volume_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.rotation_pivot); } else { if (transformation_type.instance()) { @@ -1024,7 +1076,7 @@ void Selection::rotate(const Vec3d& rotation, TransformationType transformation_ vol_rotation_matrix.inverse() * inst_scale_matrix * vol_matrix_no_offset; } } - transform_volume_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.dragging_center); + transform_volume_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.rotation_pivot); } } } @@ -1033,7 +1085,11 @@ void Selection::rotate(const Vec3d& rotation, TransformationType transformation_ if (m_mode == Instance) { int rot_axis_max = 0; rotation.cwiseAbs().maxCoeff(&rot_axis_max); - synchronize_unselected_instances((rot_axis_max == 2) ? SyncRotationType::NONE : SyncRotationType::GENERAL); + const SyncRotationType type = (transformation_type.instance() && requires_general_synchronization) || + (!transformation_type.instance() && rot_axis_max != 2) || + rotation.isApprox(Vec3d::Zero()) ? + SyncRotationType::GENERAL : SyncRotationType::NONE; + synchronize_unselected_instances(type); } else if (m_mode == Volume) synchronize_unselected_volumes(); @@ -1547,10 +1603,7 @@ void Selection::erase() ensure_not_below_bed(); } - GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); - canvas->set_sequential_clearance_as_evaluating(); - canvas->set_as_dirty(); - canvas->request_extra_frame(); + wxGetApp().plater()->canvas3D()->set_sequential_clearance_as_evaluating(); } void Selection::render(float scale_factor) @@ -2036,6 +2089,7 @@ void Selection::set_caches() m_cache.sinking_volumes.push_back(i); } m_cache.dragging_center = get_bounding_box().center(); + m_cache.rotation_pivot = get_bounding_sphere().first; } void Selection::do_add_volume(unsigned int volume_idx) @@ -2989,5 +3043,29 @@ const GLVolume *get_selected_gl_volume(const Selection &selection) return selection.get_volume(volume_idx); } +ModelVolume *get_selected_volume(const ObjectID &volume_id, const Selection &selection) { + const Selection::IndicesList &volume_ids = selection.get_volume_idxs(); + const ModelObjectPtrs &model_objects = selection.get_model()->objects; + for (auto id : volume_ids) { + const GLVolume *selected_volume = selection.get_volume(id); + const GLVolume::CompositeID &cid = selected_volume->composite_id; + ModelObject *obj = model_objects[cid.object_id]; + ModelVolume *volume = obj->volumes[cid.volume_id]; + if (volume_id == volume->id()) + return volume; + } + return nullptr; +} + +ModelVolume *get_volume(const ObjectID &volume_id, const Selection &selection) { + const ModelObjectPtrs &objects = selection.get_model()->objects; + for (const ModelObject *object : objects) { + for (ModelVolume *volume : object->volumes) { + if (volume->id() == volume_id) + return volume; + } + } + return nullptr; +} } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index 1e7bf11..606de7d 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -14,6 +14,7 @@ class Shader; class Model; class ModelObject; class ModelVolume; +class ObjectID; class GLVolume; class GLArrow; class GLCurvedArrow; @@ -110,6 +111,7 @@ private: ObjectIdxsToInstanceIdxsMap content; // List of ids of the volumes which are sinking when starting dragging std::vector sinking_volumes; + Vec3d rotation_pivot; }; // Volumes owned by GLCanvas3D. @@ -146,6 +148,7 @@ private: // and transform to place and orient it in world coordinates std::optional> m_bounding_box_in_current_reference_system; + std::optional> m_bounding_sphere; #if ENABLE_RENDER_SELECTION_CENTER GLModel m_vbo_sphere; #endif // ENABLE_RENDER_SELECTION_CENTER @@ -291,6 +294,8 @@ public: // Returns the screen space bounding box BoundingBoxf get_screen_space_bounding_box(); + // Returns the bounding sphere: first = center, second = radius + const std::pair get_bounding_sphere() const; void setup_cache(); void translate(const Vec3d& displacement, TransformationType transformation_type); @@ -356,6 +361,7 @@ private: m_full_unscaled_instance_bounding_box.reset(); m_full_scaled_instance_bounding_box.reset(); m_full_unscaled_instance_local_bounding_box.reset(); m_bounding_box_in_current_reference_system.reset(); + m_bounding_sphere.reset(); } void render_synchronized_volumes(); void render_bounding_box(const BoundingBoxf3& box, const Transform3d& trafo, const ColorRGB& color); @@ -395,6 +401,8 @@ private: ModelVolume *get_selected_volume(const Selection &selection); const GLVolume *get_selected_gl_volume(const Selection &selection); +ModelVolume *get_selected_volume (const ObjectID &volume_id, const Selection &selection); +ModelVolume *get_volume (const ObjectID &volume_id, const Selection &selection); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/SurfaceDrag.cpp b/src/slic3r/GUI/SurfaceDrag.cpp index c8ce171..c6adb28 100644 --- a/src/slic3r/GUI/SurfaceDrag.cpp +++ b/src/slic3r/GUI/SurfaceDrag.cpp @@ -1,12 +1,70 @@ #include "SurfaceDrag.hpp" -#include "libslic3r/Model.hpp" // ModelVolume -#include "GLCanvas3D.hpp" +#include // ModelVolume +#include + #include "slic3r/Utils/RaycastManager.hpp" -#include "slic3r/GUI/Camera.hpp" -#include "slic3r/GUI/CameraUtils.hpp" -#include "slic3r/GUI/I18N.hpp" -#include "libslic3r/Emboss.hpp" +#include "GLCanvas3D.hpp" +#include "Camera.hpp" +#include "CameraUtils.hpp" +#include "I18N.hpp" +#include "GUI_App.hpp" +#include "GUI_ObjectManipulation.hpp" + + +using namespace Slic3r; +using namespace Slic3r::GUI; + +namespace{ +// Distance of embossed volume from surface to be represented as distance surface +// Maximal distance is also enlarge by size of emboss depth +constexpr Slic3r::MinMax surface_distance_sq{1e-4, 10.}; // [in mm] + +/// +/// Extract position of mouse from mouse event +/// +/// Event +/// Position +Vec2d mouse_position(const wxMouseEvent &mouse_event); + +bool start_dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional &up_limit); + +bool dragging(const Vec2d &mouse_pos, + const Camera &camera, + SurfaceDrag &surface_drag, // need to write whether exist hit + GLCanvas3D &canvas, + const RaycastManager &raycast_manager, + const std::optional &up_limit); + +Transform3d get_volume_transformation( + Transform3d world, // from volume + const Vec3d& world_dir, // wanted new direction + const Vec3d& world_position, // wanted new position + const std::optional& fix, // [optional] fix matrix + // Invers transformation of text volume instance + // Help convert world transformation to instance space + const Transform3d& instance_inv, + // initial rotation in Z axis + std::optional current_angle = {}, + const std::optional &up_limit = {}); + +// distinguish between transformation of volume inside object +// and object(single full instance with one volume) +bool is_embossed_object(const Selection &selection); + +/// +/// Get fix transformation for selected volume +/// Fix after store to 3mf +/// +/// Select only wanted volume +/// Pointer on fix transformation from ModelVolume when exists otherwise nullptr +const Transform3d *get_fix_transformation(const Selection &selection); +} namespace Slic3r::GUI { @@ -27,13 +85,14 @@ bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, std::optional &surface_drag, GLCanvas3D &canvas, RaycastManager &raycast_manager, - std::optional up_limit) + const std::optional&up_limit) { // Fix when leave window during dragging // Fix when click right button if (surface_drag.has_value() && !mouse_event.Dragging()) { // write transformation from UI into model canvas.do_move(L("Move over surface")); + wxGetApp().obj_manipul()->set_dirty(); // allow moving with object again canvas.enable_moving(true); @@ -48,189 +107,16 @@ bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, if (mouse_event.Moving()) return false; - // detect start text dragging - if (mouse_event.LeftDown()) { - // selected volume - GLVolume *gl_volume = get_selected_gl_volume(canvas); - if (gl_volume == nullptr) - return false; - - // is selected volume closest hovered? - const GLVolumePtrs &gl_volumes = canvas.get_volumes().volumes; - if (int hovered_idx = canvas.get_first_hover_volume_idx(); - hovered_idx < 0) - return false; - else if (auto hovered_idx_ = static_cast(hovered_idx); - hovered_idx_ >= gl_volumes.size() || - gl_volumes[hovered_idx_] != gl_volume) - return false; - - const ModelObjectPtrs &objects = canvas.get_model()->objects; - const ModelObject *object = get_model_object(*gl_volume, objects); - assert(object != nullptr); - if (object == nullptr) - return false; - - const ModelInstance *instance = get_model_instance(*gl_volume, *object); - const ModelVolume *volume = get_model_volume(*gl_volume, *object); - assert(instance != nullptr && volume != nullptr); - if (object == nullptr || instance == nullptr || volume == nullptr) - return false; - - // allowed drag&drop by canvas for object - if (volume->is_the_only_one_part()) - return false; - - const ModelVolumePtrs &volumes = object->volumes; - std::vector allowed_volumes_id; - if (volumes.size() > 1) { - allowed_volumes_id.reserve(volumes.size() - 1); - for (auto &v : volumes) { - // skip actual selected object - if (v->id() == volume->id()) - continue; - // drag only above part not modifiers or negative surface - if (!v->is_model_part()) - continue; - allowed_volumes_id.emplace_back(v->id().id); - } - } - RaycastManager::AllowVolumes condition(std::move(allowed_volumes_id)); - RaycastManager::Meshes meshes = create_meshes(canvas, condition); - // initialize raycasters - // INFO: It could slows down for big objects - // (may be move to thread and do not show drag until it finish) - raycast_manager.actualize(*instance, &condition, &meshes); - - // wxCoord == int --> wx/types.h - Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); - Vec2d mouse_pos = mouse_coord.cast(); - - // world_matrix_fixed() without sla shift - Transform3d to_world = world_matrix_fixed(*gl_volume, objects); - - // zero point of volume in world coordinate system - Vec3d volume_center = to_world.translation(); - // screen coordinate of volume center - Vec2i coor = CameraUtils::project(camera, volume_center); - Vec2d mouse_offset = coor.cast() - mouse_pos; - Vec2d mouse_offset_without_sla_shift = mouse_offset; - if (double sla_shift = gl_volume->get_sla_shift_z(); !is_approx(sla_shift, 0.)) { - Transform3d to_world_without_sla_move = instance->get_matrix() * volume->get_matrix(); - if (volume->text_configuration.has_value() && volume->text_configuration->fix_3mf_tr.has_value()) - to_world_without_sla_move = to_world_without_sla_move * (*volume->text_configuration->fix_3mf_tr); - // zero point of volume in world coordinate system - volume_center = to_world_without_sla_move.translation(); - // screen coordinate of volume center - coor = CameraUtils::project(camera, volume_center); - mouse_offset_without_sla_shift = coor.cast() - mouse_pos; - } - - Transform3d volume_tr = gl_volume->get_volume_transformation().get_matrix(); - - if (volume->text_configuration.has_value()) { - const TextConfiguration &tc = *volume->text_configuration; - // fix baked transformation from .3mf store process - if (tc.fix_3mf_tr.has_value()) - volume_tr = volume_tr * tc.fix_3mf_tr->inverse(); - } - - Transform3d instance_tr = instance->get_matrix(); - Transform3d instance_tr_inv = instance_tr.inverse(); - Transform3d world_tr = instance_tr * volume_tr; - std::optional start_angle; - if (up_limit.has_value()) - start_angle = Emboss::calc_up(world_tr, *up_limit); - surface_drag = SurfaceDrag{mouse_offset, world_tr, instance_tr_inv, gl_volume, condition, start_angle, true, mouse_offset_without_sla_shift}; - - // disable moving with object by mouse - canvas.enable_moving(false); - canvas.enable_picking(false); - return true; - } + if (mouse_event.LeftDown()) + return start_dragging(mouse_position(mouse_event), camera, surface_drag, canvas, raycast_manager, up_limit); // Dragging starts out of window if (!surface_drag.has_value()) return false; - if (mouse_event.Dragging()) { - // wxCoord == int --> wx/types.h - Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); - Vec2d mouse_pos = mouse_coord.cast(); - Vec2d offseted_mouse = mouse_pos + surface_drag->mouse_offset_without_sla_shift; + if (mouse_event.Dragging()) + return dragging(mouse_position(mouse_event), camera, *surface_drag, canvas, raycast_manager, up_limit); - std::optional hit = ray_from_camera( - raycast_manager, offseted_mouse, camera, &surface_drag->condition); - - surface_drag->exist_hit = hit.has_value(); - if (!hit.has_value()) { - // cross hair need redraw - canvas.set_as_dirty(); - return true; - } - - auto world_linear = surface_drag->world.linear(); - // Calculate offset: transformation to wanted position - { - // Reset skew of the text Z axis: - // Project the old Z axis into a new Z axis, which is perpendicular to the old XY plane. - Vec3d old_z = world_linear.col(2); - Vec3d new_z = world_linear.col(0).cross(world_linear.col(1)); - world_linear.col(2) = new_z * (old_z.dot(new_z) / new_z.squaredNorm()); - } - - Vec3d text_z_world = world_linear.col(2); // world_linear * Vec3d::UnitZ() - auto z_rotation = Eigen::Quaternion::FromTwoVectors(text_z_world, hit->normal); - Transform3d world_new = z_rotation * surface_drag->world; - auto world_new_linear = world_new.linear(); - - // Fix direction of up vector to zero initial rotation - if(up_limit.has_value()){ - Vec3d z_world = world_new_linear.col(2); - z_world.normalize(); - Vec3d wanted_up = Emboss::suggest_up(z_world, *up_limit); - - Vec3d y_world = world_new_linear.col(1); - auto y_rotation = Eigen::Quaternion::FromTwoVectors(y_world, wanted_up); - - world_new = y_rotation * world_new; - world_new_linear = world_new.linear(); - } - - // Edit position from right - Transform3d volume_new{Eigen::Translation(surface_drag->instance_inv * hit->position)}; - volume_new.linear() = surface_drag->instance_inv.linear() * world_new_linear; - - // Check that transformation matrix is valid transformation - assert(volume_new.matrix()(0, 0) == volume_new.matrix()(0, 0)); // Check valid transformation not a NAN - if (volume_new.matrix()(0, 0) != volume_new.matrix()(0, 0)) - return true; - - // Check that scale in world did not changed - assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitY()).has_value()); - assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitZ()).has_value()); - - const ModelVolume *volume = get_model_volume(*surface_drag->gl_volume, canvas.get_model()->objects); - if (volume != nullptr && volume->text_configuration.has_value()) { - const TextConfiguration &tc = *volume->text_configuration; - // fix baked transformation from .3mf store process - if (tc.fix_3mf_tr.has_value()) - volume_new = volume_new * (*tc.fix_3mf_tr); - - // apply move in Z direction and rotation by up vector - Emboss::apply_transformation(surface_drag->start_angle, tc.style.prop.distance, volume_new); - } - - // Update transformation for all instances - for (GLVolume *vol : canvas.get_volumes().volumes) { - if (vol->object_idx() != surface_drag->gl_volume->object_idx() || vol->volume_idx() != surface_drag->gl_volume->volume_idx()) - continue; - vol->set_volume_transformation(volume_new); - } - - canvas.set_as_dirty(); - return true; - } return false; } @@ -253,12 +139,11 @@ std::optional calc_surface_offset(const Selection &selection, RaycastMana auto cond = RaycastManager::SkipVolume(volume->id().id); raycast_manager.actualize(*instance, &cond); - Transform3d to_world = world_matrix_fixed(gl_volume, selection.get_model()->objects); - Vec3d point = to_world * Vec3d::Zero(); - Vec3d direction = to_world.linear() * (-Vec3d::UnitZ()); - + Transform3d to_world = world_matrix_fixed(gl_volume, objects); + Vec3d point = to_world.translation(); + Vec3d dir = -get_z_base(to_world); // ray in direction of text projection(from volume zero to z-dir) - std::optional hit_opt = raycast_manager.closest_hit(point, direction, &cond); + std::optional hit_opt = raycast_manager.closest_hit(point, dir, &cond); // Try to find closest point when no hit object in emboss direction if (!hit_opt.has_value()) { @@ -293,6 +178,81 @@ std::optional calc_surface_offset(const Selection &selection, RaycastMana return offset_volume; } +std::optional calc_distance(const GLVolume &gl_volume, RaycastManager &raycaster, GLCanvas3D &canvas) +{ + const ModelObject *object = get_model_object(gl_volume, canvas.get_model()->objects); + assert(object != nullptr); + if (object == nullptr) + return {}; + + const ModelInstance *instance = get_model_instance(gl_volume, *object); + const ModelVolume *volume = get_model_volume(gl_volume, *object); + assert(instance != nullptr && volume != nullptr); + if (object == nullptr || instance == nullptr || volume == nullptr) + return {}; + + if (volume->is_the_only_one_part()) + return {}; + + RaycastManager::AllowVolumes condition = create_condition(object->volumes, volume->id()); + RaycastManager::Meshes meshes = create_meshes(canvas, condition); + raycaster.actualize(*instance, &condition, &meshes); + return calc_distance(gl_volume, raycaster, &condition); +} + +std::optional calc_distance(const GLVolume &gl_volume, const RaycastManager &raycaster, const RaycastManager::ISkip *condition) +{ + Transform3d w = gl_volume.world_matrix(); + Vec3d p = w.translation(); + Vec3d dir = -get_z_base(w); + auto hit_opt = raycaster.closest_hit(p, dir, condition); + if (!hit_opt.has_value()) + return {}; + + const RaycastManager::Hit &hit = *hit_opt; + // NOTE: hit.squared_distance is in volume space not world + + const Transform3d &tr = raycaster.get_transformation(hit.tr_key); + Vec3d hit_world = tr * hit.position; + Vec3d p_to_hit = hit_world - p; + double distance_sq = p_to_hit.squaredNorm(); + + // too small distance is calculated as zero distance + if (distance_sq < ::surface_distance_sq.min) + return {}; + + // check maximal distance + const BoundingBoxf3& bb = gl_volume.bounding_box(); + double max_squared_distance = std::max(std::pow(2 * bb.size().z(), 2), ::surface_distance_sq.max); + if (distance_sq > max_squared_distance) + return {}; + + // calculate sign + float sign = (p_to_hit.dot(dir) > 0)? 1.f : -1.f; + + // distiguish sign + return sign * static_cast(sqrt(distance_sq)); +} + +std::optional calc_angle(const Selection &selection) +{ + const GLVolume *gl_volume = selection.get_first_volume(); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return {}; + + Transform3d to_world = gl_volume->world_matrix(); + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume != nullptr); + assert(volume->emboss_shape.has_value()); + if (volume == nullptr || !volume->emboss_shape.has_value() || !volume->emboss_shape->fix_3mf_tr) + return Emboss::calc_up(to_world, UP_LIMIT); + + // exist fix matrix and must be applied before calculation + to_world = to_world * volume->emboss_shape->fix_3mf_tr->inverse(); + return Emboss::calc_up(to_world, UP_LIMIT); +} + Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs &objects) { Transform3d res = gl_volume.world_matrix(); @@ -301,11 +261,11 @@ Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs if (!mv) return res; - const std::optional &tc = mv->text_configuration; - if (!tc.has_value()) + const std::optional &es = mv->emboss_shape; + if (!es.has_value()) return res; - const std::optional &fix = tc->fix_3mf_tr; + const std::optional &fix = es->fix_3mf_tr; if (!fix.has_value()) return res; @@ -322,4 +282,447 @@ Transform3d world_matrix_fixed(const Selection &selection) return world_matrix_fixed(*gl_volume, selection.get_model()->objects); } -} // namespace Slic3r::GUI \ No newline at end of file +void selection_transform(Selection &selection, const std::function &selection_transformation_fnc) +{ + if (const Transform3d *fix = get_fix_transformation(selection); fix != nullptr) { + // NOTE: need editable gl volume .. can't use selection.get_first_volume() + GLVolume *gl_volume = selection.get_volume(*selection.get_volume_idxs().begin()); + Transform3d volume_tr = gl_volume->get_volume_transformation().get_matrix(); + gl_volume->set_volume_transformation(volume_tr * fix->inverse()); + selection.setup_cache(); + + selection_transformation_fnc(); + + volume_tr = gl_volume->get_volume_transformation().get_matrix(); + gl_volume->set_volume_transformation(volume_tr * (*fix)); + selection.setup_cache(); + } else { + selection_transformation_fnc(); + } + + if (selection.is_single_full_instance()) + selection.synchronize_unselected_instances(Selection::SyncRotationType::GENERAL); +} + +bool face_selected_volume_to_camera(const Camera &camera, GLCanvas3D &canvas, const std::optional &wanted_up_limit) +{ + GLVolume *gl_volume_ptr = get_selected_gl_volume(canvas); + if (gl_volume_ptr == nullptr) + return false; + GLVolume &gl_volume = *gl_volume_ptr; + + const ModelObjectPtrs &objects = canvas.get_model()->objects; + ModelObject *object_ptr = get_model_object(gl_volume, objects); + assert(object_ptr != nullptr); + if (object_ptr == nullptr) + return false; + ModelObject &object = *object_ptr; + + ModelInstance *instance_ptr = get_model_instance(gl_volume, object); + assert(instance_ptr != nullptr); + if (instance_ptr == nullptr) + return false; + ModelInstance &instance = *instance_ptr; + + ModelVolume *volume_ptr = get_model_volume(gl_volume, object); + assert(volume_ptr != nullptr); + if (volume_ptr == nullptr) + return false; + ModelVolume &volume = *volume_ptr; + + // Calculate new volume transformation + Transform3d volume_tr = volume.get_matrix(); + std::optional fix; + if (volume.emboss_shape.has_value()) { + fix = volume.emboss_shape->fix_3mf_tr; + if (fix.has_value()) + volume_tr = volume_tr * fix->inverse(); + } + + Transform3d instance_tr = instance.get_matrix(); + Transform3d instance_tr_inv = instance_tr.inverse(); + Transform3d world_tr = instance_tr * volume_tr; // without sla !!! + std::optional current_angle; + if (wanted_up_limit.has_value()) + current_angle = Emboss::calc_up(world_tr, *wanted_up_limit); + + Vec3d world_position = gl_volume.world_matrix()*Vec3d::Zero(); + + assert(camera.get_type() == Camera::EType::Perspective || + camera.get_type() == Camera::EType::Ortho); + Vec3d wanted_direction = (camera.get_type() == Camera::EType::Perspective) ? + Vec3d(camera.get_position() - world_position) : + (-camera.get_dir_forward()); + + Transform3d new_volume_tr = get_volume_transformation(world_tr, wanted_direction, world_position, + fix, instance_tr_inv, current_angle, wanted_up_limit); + + Selection &selection = canvas.get_selection(); + if (is_embossed_object(selection)) { + // transform instance instead of volume + Transform3d new_instance_tr = instance_tr * new_volume_tr * volume.get_matrix().inverse(); + gl_volume.set_instance_transformation(new_instance_tr); + + // set same transformation to other instances when instance is embossed object + if (selection.is_single_full_instance()) + selection.synchronize_unselected_instances(Selection::SyncRotationType::GENERAL); + } else { + // write result transformation + gl_volume.set_volume_transformation(new_volume_tr); + } + + if (volume.type() == ModelVolumeType::MODEL_PART) { + object.invalidate_bounding_box(); + object.ensure_on_bed(); + } + + canvas.do_rotate(L("Face the camera")); + wxGetApp().obj_manipul()->set_dirty(); + return true; +} + +void do_local_z_rotate(GLCanvas3D &canvas, double relative_angle) +{ + Selection &selection = canvas.get_selection(); + + assert(!selection.is_empty()); + if(selection.is_empty()) return; + + bool is_single_volume = selection.volumes_count() == 1; + assert(is_single_volume); + if (!is_single_volume) return; + + // Fix angle for mirrored volume + bool is_mirrored = false; + const GLVolume* gl_volume = selection.get_first_volume(); + if (gl_volume != nullptr) { + const ModelInstance *instance = get_model_instance(*gl_volume, selection.get_model()->objects); + bool is_instance_mirrored = (instance != nullptr)? has_reflection(instance->get_matrix()) : false; + if (is_embossed_object(selection)) { + is_mirrored = is_instance_mirrored; + } else { + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume != nullptr) + is_mirrored = is_instance_mirrored != has_reflection(volume->get_matrix()); + } + } + if (is_mirrored) + relative_angle *= -1; + + selection.setup_cache(); + auto selection_rotate_fnc = [&selection, &relative_angle](){ + selection.rotate(Vec3d(0., 0., relative_angle), get_drag_transformation_type(selection)); + }; + selection_transform(selection, selection_rotate_fnc); + + std::string snapshot_name; // empty meand no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended + // inside function do_move + // snapshot_name = L("Set text rotation"); + canvas.do_rotate(snapshot_name); +} + +void do_local_z_move(GLCanvas3D &canvas, double relative_move) { + + Selection &selection = canvas.get_selection(); + assert(!selection.is_empty()); + if (selection.is_empty()) return; + + selection.setup_cache(); + auto selection_translate_fnc = [&selection, relative_move]() { + Vec3d translate = Vec3d::UnitZ() * relative_move; + selection.translate(translate, TransformationType::Local); + }; + selection_transform(selection, selection_translate_fnc); + + std::string snapshot_name; // empty mean no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended inside + // function do_move + // snapshot_name = L("Set surface distance"); + canvas.do_move(snapshot_name); +} + +TransformationType get_drag_transformation_type(const Selection &selection) +{ + return is_embossed_object(selection) ? + TransformationType::Instance_Relative_Joint : + TransformationType::Local_Relative_Joint; +} + +void dragging_rotate_gizmo(double gizmo_angle, std::optional& current_angle, std::optional &start_angle, Selection &selection) +{ + if (!start_angle.has_value()) + // create cache for initial angle + start_angle = current_angle.value_or(0.f); + + gizmo_angle -= PI / 2; // Grabber is upward + + double new_angle = gizmo_angle + *start_angle; + + const GLVolume *gl_volume = selection.get_first_volume(); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return; + + bool is_volume_mirrored = has_reflection(gl_volume->get_volume_transformation().get_matrix()); + bool is_instance_mirrored = has_reflection(gl_volume->get_instance_transformation().get_matrix()); + if (is_volume_mirrored != is_instance_mirrored) + new_angle = -gizmo_angle + *start_angle; + + // move to range <-M_PI, M_PI> + Geometry::to_range_pi_pi(new_angle); + + const Transform3d* fix = get_fix_transformation(selection); + double z_rotation = (fix!=nullptr) ? (new_angle - current_angle.value_or(0.f)) : // relative angle + gizmo_angle; // relativity is keep by selection cache + + auto selection_rotate_fnc = [z_rotation, &selection]() { + selection.rotate(Vec3d(0., 0., z_rotation), get_drag_transformation_type(selection)); + }; + selection_transform(selection, selection_rotate_fnc); + + // propagate angle into property + current_angle = static_cast(new_angle); + + // do not store zero + if (is_approx(*current_angle, 0.f)) + current_angle.reset(); +} + +} // namespace Slic3r::GUI + +// private implementation +namespace { + +Vec2d mouse_position(const wxMouseEvent &mouse_event){ + // wxCoord == int --> wx/types.h + Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + return mouse_coord.cast(); +} + +bool start_dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional&up_limit) +{ + // selected volume + GLVolume *gl_volume_ptr = get_selected_gl_volume(canvas); + if (gl_volume_ptr == nullptr) + return false; + const GLVolume &gl_volume = *gl_volume_ptr; + + // is selected volume closest hovered? + const GLVolumePtrs &gl_volumes = canvas.get_volumes().volumes; + if (int hovered_idx = canvas.get_first_hover_volume_idx(); hovered_idx < 0) + return false; + else if (auto hovered_idx_ = static_cast(hovered_idx); + hovered_idx_ >= gl_volumes.size() || gl_volumes[hovered_idx_] != gl_volume_ptr) + return false; + + const ModelObjectPtrs &objects = canvas.get_model()->objects; + const ModelObject *object = get_model_object(gl_volume, objects); + assert(object != nullptr); + if (object == nullptr) + return false; + + const ModelInstance *instance = get_model_instance(gl_volume, *object); + const ModelVolume *volume = get_model_volume(gl_volume, *object); + assert(instance != nullptr && volume != nullptr); + if (object == nullptr || instance == nullptr || volume == nullptr) + return false; + + // allowed drag&drop by canvas for object + if (volume->is_the_only_one_part()) + return false; + + RaycastManager::AllowVolumes condition = create_condition(object->volumes, volume->id()); + RaycastManager::Meshes meshes = create_meshes(canvas, condition); + // initialize raycasters + // INFO: It could slows down for big objects + // (may be move to thread and do not show drag until it finish) + raycast_manager.actualize(*instance, &condition, &meshes); + + + // world_matrix_fixed() without sla shift + Transform3d to_world = world_matrix_fixed(gl_volume, objects); + + // zero point of volume in world coordinate system + Vec3d volume_center = to_world.translation(); + // screen coordinate of volume center + Vec2i coor = CameraUtils::project(camera, volume_center); + Vec2d mouse_offset = coor.cast() - mouse_pos; + Vec2d mouse_offset_without_sla_shift = mouse_offset; + if (double sla_shift = gl_volume.get_sla_shift_z(); !is_approx(sla_shift, 0.)) { + Transform3d to_world_without_sla_move = instance->get_matrix() * volume->get_matrix(); + if (volume->emboss_shape.has_value() && volume->emboss_shape->fix_3mf_tr.has_value()) + to_world_without_sla_move = to_world_without_sla_move * (*volume->emboss_shape->fix_3mf_tr); + // zero point of volume in world coordinate system + volume_center = to_world_without_sla_move.translation(); + // screen coordinate of volume center + coor = CameraUtils::project(camera, volume_center); + mouse_offset_without_sla_shift = coor.cast() - mouse_pos; + } + + Transform3d volume_tr = gl_volume.get_volume_transformation().get_matrix(); + + // fix baked transformation from .3mf store process + if (const std::optional &es_opt = volume->emboss_shape; es_opt.has_value()) { + const std::optional &fix = es_opt->fix_3mf_tr; + if (fix.has_value()) + volume_tr = volume_tr * fix->inverse(); + } + + Transform3d instance_tr = instance->get_matrix(); + Transform3d instance_tr_inv = instance_tr.inverse(); + Transform3d world_tr = instance_tr * volume_tr; + std::optional start_angle; + if (up_limit.has_value()) { + start_angle = Emboss::calc_up(world_tr, *up_limit); + if (start_angle.has_value() && has_reflection(world_tr)) + start_angle = -(*start_angle); + } + + std::optional start_distance; + if (!volume->emboss_shape->projection.use_surface) + start_distance = calc_distance(gl_volume, raycast_manager, &condition); + surface_drag = SurfaceDrag{mouse_offset, world_tr, instance_tr_inv, + gl_volume_ptr, condition, start_angle, + start_distance, true, mouse_offset_without_sla_shift}; + + // disable moving with object by mouse + canvas.enable_moving(false); + canvas.enable_picking(false); + return true; + } + +Transform3d get_volume_transformation( + Transform3d world, // from volume + const Vec3d& world_dir, // wanted new direction + const Vec3d& world_position, // wanted new position + const std::optional& fix, // [optional] fix matrix + // Invers transformation of text volume instance + // Help convert world transformation to instance space + const Transform3d& instance_inv, + // initial rotation in Z axis + std::optional current_angle, + const std::optional &up_limit) +{ + auto world_linear = world.linear(); + // Calculate offset: transformation to wanted position + { + // Reset skew of the text Z axis: + // Project the old Z axis into a new Z axis, which is perpendicular to the old XY plane. + Vec3d old_z = world_linear.col(2); + Vec3d new_z = world_linear.col(0).cross(world_linear.col(1)); + world_linear.col(2) = new_z * (old_z.dot(new_z) / new_z.squaredNorm()); + } + + Vec3d text_z_world = world_linear.col(2); // world_linear * Vec3d::UnitZ() + auto z_rotation = Eigen::Quaternion::FromTwoVectors(text_z_world, world_dir); + Transform3d world_new = z_rotation * world; + auto world_new_linear = world_new.linear(); + + // Fix direction of up vector to zero initial rotation + if(up_limit.has_value()){ + Vec3d z_world = world_new_linear.col(2); + z_world.normalize(); + Vec3d wanted_up = Emboss::suggest_up(z_world, *up_limit); + + Vec3d y_world = world_new_linear.col(1); + auto y_rotation = Eigen::Quaternion::FromTwoVectors(y_world, wanted_up); + + world_new = y_rotation * world_new; + world_new_linear = world_new.linear(); + } + + // Edit position from right + Transform3d volume_new{Eigen::Translation(instance_inv * world_position)}; + volume_new.linear() = instance_inv.linear() * world_new_linear; + + // Check that transformation matrix is valid transformation + assert(volume_new.matrix()(0, 0) == volume_new.matrix()(0, 0)); // Check valid transformation not a NAN + if (volume_new.matrix()(0, 0) != volume_new.matrix()(0, 0)) + return Transform3d::Identity(); + + // Check that scale in world did not changed + assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitY()).has_value()); + assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitZ()).has_value()); + + // fix baked transformation from .3mf store process + if (fix.has_value()) + volume_new = volume_new * (*fix); + + // apply move in Z direction and rotation by up vector + Emboss::apply_transformation(current_angle, {}, volume_new); + + return volume_new; +} + +bool dragging(const Vec2d &mouse_pos, + const Camera &camera, + SurfaceDrag &surface_drag, + GLCanvas3D &canvas, + const RaycastManager &raycast_manager, + const std::optional &up_limit) +{ + Vec2d offseted_mouse = mouse_pos + surface_drag.mouse_offset_without_sla_shift; + std::optional hit = ray_from_camera( + raycast_manager, offseted_mouse, camera, &surface_drag.condition); + + surface_drag.exist_hit = hit.has_value(); + if (!hit.has_value()) { + // cross hair need redraw + canvas.set_as_dirty(); + return true; + } + + const ModelVolume *volume = get_model_volume(*surface_drag.gl_volume, canvas.get_model()->objects); + std::optional fix; + if (volume !=nullptr && + volume->emboss_shape.has_value() && + volume->emboss_shape->fix_3mf_tr.has_value()) + fix = volume->emboss_shape->fix_3mf_tr; + Transform3d volume_new = get_volume_transformation(surface_drag.world, hit->normal, hit->position, + fix, surface_drag.instance_inv, surface_drag.start_angle, up_limit); + // Update transformation for all instances + for (GLVolume *vol : canvas.get_volumes().volumes) { + if (vol->object_idx() != surface_drag.gl_volume->object_idx() || + vol->volume_idx() != surface_drag.gl_volume->volume_idx()) + continue; + vol->set_volume_transformation(volume_new); + } + + canvas.set_as_dirty(); + // Show current position in manipulation panel + wxGetApp().obj_manipul()->set_dirty(); + return true; + } + +bool is_embossed_object(const Selection &selection) +{ + assert(selection.volumes_count() == 1); + return selection.is_single_full_object() || selection.is_single_full_instance(); +} + +const Transform3d *get_fix_transformation(const Selection &selection) { + const GLVolume *gl_volume = get_selected_gl_volume(selection); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return nullptr; + + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume != nullptr); + if (volume == nullptr) + return nullptr; + + const std::optional &es = volume->emboss_shape; + if (!volume->emboss_shape.has_value()) + return nullptr; + if (!es->fix_3mf_tr.has_value()) + return nullptr; + return &(*es->fix_3mf_tr); +} + +} // namespace \ No newline at end of file diff --git a/src/slic3r/GUI/SurfaceDrag.hpp b/src/slic3r/GUI/SurfaceDrag.hpp index 48d6a33..cd87dfc 100644 --- a/src/slic3r/GUI/SurfaceDrag.hpp +++ b/src/slic3r/GUI/SurfaceDrag.hpp @@ -5,14 +5,17 @@ #include "libslic3r/Point.hpp" // Vec2d, Transform3d #include "slic3r/Utils/RaycastManager.hpp" #include "wx/event.h" // wxMouseEvent +#include namespace Slic3r { class GLVolume; +class ModelVolume; } // namespace Slic3r namespace Slic3r::GUI { class GLCanvas3D; class Selection; +class TransformationType; struct Camera; // Data for drag&drop over surface with mouse @@ -37,6 +40,8 @@ struct SurfaceDrag // initial rotation in Z axis of volume std::optional start_angle; + // initial Z distance from surface + std::optional start_distance; // Flag whether coordinate hit some volume bool exist_hit = true; @@ -44,6 +49,9 @@ struct SurfaceDrag Vec2d mouse_offset_without_sla_shift; }; +// Limit direction of up vector on model +// Between side and top surface +constexpr double UP_LIMIT = 0.9; /// /// Mouse event handler, when move(drag&drop) volume over model surface /// NOTE: Dragged volume has to be selected. And also has to be hovered on start of dragging. @@ -61,7 +69,7 @@ bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, std::optional &surface_drag, GLCanvas3D &canvas, RaycastManager &raycast_manager, - std::optional up_limit = {}); + const std::optional&up_limit = {}); /// /// Calculate translation of volume onto surface of model @@ -71,6 +79,23 @@ bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, /// Offset of volume in volume coordinate std::optional calc_surface_offset(const Selection &selection, RaycastManager &raycast_manager); +/// +/// Calculate distance by ray to surface of object in emboss direction +/// +/// Define embossed volume +/// Way to cast rays to object +/// Contain model +/// Calculated distance from surface +std::optional calc_distance(const GLVolume &gl_volume, RaycastManager &raycaster, GLCanvas3D &canvas); +std::optional calc_distance(const GLVolume &gl_volume, const RaycastManager &raycaster, const RaycastManager::ISkip *condition); + +/// +/// Calculate up vector angle +/// +/// Calculation of angle is for selected one volume +/// +std::optional calc_angle(const Selection &selection); + /// /// Get transformation to world /// - use fix after store to 3mf when exists @@ -89,5 +114,53 @@ Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs& /// Fixed Transformation of selected volume in selection Transform3d world_matrix_fixed(const Selection &selection); +/// +/// Wrap function around selection transformation to apply fix transformation +/// Fix transformation is needed because of (store/load) volume (to/from) 3mf +/// +/// Selected gl volume will be modified +/// Function modified Selection transformation +void selection_transform(Selection &selection, const std::function& selection_transformation_fnc); + +/// +/// Apply camera direction for emboss direction +/// +/// Define view vector +/// Containe Selected ModelVolume to modify orientation +/// [Optional]Limit for direction of up vector +/// True when apply change otherwise false +bool face_selected_volume_to_camera(const Camera &camera, GLCanvas3D &canvas, const std::optional &wanted_up_limit = {}); + +/// +/// Rotation around z Axis(emboss direction) +/// +/// Selected volume for rotation +/// Relative angle to rotate around emboss direction +void do_local_z_rotate(GLCanvas3D &canvas, double relative_angle); + +/// +/// Translation along local z Axis (emboss direction) +/// +/// Selected volume for translate +/// Relative move along emboss direction +void do_local_z_move(GLCanvas3D &canvas, double relative_move); + +/// +/// Distiguish between object and volume +/// Differ in possible transformation type +/// +/// Contain selected volume/object +/// Transformation to use +TransformationType get_drag_transformation_type(const Selection &selection); + +/// +/// On dragging rotate gizmo func +/// Transform GLVolume from selection +/// +/// GLGizmoRotate::get_angle() +/// In/Out current angle visible in UI +/// Cache for start dragging angle +/// Selected only Actual embossed volume +void dragging_rotate_gizmo(double gizmo_angle, std::optional& current_angle, std::optional &start_angle, Selection &selection); } // namespace Slic3r::GUI #endif // slic3r_SurfaceDrag_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/SysInfoDialog.cpp b/src/slic3r/GUI/SysInfoDialog.cpp index 6576a58..5bd4846 100644 --- a/src/slic3r/GUI/SysInfoDialog.cpp +++ b/src/slic3r/GUI/SysInfoDialog.cpp @@ -118,13 +118,13 @@ SysInfoDialog::SysInfoDialog() wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize); wxFont title_font = wxGetApp().bold_font(); title_font.SetFamily(wxFONTFAMILY_ROMAN); - title_font.SetPointSize(22); + title_font.SetPointSize(int(2.5 * title_font.GetPointSize()));//title_font.SetPointSize(22); title->SetFont(title_font); vsizer->Add(title, 0, wxEXPAND | wxALIGN_LEFT | wxTOP, wxGetApp().em_unit()/*50*/); } // main_info_text - wxFont font = get_default_font(this); + wxFont font = GetFont();// get_default_font(this); const auto text_clr = wxGetApp().get_label_clr_default(); auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); @@ -178,7 +178,9 @@ SysInfoDialog::SysInfoDialog() } wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK); + wxGetApp().SetWindowVariantForButton(buttons->GetAffirmativeButton()); m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize); + wxGetApp().SetWindowVariantForButton(m_btn_copy_to_clipboard); buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5); m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index da33713..67f99cd 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -7,7 +7,8 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" -#include "libslic3r/GCodeWriter.hpp" +#include "libslic3r/GCode/GCodeWriter.hpp" +#include "libslic3r/GCode/Thumbnails.hpp" #include "slic3r/Utils/Http.hpp" #include "slic3r/Utils/PrintHost.hpp" @@ -45,9 +46,11 @@ #include "format.hpp" #include "UnsavedChangesDialog.hpp" #include "SavePresetDialog.hpp" +#include "EditGCodeDialog.hpp" #include "MsgDialog.hpp" #include "Notebook.hpp" +#include "Widgets/CheckBox.hpp" #ifdef WIN32 #include #endif // WIN32 @@ -61,7 +64,11 @@ Tab::Tab(wxBookCtrlBase* parent, const wxString& title, Preset::Type type) : Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL/*, name*/); this->SetFont(Slic3r::GUI::wxGetApp().normal_font()); +#ifdef __WXMSW__ wxGetApp().UpdateDarkUI(this); +#elif __WXOSX__ + SetBackgroundColour(parent->GetBackgroundColour()); +#endif m_compatible_printers.type = Preset::TYPE_PRINTER; m_compatible_printers.key_list = "compatible_printers"; @@ -146,7 +153,6 @@ void Tab::create_preset_tab() } }); - auto color = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); //buttons m_scaled_buttons.reserve(6); @@ -188,6 +194,8 @@ void Tab::create_preset_tab() // Bitmaps to be shown on the "Undo user changes" button next to each input field. add_scaled_bitmap(this, m_bmp_value_revert, "undo"); add_scaled_bitmap(this, m_bmp_white_bullet, "dot"); + // Bitmap to be shown on the "edit" button before to each editable input field. + add_scaled_bitmap(this, m_bmp_edit_value, "edit"); fill_icon_descriptions(); set_tooltips_text(); @@ -217,7 +225,7 @@ void Tab::create_preset_tab() const float scale_factor = em_unit(this)*0.1;// GetContentScaleFactor(); m_top_hsizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_top_hsizer, 0, wxEXPAND | wxBOTTOM | wxALIGN_CENTER_VERTICAL, 3); + sizer->Add(m_top_hsizer, 0, wxEXPAND | wxBOTTOM, 3); m_top_hsizer->Add(m_presets_choice, 0, wxLEFT | wxRIGHT | wxTOP | wxALIGN_CENTER_VERTICAL, 3); m_top_hsizer->AddSpacer(int(4*scale_factor)); @@ -243,7 +251,7 @@ void Tab::create_preset_tab() m_h_buttons_sizer->AddSpacer(int(8*scale_factor)); m_h_buttons_sizer->Add(m_btn_compare_preset, 0, wxALIGN_CENTER_VERTICAL); - m_top_hsizer->Add(m_h_buttons_sizer, 1, wxEXPAND | wxALIGN_CENTRE_VERTICAL); + m_top_hsizer->Add(m_h_buttons_sizer, 1, wxEXPAND); m_top_hsizer->AddSpacer(int(16*scale_factor)); // StretchSpacer has a strange behavior under OSX, so // There is used just additional sizer for m_mode_sizer with right alignment @@ -269,6 +277,9 @@ void Tab::create_preset_tab() m_treectrl = new wxTreeCtrl(panel, wxID_ANY, wxDefaultPosition, wxSize(20 * m_em_unit, -1), wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_SUNKEN | wxWANTS_CHARS); m_treectrl->SetFont(wxGetApp().normal_font()); +#ifdef __linux__ + m_treectrl->SetBackgroundColour(m_parent->GetBackgroundColour()); +#endif m_left_sizer->Add(m_treectrl, 1, wxEXPAND); // Index of the last icon inserted into m_treectrl m_icon_count = -1; @@ -399,10 +410,6 @@ Slic3r::GUI::PageShp Tab::add_options_page(const wxString& title, const std::str } // Initialize the page. PageShp page(new Page(m_page_view, title, icon_idx)); -// page->SetBackgroundStyle(wxBG_STYLE_SYSTEM); -#ifdef __WINDOWS__ -// page->SetDoubleBuffered(true); -#endif //__WINDOWS__ if (!is_extruder_pages) m_pages.push_back(page); @@ -466,6 +473,7 @@ void Tab::OnActivate() Refresh(); // Just refresh page, if m_presets_choice is already shown else { // From the tab creation whole top sizer is hidden to correct update of preset combobox's size + // (see https://github.com/prusa3d/PrusaSlicer/issues/10746) // On first OnActivate call show top sizer m_top_hsizer->ShowItems(true); @@ -600,6 +608,8 @@ void Tab::decorate() field->set_undo_tooltip(tt); field->set_undo_to_sys_tooltip(sys_tt); field->set_label_colour(color); + if (field->has_edit_ui()) + field->set_edit_bitmap(&m_bmp_edit_value); } if (m_active_page) @@ -933,6 +943,10 @@ void Tab::msw_rescale() m_presets_choice->msw_rescale(); m_treectrl->SetMinSize(wxSize(20 * m_em_unit, -1)); + if (m_compatible_printers.checkbox) + CheckBox::Rescale(m_compatible_printers.checkbox); + if (m_compatible_prints.checkbox) + CheckBox::Rescale(m_compatible_prints.checkbox); // rescale options_groups if (m_active_page) m_active_page->msw_rescale(); @@ -1336,10 +1350,10 @@ void Tab::update_preset_description_line() if (!default_filament_profiles.empty()) { description_line += "\n\n\t" + _(L("default filament profile")) + ": \n\t\t"; - for (auto& profile : default_filament_profiles) { + for (const std::string& profile : default_filament_profiles) { if (&profile != &*default_filament_profiles.begin()) description_line += ", "; - description_line += profile; + description_line += from_u8(profile); } } break; @@ -1539,14 +1553,15 @@ void TabPrint::build() optgroup->append_single_option_line("support_material_synchronize_layers", category_path + "synchronize-with-object-layers"); optgroup = page->new_optgroup(L("Organic supports")); - optgroup->append_single_option_line("support_tree_angle", category_path + "tree_angle"); - optgroup->append_single_option_line("support_tree_angle_slow", category_path + "tree_angle_slow"); - optgroup->append_single_option_line("support_tree_branch_diameter", category_path + "tree_branch_diameter"); - optgroup->append_single_option_line("support_tree_branch_diameter_angle", category_path + "tree_branch_diameter_angle"); - optgroup->append_single_option_line("support_tree_branch_diameter_double_wall", category_path + "tree_branch_diameter_double_wall"); - optgroup->append_single_option_line("support_tree_tip_diameter", category_path + "tree_tip_diameter"); - optgroup->append_single_option_line("support_tree_branch_distance", category_path + "tree_branch_distance"); - optgroup->append_single_option_line("support_tree_top_rate", category_path + "tree_top_rate"); + const std::string path = "organic-supports_480131#organic-supports-settings"; + optgroup->append_single_option_line("support_tree_angle", path); + optgroup->append_single_option_line("support_tree_angle_slow", path); + optgroup->append_single_option_line("support_tree_branch_diameter", path); + optgroup->append_single_option_line("support_tree_branch_diameter_angle", path); + optgroup->append_single_option_line("support_tree_branch_diameter_double_wall", path); + optgroup->append_single_option_line("support_tree_tip_diameter", path); + optgroup->append_single_option_line("support_tree_branch_distance", path); + optgroup->append_single_option_line("support_tree_top_rate", path); page = add_options_page(L("Speed"), "time"); optgroup = page->new_optgroup(L("Speed for print moves")); @@ -1683,6 +1698,34 @@ void TabPrint::build() Option option = optgroup->get_option("output_filename_format"); option.opt.full_width = true; optgroup->append_single_option_line(option); + optgroup->append_single_option_line("gcode_binary"); + + optgroup->m_on_change = [this](const t_config_option_key& opt_key, boost::any value) + { + if (opt_key == "gcode_binary") { + const bool is_binary = m_config->opt_bool("gcode_binary"); + std::string output_filename_format = m_config->opt_string("output_filename_format"); + bool modified = false; + if (is_binary && boost::iends_with(output_filename_format, ".gcode")) { + output_filename_format = output_filename_format.substr(0, output_filename_format.length() - 5) + "bgcode"; + modified = true; + } + else if (!is_binary && boost::iends_with(output_filename_format, ".bgcode")) { + output_filename_format = output_filename_format.substr(0, output_filename_format.length() - 6) + "gcode"; + modified = true; + } + if (modified) { + DynamicPrintConfig new_conf = *m_config; + auto off_option = static_cast(m_config->option("output_filename_format")->clone()); + off_option->value = output_filename_format; + new_conf.set_key_value("output_filename_format", off_option); + load_config(new_conf); + } + } + + update_dirty(); + update(); + }; optgroup = page->new_optgroup(L("Other")); @@ -1844,6 +1887,42 @@ static void validate_custom_gcode_cb(Tab* tab, const wxString& title, const t_co tab->on_value_change(opt_key, value); } +void Tab::edit_custom_gcode(const t_config_option_key& opt_key) +{ + EditGCodeDialog dlg = EditGCodeDialog(this, opt_key, get_custom_gcode(opt_key)); + if (dlg.ShowModal() == wxID_OK) { + set_custom_gcode(opt_key, dlg.get_edited_gcode()); + update_dirty(); + update(); + } +} + +const std::string& Tab::get_custom_gcode(const t_config_option_key& opt_key) +{ + return m_config->opt_string(opt_key); +} + +void Tab::set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) +{ + DynamicPrintConfig new_conf = *m_config; + new_conf.set_key_value(opt_key, new ConfigOptionString(value)); + load_config(new_conf); +} + +const std::string& TabFilament::get_custom_gcode(const t_config_option_key& opt_key) +{ + return m_config->opt_string(opt_key, unsigned(0)); +} + +void TabFilament::set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) +{ + std::vector gcodes = static_cast(m_config->option(opt_key))->values; + gcodes[0] = value; + + DynamicPrintConfig new_conf = *m_config; + new_conf.set_key_value(opt_key, new ConfigOptionStrings(gcodes)); + load_config(new_conf); +} void TabFilament::create_line_with_near_label_widget(ConfigOptionsGroupShp optgroup, const std::string& opt_key, int opt_index/* = 0*/) { Line line {"",""}; @@ -1856,7 +1935,8 @@ void TabFilament::create_line_with_near_label_widget(ConfigOptionsGroupShp optgr line = optgroup->create_single_option_line(optgroup->get_option(opt_key)); line.near_label_widget = [this, optgroup_wk = ConfigOptionsGroupWkp(optgroup), opt_key, opt_index](wxWindow* parent) { - wxCheckBox* check_box = new wxCheckBox(parent, wxID_ANY, ""); + wxWindow* check_box = CheckBox::GetNewWin(parent); + wxGetApp().UpdateDarkUI(check_box); check_box->Bind(wxEVT_CHECKBOX, [optgroup_wk, opt_key, opt_index](wxCommandEvent& evt) { const bool is_checked = evt.IsChecked(); @@ -1869,7 +1949,7 @@ void TabFilament::create_line_with_near_label_widget(ConfigOptionsGroupShp optgr field->set_na_value(); } } - }, check_box->GetId()); + }); m_overrides_options[opt_key] = check_box; return check_box; @@ -1885,40 +1965,63 @@ void TabFilament::update_line_with_near_label_widget(ConfigOptionsGroupShp optgr m_overrides_options[opt_key]->Enable(is_checked); is_checked &= !m_config->option(opt_key)->is_nil(); - m_overrides_options[opt_key]->SetValue(is_checked); + CheckBox::SetValue(m_overrides_options[opt_key], is_checked); Field* field = optgroup->get_fieldc(opt_key, opt_index); if (field != nullptr) field->toggle(is_checked); } -void TabFilament::add_filament_overrides_page() -{ - PageShp page = add_options_page(L("Filament Overrides"), "wrench"); - ConfigOptionsGroupShp optgroup = page->new_optgroup(L("Retraction")); - - const int extruder_idx = 0; // #ys_FIXME - - for (const std::string opt_key : { "filament_retract_length", +std::vector>> option_keys { + {"Travel lift", { "filament_retract_lift", + "filament_travel_ramping_lift", + "filament_travel_max_lift", + "filament_travel_slope", + //"filament_travel_lift_before_obstacle", "filament_retract_lift_above", - "filament_retract_lift_below", + "filament_retract_lift_below" + }}, + {"Retraction", { + "filament_retract_length", "filament_retract_speed", "filament_deretract_speed", "filament_retract_restart_extra", "filament_retract_before_travel", "filament_retract_layer_change", "filament_wipe", - "filament_retract_before_wipe" - }) - create_line_with_near_label_widget(optgroup, opt_key, extruder_idx); + "filament_retract_before_wipe", + }}, + {"Retraction when tool is disabled", { + "filament_retract_length_toolchange", + "filament_retract_restart_extra_toolchange" + }} +}; - optgroup = page->new_optgroup(L("Retraction when tool is disabled")); - for (const std::string opt_key : { "filament_retract_length_toolchange", - "filament_retract_restart_extra_toolchange" - }) - create_line_with_near_label_widget(optgroup, opt_key, extruder_idx); +void TabFilament::add_filament_overrides_page() +{ + PageShp page = add_options_page(L("Filament Overrides"), "wrench"); + const int extruder_idx = 0; // #ys_FIXME + + for (const auto&[title, keys] : option_keys) { + ConfigOptionsGroupShp optgroup = page->new_optgroup(L(title)); + for (const std::string& opt_key : keys) { + create_line_with_near_label_widget(optgroup, opt_key, extruder_idx); + } + } +} + +std::optional get_option_group(const Page* page, const std::string& title) { + auto og_it = std::find_if( + page->m_optgroups.begin(), page->m_optgroups.end(), + [&](const ConfigOptionsGroupShp& og) { + return og->title == title; + } + ); + if (og_it == page->m_optgroups.end()) + return {}; + return *og_it; } void TabFilament::update_filament_overrides_page() @@ -1927,44 +2030,77 @@ void TabFilament::update_filament_overrides_page() return; Page* page = m_active_page; - auto og_it = std::find_if(page->m_optgroups.begin(), page->m_optgroups.end(), [](const ConfigOptionsGroupShp og) { return og->title == "Retraction"; }); - if (og_it == page->m_optgroups.end()) - return; - ConfigOptionsGroupShp optgroup = *og_it; - - std::vector opt_keys = { "filament_retract_length", - "filament_retract_lift", - "filament_retract_lift_above", - "filament_retract_lift_below", - "filament_retract_speed", - "filament_deretract_speed", - "filament_retract_restart_extra", - "filament_retract_before_travel", - "filament_retract_layer_change", - "filament_wipe", - "filament_retract_before_wipe" - }; const int extruder_idx = 0; // #ys_FIXME - const bool have_retract_length = m_config->option("filament_retract_length")->is_nil() || - m_config->opt_float("filament_retract_length", extruder_idx) > 0; + const bool have_retract_length = ( + m_config->option("filament_retract_length")->is_nil() + || m_config->opt_float("filament_retract_length", extruder_idx) > 0 + ); - for (const std::string& opt_key : opt_keys) - { - bool is_checked = opt_key=="filament_retract_length" ? true : have_retract_length; - update_line_with_near_label_widget(optgroup, opt_key, extruder_idx, is_checked); + const bool uses_ramping_lift = ( + m_config->option("filament_travel_ramping_lift")->is_nil() + || m_config->opt_bool("filament_travel_ramping_lift", extruder_idx) + ); + + const bool is_lifting = ( + m_config->option("filament_travel_max_lift")->is_nil() + || m_config->opt_float("filament_travel_max_lift", extruder_idx) > 0 + || m_config->option("filament_retract_lift")->is_nil() + || m_config->opt_float("filament_retract_lift", extruder_idx) > 0 + ); + + for (const auto&[title, keys] : option_keys) { + std::optional optgroup{get_option_group(page, title)}; + if (!optgroup) { + continue; + } + + for (const std::string& opt_key : keys) { + bool is_checked{true}; + if ( + title == "Retraction" + && opt_key != "filament_retract_length" + && !have_retract_length + ) { + is_checked = false; } - og_it = std::find_if(page->m_optgroups.begin(), page->m_optgroups.end(), [](const ConfigOptionsGroupShp og) { return og->title == "Retraction when tool is disabled"; }); - if (og_it == page->m_optgroups.end()) - return; - optgroup = *og_it; + if ( + title == "Travel lift" + && uses_ramping_lift + && opt_key == "filament_retract_lift" + && !m_config->option("filament_travel_ramping_lift")->is_nil() + && m_config->opt_bool("filament_travel_ramping_lift", extruder_idx) + ) { + is_checked = false; + } - for (const std::string& opt_key : {"filament_retract_length_toolchange", "filament_retract_restart_extra_toolchange"}) - update_line_with_near_label_widget(optgroup, opt_key, extruder_idx); + if ( + title == "Travel lift" + && !is_lifting + && ( + opt_key == "filament_retract_lift_above" + || opt_key == "filament_retract_lift_below" + ) + ) { + is_checked = false; + } + if ( + title == "Travel lift" + && !uses_ramping_lift + && opt_key != "filament_travel_ramping_lift" + && opt_key != "filament_retract_lift" + && opt_key != "filament_retract_lift_above" + && opt_key != "filament_retract_lift_below" + ) { + is_checked = false; + } + update_line_with_near_label_widget(*optgroup, opt_key, extruder_idx, is_checked); + } + } } void TabFilament::create_extruder_combobox() @@ -2158,6 +2294,7 @@ void TabFilament::build() create_line_with_widget(optgroup.get(), "filament_ramming_parameters", "", [this](wxWindow* parent) { auto ramming_dialog_btn = new wxButton(parent, wxID_ANY, _(L("Ramming settings"))+dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); + wxGetApp().SetWindowVariantForButton(ramming_dialog_btn); wxGetApp().UpdateDarkUI(ramming_dialog_btn); ramming_dialog_btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); ramming_dialog_btn->SetSize(ramming_dialog_btn->GetBestSize()); @@ -2192,6 +2329,7 @@ void TabFilament::build() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("start_filament_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2202,6 +2340,7 @@ void TabFilament::build() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("end_filament_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2349,6 +2488,8 @@ void TabFilament::clear_pages() m_volumetric_speed_description_line = nullptr; m_cooling_description_line = nullptr; + for (auto& over_opt : m_overrides_options) + over_opt.second = nullptr; } void TabFilament::msw_rescale() @@ -2362,10 +2503,16 @@ void TabFilament::msw_rescale() void TabFilament::sys_color_changed() { + wxGetApp().UpdateDarkUI(m_extruders_cb); m_extruders_cb->Clear(); update_extruder_combobox(); Tab::sys_color_changed(); + for (const auto& over_opt : m_overrides_options) + if (wxWindow* check_box = over_opt.second) { + wxGetApp().UpdateDarkUI(check_box); + CheckBox::SysColorChanged(check_box); + } } void TabFilament::load_current_preset() @@ -2391,7 +2538,7 @@ void TabFilament::load_current_preset() m_extruders_cb->Select(m_active_extruder); } - assert(m_active_extruder >= 0 && m_active_extruder < m_preset_bundle->extruders_filaments.size()); + assert(m_active_extruder >= 0 && size_t(m_active_extruder) < m_preset_bundle->extruders_filaments.size()); const std::string& selected_extr_filament_name = m_preset_bundle->extruders_filaments[m_active_extruder].get_selected_preset_name(); if (selected_extr_filament_name != selected_filament_name) { m_presets->select_preset_by_name(selected_extr_filament_name, false); @@ -2516,10 +2663,6 @@ void TabPrinter::build_fff() return create_bed_shape_widget(parent); }); -//Y18 - Option option = optgroup->get_option("bed_exclude_area"); - option.opt.full_width = true; - optgroup->append_single_option_line(option); optgroup->append_single_option_line("max_print_height"); optgroup->append_single_option_line("z_offset"); @@ -2532,7 +2675,7 @@ void TabPrinter::build_fff() def.min = 1; def.max = 256; def.mode = comExpert; - option = Option(def, "extruders_count"); + Option option(def, "extruders_count"); optgroup->append_single_option_line(option); optgroup->append_single_option_line("single_extruder_multi_material"); @@ -2606,13 +2749,39 @@ void TabPrinter::build_fff() option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option); - optgroup->append_single_option_line("thumbnails_format"); optgroup->append_single_option_line("silent_mode"); optgroup->append_single_option_line("remaining_times"); optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) { wxTheApp->CallAfter([this, opt_key, value]() { + if (opt_key == "thumbnails" && m_config->has("thumbnails_format")) { + // to backward compatibility we need to update "thumbnails_format" from new "thumbnails" + if (const std::string val = boost::any_cast(value); !value.empty()) { + auto [thumbnails_list, errors] = GCodeThumbnails::make_and_check_thumbnail_list(val); + + if (errors != enum_bitmask()) { + // TRN: First argument is parameter name, the second one is the value. + std::string error_str = format(_u8L("Invalid value provided for parameter %1%: %2%"), "thumbnails", val); + error_str += GCodeThumbnails::get_error_string(errors); + InfoDialog(parent(), _L("G-code flavor is switched"), from_u8(error_str)).ShowModal(); + } + + if (!thumbnails_list.empty()) { + GCodeThumbnailsFormat old_format = GCodeThumbnailsFormat(m_config->option("thumbnails_format")->getInt()); + GCodeThumbnailsFormat new_format = thumbnails_list.begin()->first; + if (old_format != new_format) { + DynamicPrintConfig new_conf = *m_config; + + auto* opt = m_config->option("thumbnails_format")->clone(); + opt->setInt(int(new_format)); + new_conf.set_key_value("thumbnails_format", opt); + + load_config(new_conf); + } + } + } + } if (opt_key == "silent_mode") { bool val = boost::any_cast(value); if (m_use_silent_mode != val) { @@ -2682,6 +2851,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("start_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2695,6 +2865,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("end_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2705,6 +2876,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("before_layer_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2715,6 +2887,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("layer_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2725,6 +2898,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("toolchange_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2735,6 +2909,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("between_objects_gcode"); option.opt.full_width = true; option.opt.is_code = true; @@ -2745,6 +2920,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("color_change_gcode"); option.opt.is_code = true; option.opt.height = gcode_field_height;//150; @@ -2754,6 +2930,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("pause_print_gcode"); option.opt.is_code = true; option.opt.height = gcode_field_height;//150; @@ -2763,6 +2940,7 @@ void TabPrinter::build_fff() optgroup->m_on_change = [this, &optgroup_title = optgroup->title](const t_config_option_key& opt_key, const boost::any& value) { validate_custom_gcode_cb(this, optgroup_title, opt_key, value); }; + optgroup->edit_custom_gcode = [this](const t_config_option_key& opt_key) { edit_custom_gcode(opt_key); }; option = optgroup->get_option("template_custom_gcode"); option.opt.is_code = true; option.opt.height = gcode_field_height;//150; @@ -2994,7 +3172,8 @@ const std::vector extruder_options = { "min_layer_height", "max_layer_height", "extruder_offset", "retract_length", "retract_lift", "retract_lift_above", "retract_lift_below", "retract_speed", "deretract_speed", "retract_restart_extra", "retract_before_travel", - "retract_layer_change", "wipe", "retract_before_wipe", + "retract_layer_change", "wipe", "retract_before_wipe", "travel_ramping_lift", + "travel_slope", "travel_max_lift", "travel_lift_before_obstacle", "retract_length_toolchange", "retract_restart_extra_toolchange", }; @@ -3156,14 +3335,20 @@ void TabPrinter::build_extruder_pages(size_t n_before_extruders) optgroup = page->new_optgroup(L("Position (for multi-extruder printers)")); optgroup->append_single_option_line("extruder_offset", "", extruder_idx); - optgroup = page->new_optgroup(L("Retraction")); - optgroup->append_single_option_line("retract_length", "", extruder_idx); + optgroup = page->new_optgroup(L("Travel lift")); optgroup->append_single_option_line("retract_lift", "", extruder_idx); - line = { L("Only lift Z"), "" }; + optgroup->append_single_option_line("travel_ramping_lift", "", extruder_idx); + optgroup->append_single_option_line("travel_max_lift", "", extruder_idx); + optgroup->append_single_option_line("travel_slope", "", extruder_idx); + //optgroup->append_single_option_line("travel_lift_before_obstacle", "", extruder_idx); + + line = { L("Only lift"), "" }; line.append_option(optgroup->get_option("retract_lift_above", extruder_idx)); line.append_option(optgroup->get_option("retract_lift_below", extruder_idx)); optgroup->append_line(line); + optgroup = page->new_optgroup(L("Retraction")); + optgroup->append_single_option_line("retract_length", "", extruder_idx); optgroup->append_single_option_line("retract_speed", "", extruder_idx); optgroup->append_single_option_line("deretract_speed", "", extruder_idx); optgroup->append_single_option_line("retract_restart_extra", "", extruder_idx); @@ -3371,26 +3556,32 @@ void TabPrinter::toggle_options() { size_t i = size_t(val - 1); bool have_retract_length = m_config->opt_float("retract_length", i) > 0; + const bool ramping_lift = m_config->opt_bool("travel_ramping_lift", i); + const bool lifts_z = (ramping_lift && m_config->opt_float("travel_max_lift", i) > 0) + || (! ramping_lift && m_config->opt_float("retract_lift", i) > 0); // when using firmware retraction, firmware decides retraction length bool use_firmware_retraction = m_config->opt_bool("use_firmware_retraction"); toggle_option("retract_length", !use_firmware_retraction, i); + toggle_option("retract_lift", ! ramping_lift, i); + toggle_option("travel_max_lift", ramping_lift, i); + toggle_option("travel_slope", ramping_lift, i); // user can customize travel length if we have retraction length or we"re using // firmware retraction toggle_option("retract_before_travel", have_retract_length || use_firmware_retraction, i); // user can customize other retraction options if retraction is enabled bool retraction = (have_retract_length || use_firmware_retraction); - std::vector vec = { "retract_lift", "retract_layer_change" }; + std::vector vec = { }; for (auto el : vec) - toggle_option(el, retraction, i); + toggle_option("retract_layer_change", retraction, i); // retract lift above / below only applies if using retract lift vec.resize(0); vec = { "retract_lift_above", "retract_lift_below" }; for (auto el : vec) - toggle_option(el, retraction && (m_config->opt_float("retract_lift", i) > 0), i); + toggle_option(el, lifts_z, i); // some options only apply when not using firmware retraction vec.resize(0); @@ -3421,6 +3612,7 @@ void TabPrinter::toggle_options() load_config(new_conf); } + //toggle_option("travel_lift_before_obstacle", ramping_lift, i); toggle_option("retract_length_toolchange", have_multiple_extruders, i); bool toolchange_retraction = m_config->opt_float("retract_length_toolchange", i) > 0; @@ -3617,6 +3809,7 @@ void Tab::rebuild_page_tree() continue; auto itemId = m_treectrl->AppendItem(rootItem, translate_category(p->title(), m_type), p->iconID()); m_treectrl->SetItemTextColour(itemId, p->get_item_colour()); + m_treectrl->SetItemFont(itemId, wxGetApp().normal_font()); if (translate_category(p->title(), m_type) == selected) item = itemId; } @@ -4395,9 +4588,9 @@ void Tab::create_line_with_widget(ConfigOptionsGroup* optgroup, const std::strin // Return a callback to create a Tab widget to mark the preferences as compatible / incompatible to the current printer. wxSizer* Tab::compatible_widget_create(wxWindow* parent, PresetDependencies &deps) { - deps.checkbox = new wxCheckBox(parent, wxID_ANY, _(L("All"))); + deps.checkbox = CheckBox::GetNewWin(parent, _L("All")); deps.checkbox->SetFont(Slic3r::GUI::wxGetApp().normal_font()); - wxGetApp().UpdateDarkUI(deps.checkbox, false, true); + wxGetApp().UpdateDarkUI(deps.checkbox); deps.btn = new ScalableButton(parent, wxID_ANY, "printer", format_wxstr(" %s %s", _L("Set"), dots), wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); deps.btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); @@ -4409,11 +4602,12 @@ wxSizer* Tab::compatible_widget_create(wxWindow* parent, PresetDependencies &dep deps.checkbox->Bind(wxEVT_CHECKBOX, ([this, &deps](wxCommandEvent e) { - deps.btn->Enable(! deps.checkbox->GetValue()); + const bool is_checked = CheckBox::GetValue(deps.checkbox); + deps.btn->Enable(!is_checked); // All printers have been made compatible with this preset. - if (deps.checkbox->GetValue()) + if (is_checked) this->load_key_value(deps.key_list, std::vector {}); - this->get_field(deps.key_condition)->toggle(deps.checkbox->GetValue()); + this->get_field(deps.key_condition)->toggle(is_checked); this->update_changed_ui(); }) ); @@ -4456,7 +4650,7 @@ wxSizer* Tab::compatible_widget_create(wxWindow* parent, PresetDependencies &dep for (auto idx : selections) value.push_back(presets[idx].ToUTF8().data()); if (value.empty()) { - deps.checkbox->SetValue(1); + CheckBox::SetValue(deps.checkbox, true); deps.btn->Disable(); } // All depending_presets have been made compatible with this preset. @@ -4476,22 +4670,24 @@ void SubstitutionManager::init(DynamicPrintConfig* config, wxWindow* parent, wxF m_parent = parent; m_grid_sizer = grid_sizer; m_em = em_unit(parent); + m_substitutions = m_config->option("gcode_substitutions")->values; + m_chb_match_single_lines.clear(); } -void SubstitutionManager::validate_lenth() +void SubstitutionManager::validate_length() { - std::vector& substitutions = m_config->option("gcode_substitutions")->values; - if ((substitutions.size() % 4) != 0) { + if ((m_substitutions.size() % 4) != 0) { WarningDialog(m_parent, "Value of gcode_substitutions parameter will be cut to valid length", "Invalid length of gcode_substitutions parameter").ShowModal(); - substitutions.resize(substitutions.size() - (substitutions.size() % 4)); + m_substitutions.resize(m_substitutions.size() - (m_substitutions.size() % 4)); + // save changes from m_substitutions to config + m_config->option("gcode_substitutions")->values = m_substitutions; } } -bool SubstitutionManager::is_compatibile_with_ui() +bool SubstitutionManager::is_compatible_with_ui() { - const std::vector& substitutions = m_config->option("gcode_substitutions")->values; - if (int(substitutions.size() / 4) != m_grid_sizer->GetEffectiveRowsCount() - 1) { + if (int(m_substitutions.size() / 4) != m_grid_sizer->GetEffectiveRowsCount() - 1) { ErrorDialog(m_parent, "Invalid compatibility between UI and BE", false).ShowModal(); return false; } @@ -4500,8 +4696,7 @@ bool SubstitutionManager::is_compatibile_with_ui() bool SubstitutionManager::is_valid_id(int substitution_id, const wxString& message) { - const std::vector& substitutions = m_config->option("gcode_substitutions")->values; - if (int(substitutions.size() / 4) < substitution_id) { + if (int(m_substitutions.size() / 4) < substitution_id) { ErrorDialog(m_parent, message, false).ShowModal(); return false; } @@ -4527,7 +4722,7 @@ void SubstitutionManager::create_legend() // delete substitution_id from substitutions void SubstitutionManager::delete_substitution(int substitution_id) { - validate_lenth(); + validate_length(); if (!is_valid_id(substitution_id, "Invalid substitution_id to delete")) return; @@ -4552,15 +4747,16 @@ void SubstitutionManager::add_substitution( int substitution_id, if (substitution_id < 0) { if (m_grid_sizer->IsEmpty()) { create_legend(); - substitution_id = 0; } substitution_id = m_grid_sizer->GetEffectiveRowsCount() - 1; // create new substitution // it have to be added to config too - std::vector& substitutions = m_config->option("gcode_substitutions")->values; for (size_t i = 0; i < 4; i ++) - substitutions.push_back(std::string()); + m_substitutions.push_back(std::string()); + + // save changes from config to m_substitutions + m_config->option("gcode_substitutions")->values = m_substitutions; call_after_layout = true; } @@ -4574,11 +4770,7 @@ void SubstitutionManager::add_substitution( int substitution_id, auto top_sizer = new wxBoxSizer(wxHORIZONTAL); auto add_text_editor = [substitution_id, top_sizer, this](const wxString& value, int opt_pos, int proportion) { - auto editor = new wxTextCtrl(m_parent, wxID_ANY, value, wxDefaultPosition, wxSize(15 * m_em, wxDefaultCoord), wxTE_PROCESS_ENTER -#ifdef _WIN32 - | wxBORDER_SIMPLE -#endif - ); + auto editor = new ::TextInput(m_parent, value, "", "", wxDefaultPosition, wxSize(15 * m_em, wxDefaultCoord), wxTE_PROCESS_ENTER); editor->SetFont(wxGetApp().normal_font()); wxGetApp().UpdateDarkUI(editor); @@ -4607,37 +4799,38 @@ void SubstitutionManager::add_substitution( int substitution_id, bool whole_word = strchr(params.c_str(), 'w') != nullptr || strchr(params.c_str(), 'W') != nullptr; bool match_single_line = strchr(params.c_str(), 's') != nullptr || strchr(params.c_str(), 'S') != nullptr; - auto chb_regexp = new wxCheckBox(m_parent, wxID_ANY, _L("Regular expression")); - chb_regexp->SetValue(regexp); + auto chb_regexp = CheckBox::GetNewWin(m_parent, _L("Regular expression")); + CheckBox::SetValue(chb_regexp, regexp); params_sizer->Add(chb_regexp, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, m_em); - auto chb_case_insensitive = new wxCheckBox(m_parent, wxID_ANY, _L("Case insensitive")); - chb_case_insensitive->SetValue(case_insensitive); + auto chb_case_insensitive = CheckBox::GetNewWin(m_parent, _L("Case insensitive")); + CheckBox::SetValue(chb_case_insensitive, case_insensitive); params_sizer->Add(chb_case_insensitive, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, m_em); - auto chb_whole_word = new wxCheckBox(m_parent, wxID_ANY, _L("Whole word")); - chb_whole_word->SetValue(whole_word); + auto chb_whole_word = CheckBox::GetNewWin(m_parent, _L("Whole word")); + CheckBox::SetValue(chb_whole_word, whole_word); params_sizer->Add(chb_whole_word, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, m_em); - auto chb_match_single_line = new wxCheckBox(m_parent, wxID_ANY, _L("Match single line")); - chb_match_single_line->SetValue(match_single_line); + auto chb_match_single_line = CheckBox::GetNewWin(m_parent, _L("Match single line")); + CheckBox::SetValue(chb_match_single_line, match_single_line); chb_match_single_line->Show(regexp); + m_chb_match_single_lines.emplace_back(chb_match_single_line); params_sizer->Add(chb_match_single_line, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, m_em); - for (wxCheckBox* chb : std::initializer_list{ chb_regexp, chb_case_insensitive, chb_whole_word, chb_match_single_line }) { + for (wxWindow* chb : std::initializer_list{ chb_regexp, chb_case_insensitive, chb_whole_word, chb_match_single_line }) { chb->SetFont(wxGetApp().normal_font()); chb->Bind(wxEVT_CHECKBOX, [this, substitution_id, chb_regexp, chb_case_insensitive, chb_whole_word, chb_match_single_line](wxCommandEvent e) { std::string value = std::string(); - if (chb_regexp->GetValue()) + if (CheckBox::GetValue(chb_regexp)) value += "r"; - if (chb_case_insensitive->GetValue()) + if (CheckBox::GetValue(chb_case_insensitive)) value += "i"; - if (chb_whole_word->GetValue()) + if (CheckBox::GetValue(chb_whole_word)) value += "w"; - if (chb_match_single_line->GetValue()) + if (CheckBox::GetValue(chb_match_single_line)) value += "s"; - chb_match_single_line->Show(chb_regexp->GetValue()); + chb_match_single_line->Show(CheckBox::GetValue(chb_regexp)); m_grid_sizer->Layout(); edit_substitution(substitution_id, 2, value); @@ -4657,16 +4850,34 @@ void SubstitutionManager::add_substitution( int substitution_id, void SubstitutionManager::update_from_config() { - if (!m_grid_sizer->IsEmpty()) - m_grid_sizer->Clear(true); - std::vector& subst = m_config->option("gcode_substitutions")->values; + if (m_substitutions == subst && m_grid_sizer->IsShown(1)) { + // just update visibility for chb_match_single_lines + int subst_id = 0; + assert(m_chb_match_single_lines.size() == size_t(subst.size()/4)); + for (size_t i = 0; i < subst.size(); i += 4) { + const std::string& params = subst[i + 2]; + const bool regexp = strchr(params.c_str(), 'r') != nullptr || strchr(params.c_str(), 'R') != nullptr; + m_chb_match_single_lines[subst_id++]->Show(regexp); + } + + // "gcode_substitutions" values didn't changed in config. There is no need to update/recreate controls + return; + } + + m_substitutions = subst; + + if (!m_grid_sizer->IsEmpty()) { + m_grid_sizer->Clear(true); + m_chb_match_single_lines.clear(); + } + if (subst.empty()) hide_delete_all_btn(); else create_legend(); - validate_lenth(); + validate_length(); int subst_id = 0; for (size_t i = 0; i < subst.size(); i += 4) @@ -4677,24 +4888,27 @@ void SubstitutionManager::update_from_config() void SubstitutionManager::delete_all() { + m_substitutions.clear(); m_config->option("gcode_substitutions")->values.clear(); call_ui_update(); - if (!m_grid_sizer->IsEmpty()) + if (!m_grid_sizer->IsEmpty()) { m_grid_sizer->Clear(true); + m_chb_match_single_lines.clear(); + } m_parent->GetParent()->Layout(); } void SubstitutionManager::edit_substitution(int substitution_id, int opt_pos, const std::string& value) { - std::vector& substitutions = m_config->option("gcode_substitutions")->values; - - validate_lenth(); - if(!is_compatibile_with_ui() || !is_valid_id(substitution_id, "Invalid substitution_id to edit")) + validate_length(); + if(!is_compatible_with_ui() || !is_valid_id(substitution_id, "Invalid substitution_id to edit")) return; - substitutions[substitution_id * 4 + opt_pos] = value; + m_substitutions[substitution_id * 4 + opt_pos] = value; + // save changes from m_substitutions to config + m_config->option("gcode_substitutions")->values = m_substitutions; call_ui_update(); } @@ -4747,6 +4961,7 @@ wxSizer* TabPrint::create_substitutions_widget(wxWindow* parent) m_subst_manager.init(m_config, parent, grid_sizer); m_subst_manager.set_cb_edited_substitution([this]() { update_dirty(); + Layout(); wxGetApp().mainframe->on_config_changed(m_config); // invalidate print }); m_subst_manager.set_cb_hide_delete_all_btn([this]() { @@ -4882,7 +5097,7 @@ void Tab::compatible_widget_reload(PresetDependencies &deps) bool has_any = ! m_config->option(deps.key_list)->values.empty(); has_any ? deps.btn->Enable() : deps.btn->Disable(); - deps.checkbox->SetValue(! has_any); + CheckBox::SetValue(deps.checkbox, !has_any); field->toggle(! has_any); } @@ -4911,6 +5126,9 @@ void Tab::fill_icon_descriptions() "the current option group.\n" "Click the BACK ARROW icon to reset all settings for the current option group to " "the last saved preset.")); + m_icon_descriptions.emplace_back(&m_bmp_edit_value, L("EDIT VALUE"), + // TRN Description for "EDIT VALUE" in the Help dialog (the icon is currently used only to edit custom gcodes). + L("clicking this icon opens a dialog allowing to edit this value.")); } void Tab::set_tooltips_text() diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 9d57709..53ad7c4 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -37,6 +37,7 @@ #include "OptionsGroup.hpp" #include "libslic3r/Preset.hpp" +class CheckBox; namespace Slic3r { namespace GUI { @@ -56,8 +57,11 @@ class SubstitutionManager std::function m_cb_edited_substitution{ nullptr }; std::function m_cb_hide_delete_all_btn{ nullptr }; - void validate_lenth(); - bool is_compatibile_with_ui(); + std::vector m_substitutions; + std::vector m_chb_match_single_lines; + + void validate_length(); + bool is_compatible_with_ui(); bool is_valid_id(int substitution_id, const wxString& message); public: @@ -185,7 +189,7 @@ protected: struct PresetDependencies { Preset::Type type = Preset::TYPE_INVALID; - wxCheckBox *checkbox = nullptr; + wxWindow *checkbox = nullptr; ScalableButton *btn = nullptr; std::string key_list; // "compatible_printers" std::string key_condition; @@ -213,6 +217,8 @@ protected: ScalableBitmap *m_bmp_non_system; // Bitmaps to be shown on the "Undo user changes" button next to each input field. ScalableBitmap m_bmp_value_revert; + // Bitmaps to be shown on the "Undo user changes" button next to each input field. + ScalableBitmap m_bmp_edit_value; std::vector m_scaled_buttons = {}; std::vector m_scaled_bitmaps = {}; @@ -389,6 +395,9 @@ public: bool validate_custom_gcodes(); bool validate_custom_gcodes_was_shown{ false }; + void edit_custom_gcode(const t_config_option_key& opt_key); + virtual const std::string& get_custom_gcode(const t_config_option_key& opt_key); + virtual void set_custom_gcode(const t_config_option_key& opt_key, const std::string& value); protected: void create_line_with_widget(ConfigOptionsGroup* optgroup, const std::string& opt_key, const std::string& path, widget_t widget); wxSizer* compatible_widget_create(wxWindow* parent, PresetDependencies &deps); @@ -450,7 +459,7 @@ class TabFilament : public Tab void create_extruder_combobox(); void update_volumetric_flow_preset_hints(); - std::map m_overrides_options; + std::map m_overrides_options; public: TabFilament(wxBookCtrlBase* parent) : Tab(parent, _(L("Filament Settings")), Slic3r::Preset::TYPE_FILAMENT) {} @@ -473,6 +482,8 @@ public: void update_extruder_combobox(); int get_active_extruder() const { return m_active_extruder; } + const std::string& get_custom_gcode(const t_config_option_key& opt_key) override; + void set_custom_gcode(const t_config_option_key& opt_key, const std::string& value) override; protected: bool select_preset_by_name(const std::string& name_w_suffix, bool force) override; bool save_current_preset(const std::string& new_name, bool detach) override; diff --git a/src/slic3r/GUI/TextLines.cpp b/src/slic3r/GUI/TextLines.cpp index bc3eb58..3a0baa4 100644 --- a/src/slic3r/GUI/TextLines.cpp +++ b/src/slic3r/GUI/TextLines.cpp @@ -154,7 +154,7 @@ TextLines select_closest_contour(const std::vector &line_contours) { std::vector linesf = to_linesf(expolygons); AABBTreeIndirect::Tree2d tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf); - size_t line_idx; + size_t line_idx = 0; Vec2d hit_point; // double distance = AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, zero, line_idx, hit_point); @@ -218,24 +218,6 @@ GLModel::Geometry create_geometry(const TextLines &lines, float radius, bool is_ return geometry; } -bool get_line_height_offset(const FontProp &fp, const FontFile &ff, double &line_height_mm, double &line_offset_mm) -{ - double third_ascent_shape_size = get_font_info(ff, fp).ascent / 3.; - int line_height_shape_size = get_line_height(ff, fp); // In shape size - - double scale = get_shape_scale(fp, ff); - line_offset_mm = third_ascent_shape_size * scale / SHAPE_SCALE; - line_height_mm = line_height_shape_size * scale; - - if (line_height_mm < 0) - return false; - - // fix for bad filled ascent in font file - if (line_offset_mm <= 0) - line_offset_mm = line_height_mm / 3; - - return true; -} } // namespace void TextLinesModel::init(const Transform3d &text_tr, @@ -259,8 +241,9 @@ void TextLinesModel::init(const Transform3d &text_tr, FontProp::VerticalAlign align = fp.align.second; - double line_height_mm, line_offset_mm; - if (!get_line_height_offset(fp, ff, line_height_mm, line_offset_mm)) + double line_height_mm = calc_line_height_in_mm(ff, fp); + assert(line_height_mm > 0); + if (line_height_mm <= 0) return; m_model.reset(); @@ -364,9 +347,9 @@ void TextLinesModel::render(const Transform3d &text_world) shader->stop_using(); } -double TextLinesModel::calc_line_height(const Slic3r::Emboss::FontFile &ff, const FontProp &fp) +double TextLinesModel::calc_line_height_in_mm(const Slic3r::Emboss::FontFile &ff, const FontProp &fp) { int line_height = Slic3r::Emboss::get_line_height(ff, fp); // In shape size - double scale = Slic3r::Emboss::get_shape_scale(fp, ff); + double scale = Slic3r::Emboss::get_text_shape_scale(fp, ff); return line_height * scale; } diff --git a/src/slic3r/GUI/TextLines.hpp b/src/slic3r/GUI/TextLines.hpp index a6a7b19..f6e297c 100644 --- a/src/slic3r/GUI/TextLines.hpp +++ b/src/slic3r/GUI/TextLines.hpp @@ -32,7 +32,7 @@ public: void reset() { m_model.reset(); m_lines.clear(); } const Slic3r::Emboss::TextLines &get_lines() const { return m_lines; } - static double calc_line_height(const Slic3r::Emboss::FontFile& ff, const FontProp& fp); // return lineheight in mm + static double calc_line_height_in_mm(const Slic3r::Emboss::FontFile& ff, const FontProp& fp); // return lineheight in mm private: Slic3r::Emboss::TextLines m_lines; diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 556b1f6..6b6a04e 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -838,13 +838,7 @@ UnsavedChangesDialog::UnsavedChangesDialog(Preset::Type type, PresetCollection* void UnsavedChangesDialog::build(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header) { -#if defined(__WXMSW__) - // ys_FIXME! temporary workaround for correct font scaling - // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, - // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT -// this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); this->SetFont(wxGetApp().normal_font()); -#endif // __WXMSW__ int border = 10; int em = em_unit(); @@ -1209,11 +1203,6 @@ static wxString get_string_value(std::string opt_key, const DynamicPrintConfig& BedShape shape(*config.option(opt_key)); return shape.get_full_name_with_params(); } -//Y18 - if (opt_key == "bed_exclude_area") - return get_thumbnails_string(config.option(opt_key)->values); - if (opt_key == "thumbnails") - return get_thumbnails_string(config.option(opt_key)->values); Vec2d val = config.opt(opt_key)->get_at(opt_idx); return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str()); @@ -1342,7 +1331,7 @@ void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect) { int em = em_unit(); - msw_buttons_rescale(this, em, { wxID_CANCEL, m_save_btn_id, m_move_btn_id, m_continue_btn_id }); + msw_buttons_rescale(this, em, { wxID_CANCEL, m_save_btn_id, m_move_btn_id, m_continue_btn_id }, 1.5); const wxSize& size = wxSize(70 * em, 30 * em); SetMinSize(size); @@ -1491,6 +1480,7 @@ void DiffPresetDialog::create_presets_sizer() auto add_preset_combobox = [collection, sizer, new_type, this](PresetComboBox** cb_, PresetBundle* preset_bundle) { *cb_ = new PresetComboBox(this, new_type, wxSize(em_unit() * 35, -1), preset_bundle); PresetComboBox*cb = (*cb_); + cb->SetFont(this->GetFont()); cb->show_modif_preset_separately(); cb->set_selection_changed_function([this, new_type, preset_bundle, cb](int selection) { std::string preset_name = Preset::remove_suffix_modified(cb->GetString(selection).ToUTF8().data()); @@ -1555,6 +1545,7 @@ void DiffPresetDialog::create_info_lines() void DiffPresetDialog::create_tree() { m_tree = new DiffViewCtrl(this, wxSize(em_unit() * 65, em_unit() * 40)); + m_tree->SetFont(this->GetFont()); m_tree->AppendToggleColumn_(L"\u2714", DiffModel::colToggle, wxLinux ? 9 : 6); m_tree->AppendBmpTextColumn("", DiffModel::colIconText, 35); m_tree->AppendBmpTextColumn(_L("Left Preset Value"), DiffModel::colOldValue, 15); @@ -1676,16 +1667,9 @@ void DiffPresetDialog::complete_dialog_creation() } DiffPresetDialog::DiffPresetDialog(MainFrame* mainframe) - : DPIDialog(mainframe, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + : DPIDialog(mainframe, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER, "diff_presets_dialog", mainframe->normal_font().GetPointSize()), m_pr_technology(wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology()) { -#if defined(__WXMSW__) - // ys_FIXME! temporary workaround for correct font scaling - // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, - // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT -// this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); - this->SetFont(mainframe->normal_font()); -#endif // __WXMSW__ // Init bundles diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index 0ec4e70..fbf3896 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -315,6 +315,12 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, bool force_ flex->Add(update_comment); } + if (! update.new_printers.empty()) { + flex->Add(new wxStaticText(this, wxID_ANY, _L_PLURAL("New printer", "New printers", update.new_printers.find(',') == std::string::npos ? 1 : 2) + ":"), 0, wxALIGN_RIGHT); + auto* update_printer = new wxStaticText(this, wxID_ANY, from_u8(update.new_printers)); + update_printer->Wrap(CONTENT_WIDTH * wxGetApp().em_unit()); + flex->Add(update_printer); + } versions->Add(flex); if (! update.changelog_url.empty() && update.version.prerelease() == nullptr) { @@ -375,6 +381,12 @@ MsgUpdateForced::MsgUpdateForced(const std::vector& updates) : versions->Add(update_comment); } + if (!update.new_printers.empty()) { + versions->Add(new wxStaticText(this, wxID_ANY, _L_PLURAL("New printer", "New printers", update.new_printers.find(',') == std::string::npos ? 1 : 2)+":")/*, 0, wxALIGN_RIGHT*/); + auto* update_printer = new wxStaticText(this, wxID_ANY, from_u8(update.new_printers)); + update_printer->Wrap(CONTENT_WIDTH * wxGetApp().em_unit()); + versions->Add(update_printer); + } if (!update.changelog_url.empty() && update.version.prerelease() == nullptr) { auto* line = new wxBoxSizer(wxHORIZONTAL); auto changelog_url = (boost::format(update.changelog_url) % lang_code).str(); diff --git a/src/slic3r/GUI/UpdateDialogs.hpp b/src/slic3r/GUI/UpdateDialogs.hpp index 5e2e50d..49b8c52 100644 --- a/src/slic3r/GUI/UpdateDialogs.hpp +++ b/src/slic3r/GUI/UpdateDialogs.hpp @@ -95,12 +95,14 @@ public: Semver version; std::string comment; std::string changelog_url; + std::string new_printers; - Update(std::string vendor, Semver version, std::string comment, std::string changelog_url) + Update(std::string vendor, Semver version, std::string comment, std::string changelog_url, std::string new_printers) : vendor(std::move(vendor)) , version(std::move(version)) , comment(std::move(comment)) , changelog_url(std::move(changelog_url)) + , new_printers(std::move(new_printers)) {} }; @@ -123,12 +125,14 @@ public: Semver version; std::string comment; std::string changelog_url; + std::string new_printers; - Update(std::string vendor, Semver version, std::string comment, std::string changelog_url) + Update(std::string vendor, Semver version, std::string comment, std::string changelog_url, std::string new_printers) : vendor(std::move(vendor)) , version(std::move(version)) , comment(std::move(comment)) , changelog_url(std::move(changelog_url)) + , new_printers(std::move(new_printers)) {} }; diff --git a/src/slic3r/GUI/Widgets/BitmapToggleButton.cpp b/src/slic3r/GUI/Widgets/BitmapToggleButton.cpp new file mode 100644 index 0000000..ccb6b42 --- /dev/null +++ b/src/slic3r/GUI/Widgets/BitmapToggleButton.cpp @@ -0,0 +1,47 @@ +#include "BitmapToggleButton.hpp" + +#include + +BitmapToggleButton::BitmapToggleButton(wxWindow* parent, const wxString& label, wxWindowID id) +{ + const long style = wxBORDER_NONE | wxBU_EXACTFIT | wxBU_LEFT; + if (label.IsEmpty()) + wxBitmapToggleButton::Create(parent, id, wxNullBitmap, wxDefaultPosition, wxDefaultSize, style); + else { +#ifdef __WXGTK3__ + wxSize label_size = parent->GetTextExtent(label); + wxSize def_size = wxSize(label_size.GetX() + 20, label_size.GetY()); +#else + wxSize def_size = wxDefaultSize; +#endif + // Call Create() from wxToggleButton instead of wxBitmapToggleButton to allow add Label text under Linux + wxToggleButton::Create(parent, id, label, wxDefaultPosition, def_size, style); + } + +#ifdef __WXMSW__ + if (parent) { + SetBackgroundColour(parent->GetBackgroundColour()); + SetForegroundColour(parent->GetForegroundColour()); + } +#elif __WXGTK3__ + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif + + Bind(wxEVT_TOGGLEBUTTON, [this](auto& e) { + update(); + + wxCommandEvent evt(wxEVT_CHECKBOX); + evt.SetInt(int(GetValue())); + wxPostEvent(this, evt); + + e.Skip(); + }); +} + +void BitmapToggleButton::update_size() +{ +#ifndef __WXGTK3__ + wxSize best_sz = GetBestSize(); + SetSize(best_sz); +#endif +} diff --git a/src/slic3r/GUI/Widgets/BitmapToggleButton.hpp b/src/slic3r/GUI/Widgets/BitmapToggleButton.hpp new file mode 100644 index 0000000..db653e0 --- /dev/null +++ b/src/slic3r/GUI/Widgets/BitmapToggleButton.hpp @@ -0,0 +1,17 @@ +#ifndef slic3r_GUI_BitmapToggleButton_hpp_ +#define slic3r_GUI_BitmapToggleButton_hpp_ + +#include + +class BitmapToggleButton : public wxBitmapToggleButton +{ + virtual void update() = 0; + +public: + BitmapToggleButton(wxWindow * parent = NULL, const wxString& label = wxEmptyString, wxWindowID id = wxID_ANY); + +protected: + void update_size(); +}; + +#endif // !slic3r_GUI_BitmapToggleButton_hpp_ diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 585e8e2..34b0f98 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -1,7 +1,8 @@ #include "Button.hpp" -#include "Label.hpp" #include +#include +#include BEGIN_EVENT_TABLE(Button, StaticBox) @@ -26,9 +27,9 @@ Button::Button() : paddingSize(10, 8) { background_color = StateColor( - std::make_pair(0xF0F0F1, (int) StateColor::Disabled), - std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked), - std::make_pair(0x009688, (int) StateColor::Checked), + std::make_pair(0xF0F0F0, (int) StateColor::Disabled), + std::make_pair(0x37EE7C, (int) StateColor::Hovered | StateColor::Checked), + std::make_pair(0x00AE42, (int) StateColor::Checked), std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered), std::make_pair(*wxWHITE, (int) StateColor::Normal)); text_color = StateColor( @@ -36,23 +37,20 @@ Button::Button() std::make_pair(*wxBLACK, (int) StateColor::Normal)); } -Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) +Button::Button(wxWindow* parent, wxString text, wxString icon, long style, wxSize iconSize/* = wxSize(16, 16)*/) : Button() { - Create(parent, text, icon, style, iconSize, btn_id); + Create(parent, text, icon, style, iconSize); } -bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) +bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, wxSize iconSize/* = wxSize(16, 16)*/) { - StaticBox::Create(parent, btn_id, wxDefaultPosition, wxDefaultSize, style); + StaticBox::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, style); state_handler.attach({&text_color}); state_handler.update_binds(); - //BBS set default font - SetFont(Label::Body_14); wxWindow::SetLabel(text); if (!icon.IsEmpty()) { - //BBS set button icon default size to 20 - this->active_icon = ScalableBitmap(this, icon.ToStdString(), iconSize > 0 ? iconSize : 20); + this->active_icon = ScalableBitmap(this, icon.ToStdString(), iconSize); } messureSize(); return true; @@ -65,19 +63,11 @@ void Button::SetLabel(const wxString& label) Refresh(); } -bool Button::SetFont(const wxFont& font) -{ - wxWindow::SetFont(font); - messureSize(); - Refresh(); - return true; -} void Button::SetIcon(const wxString& icon) { if (!icon.IsEmpty()) { - //BBS set button icon default size to 20 - this->active_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); + this->active_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_size()); } else { @@ -89,8 +79,7 @@ void Button::SetIcon(const wxString& icon) void Button::SetInactiveIcon(const wxString &icon) { if (!icon.IsEmpty()) { - // BBS set button icon default size to 20 - this->inactive_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); + this->inactive_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_size()); } else { this->inactive_icon = ScalableBitmap(); } @@ -135,22 +124,16 @@ bool Button::Enable(bool enable) void Button::SetCanFocus(bool canFocus) { this->canFocus = canFocus; } -void Button::SetValue(bool state) -{ - if (GetValue() == state) return; - state_handler.set_state(state ? StateHandler::Checked : 0, StateHandler::Checked); -} - -bool Button::GetValue() const { return state_handler.states() & StateHandler::Checked; } void Button::Rescale() { - if (this->active_icon.get_bitmap().IsOk()) +/* if (this->active_icon.bmp().IsOk()) this->active_icon.msw_rescale(); - if (this->inactive_icon.get_bitmap().IsOk()) + if (this->inactive_icon.bmp().IsOk()) this->inactive_icon.msw_rescale(); +*/ messureSize(); } @@ -174,15 +157,16 @@ void Button::render(wxDC& dc) dc.SetBrush(*wxTRANSPARENT_BRUSH); // calc content size wxSize szIcon; - wxSize szContent = textSize.GetSize(); + wxSize szContent = textSize; ScalableBitmap icon; if (m_selected || ((states & (int)StateColor::State::Hovered) != 0)) +// if (m_selected || (states & (int)StateColor::State::Hovered)) icon = active_icon; else icon = inactive_icon; int padding = 5; - if (icon.get_bitmap().IsOk()) { + if (icon.bmp().IsOk()) { if (szContent.y > 0) { //BBS norrow size between text and icon szContent.x += padding; @@ -204,7 +188,7 @@ void Button::render(wxDC& dc) rcContent.Deflate(offset.x, offset.y); // start draw wxPoint pt = rcContent.GetLeftTop(); - if (icon.get_bitmap().IsOk()) { + if (icon.bmp().IsOk()) { pt.y += (rcContent.height - szIcon.y) / 2; dc.DrawBitmap(icon.get_bitmap(), pt); //BBS norrow size between text and icon @@ -213,18 +197,11 @@ void Button::render(wxDC& dc) } auto text = GetLabel(); if (!text.IsEmpty()) { - if (pt.x + textSize.width > size.x) + if (pt.x + textSize.x > size.x) text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - pt.x); - pt.y += (rcContent.height - textSize.height) / 2; + pt.y += (rcContent.height - textSize.y) / 2; + dc.SetFont(GetFont()); dc.SetTextForeground(text_color.colorForStates(states)); -#if 0 - dc.SetBrush(*wxLIGHT_GREY); - dc.SetPen(wxPen(*wxLIGHT_GREY)); - dc.DrawRectangle(pt, textSize.GetSize()); -#endif -#ifdef __WXOSX__ - pt.y -= textSize.x / 2; -#endif dc.DrawText(text, pt); } } @@ -232,9 +209,13 @@ void Button::render(wxDC& dc) void Button::messureSize() { wxClientDC dc(this); - dc.GetTextExtent(GetLabel(), &textSize.width, &textSize.height, &textSize.x, &textSize.y); - wxSize szContent = textSize.GetSize(); - if (this->active_icon.get_bitmap().IsOk()) { + textSize = dc.GetTextExtent(GetLabel()); + if (minSize.GetWidth() > 0) { + wxWindow::SetMinSize(minSize); + return; + } + wxSize szContent = textSize; + if (this->active_icon.bmp().IsOk()) { if (szContent.y > 0) { //BBS norrow size between text and icon szContent.x += 5; @@ -248,9 +229,6 @@ void Button::messureSize() if (minSize.GetHeight() > 0) size.SetHeight(minSize.GetHeight()); - if (minSize.GetWidth() > size.GetWidth()) - wxWindow::SetMinSize(minSize); - else wxWindow::SetMinSize(size); } @@ -260,7 +238,6 @@ void Button::mouseDown(wxMouseEvent& event) pressedDown = true; if (canFocus) SetFocus(); - if (!HasCapture()) CaptureMouse(); } diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 2f5c8ea..149a74a 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -6,7 +6,7 @@ class Button : public StaticBox { - wxRect textSize; + wxSize textSize; wxSize minSize; // set by outer wxSize paddingSize; ScalableBitmap active_icon; @@ -24,13 +24,12 @@ class Button : public StaticBox public: Button(); - Button(wxWindow* parent, wxString text, wxString icon = "", long style = 0, int iconSize = 0, wxWindowID btn_id = wxID_ANY); + Button(wxWindow* parent, wxString text, wxString icon = "", long style = 0, wxSize iconSize = wxSize(16, 16)); - bool Create(wxWindow* parent, wxString text, wxString icon = "", long style = 0, int iconSize = 0, wxWindowID btn_id = wxID_ANY); + bool Create(wxWindow* parent, wxString text, wxString icon = "", long style = 0, wxSize iconSize = wxSize(16, 16)); void SetLabel(const wxString& label) override; - bool SetFont(const wxFont& font) override; void SetIcon(const wxString& icon); @@ -50,9 +49,6 @@ public: void SetCanFocus(bool canFocus) override; - void SetValue(bool state); - - bool GetValue() const; void Rescale(); diff --git a/src/slic3r/GUI/Widgets/CheckBox.cpp b/src/slic3r/GUI/Widgets/CheckBox.cpp new file mode 100644 index 0000000..618c9a9 --- /dev/null +++ b/src/slic3r/GUI/Widgets/CheckBox.cpp @@ -0,0 +1,119 @@ +#include "CheckBox.hpp" + +//#include "../wxExtensions.hpp" + +const int px_cnt = 16; + +CheckBox::CheckBox(wxWindow* parent, const wxString& name) + : BitmapToggleButton(parent, name, wxID_ANY) + , m_on(this, "check_on", px_cnt) + , m_off(this, "check_off", px_cnt) + , m_on_disabled(this, "check_on_disabled", px_cnt) + , m_off_disabled(this, "check_off_disabled", px_cnt) + , m_on_focused(this, "check_on_focused", px_cnt) + , m_off_focused(this, "check_off_focused", px_cnt) +{ +#ifdef __WXOSX__ // State not fully implement on MacOS + Bind(wxEVT_SET_FOCUS, &CheckBox::updateBitmap, this); + Bind(wxEVT_KILL_FOCUS, &CheckBox::updateBitmap, this); + Bind(wxEVT_ENTER_WINDOW, &CheckBox::updateBitmap, this); + Bind(wxEVT_LEAVE_WINDOW, &CheckBox::updateBitmap, this); +#endif + + update(); +} + +void CheckBox::SetValue(bool value) +{ + wxBitmapToggleButton::SetValue(value); + update(); +} + +void CheckBox::Update() +{ + update(); +} + +void CheckBox::Rescale() +{ + update(); +} + +void CheckBox::update() +{ + const bool val = GetValue(); + const wxBitmapBundle& bmp = (val ? m_on : m_off).bmp(); + SetBitmap(bmp); + SetBitmapCurrent(bmp); + SetBitmapDisabled((val ? m_on_disabled : m_off_disabled).bmp()); +#ifdef __WXMSW__ + SetBitmapFocus((val ? m_on_focused : m_off_focused).bmp()); +#endif +#ifdef __WXOSX__ + wxCommandEvent e(wxEVT_UPDATE_UI); + updateBitmap(e); +#endif + + if (GetBitmapMargins().GetWidth() == 0 && !GetLabelText().IsEmpty()) + SetBitmapMargins(4, 0); + update_size(); +} + +#ifdef __WXMSW__ + +CheckBox::State CheckBox::GetNormalState() const +{ + return State_Normal; +} + +#endif + +bool CheckBox::Enable(bool enable) +{ + bool result = wxBitmapToggleButton::Enable(enable); + +#ifdef __WXOSX__ + if (result) { + m_disable = !enable; + wxCommandEvent e(wxEVT_ACTIVATE); + updateBitmap(e); + } +#endif + return result; +} + +#ifdef __WXOSX__ + +wxBitmap CheckBox::DoGetBitmap(State which) const +{ + if (m_disable) { + return wxBitmapToggleButton::DoGetBitmap(State_Disabled); + } + if (m_focus) { + return wxBitmapToggleButton::DoGetBitmap(State_Current); + } + return wxBitmapToggleButton::DoGetBitmap(which); +} + +void CheckBox::updateBitmap(wxEvent & evt) +{ + evt.Skip(); + if (evt.GetEventType() == wxEVT_ENTER_WINDOW) { + m_hover = true; + } else if (evt.GetEventType() == wxEVT_LEAVE_WINDOW) { + m_hover = false; + } else { + if (evt.GetEventType() == wxEVT_SET_FOCUS) { + m_focus = true; + } else if (evt.GetEventType() == wxEVT_KILL_FOCUS) { + m_focus = false; + } + wxMouseEvent e; + if (m_hover) + OnEnterWindow(e); + else + OnLeaveWindow(e); + } +} + +#endif diff --git a/src/slic3r/GUI/Widgets/CheckBox.hpp b/src/slic3r/GUI/Widgets/CheckBox.hpp new file mode 100644 index 0000000..727bbce --- /dev/null +++ b/src/slic3r/GUI/Widgets/CheckBox.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_GUI_CheckBox_hpp_ +#define slic3r_GUI_CheckBox_hpp_ + +#include "../wxExtensions.hpp" +#include "BitmapToggleButton.hpp" + +class CheckBox : public BitmapToggleButton +{ +public: + CheckBox(wxWindow* parent = NULL, const wxString& name = wxEmptyString); + +public: + void SetValue(bool value) override; + void Update() override; + bool Enable(bool enable = true) override; + + void Rescale(); + +protected: +#ifdef __WXMSW__ + virtual State GetNormalState() const wxOVERRIDE; +#endif + +#ifdef __WXOSX__ + virtual wxBitmap DoGetBitmap(State which) const wxOVERRIDE; + + void updateBitmap(wxEvent & evt); + + bool m_disable = false; + bool m_hover = false; + bool m_focus = false; +#endif + +private: + void update() override; + + ScalableBitmap m_on; + ScalableBitmap m_off; + ScalableBitmap m_on_disabled; + ScalableBitmap m_off_disabled; + ScalableBitmap m_on_focused; + ScalableBitmap m_off_focused; +}; + +#endif // !slic3r_GUI_CheckBox_hpp_ diff --git a/src/slic3r/GUI/Widgets/ComboBox.cpp b/src/slic3r/GUI/Widgets/ComboBox.cpp index 5600477..26cc3ab 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.cpp +++ b/src/slic3r/GUI/Widgets/ComboBox.cpp @@ -1,13 +1,13 @@ #include "ComboBox.hpp" -#include "Label.hpp" +#include "UIColors.hpp" #include +#include "../GUI_App.hpp" BEGIN_EVENT_TABLE(ComboBox, TextInput) EVT_LEFT_DOWN(ComboBox::mouseDown) -EVT_LEFT_DCLICK(ComboBox::mouseDown) -//EVT_MOUSEWHEEL(ComboBox::mouseWheelMoved) +EVT_MOUSEWHEEL(ComboBox::mouseWheelMoved) EVT_KEY_DOWN(ComboBox::keyDown) // catch paint events @@ -29,26 +29,20 @@ ComboBox::ComboBox(wxWindow * parent, long style) : drop(texts, icons) { - if (style & wxCB_READONLY) - style |= wxRIGHT; text_off = style & CB_NO_TEXT; TextInput::Create(parent, "", value, (style & CB_NO_DROP_ICON) ? "" : "drop_down", pos, size, style | wxTE_PROCESS_ENTER); - drop.Create(this, style & DD_STYLE_MASK); + drop.Create(this, style); - if (style & wxCB_READONLY) { + SetFont(Slic3r::GUI::wxGetApp().normal_font()); + if (style & wxCB_READONLY) GetTextCtrl()->Hide(); - TextInput::SetFont(Label::Body_14); - TextInput::SetBorderColor(StateColor(std::make_pair(0xDBDBDB, (int) StateColor::Disabled), - std::make_pair(0x009688, (int) StateColor::Hovered), - std::make_pair(0xDBDBDB, (int) StateColor::Normal))); - TextInput::SetBackgroundColor(StateColor(std::make_pair(0xF0F0F1, (int) StateColor::Disabled), - std::make_pair(0xEDFAF2, (int) StateColor::Focused), - std::make_pair(*wxWHITE, (int) StateColor::Normal))); - TextInput::SetLabelColor(StateColor(std::make_pair(0x909090, (int) StateColor::Disabled), - std::make_pair(0x262E30, (int) StateColor::Normal))); - } else { + else GetTextCtrl()->Bind(wxEVT_KEY_DOWN, &ComboBox::keyDown, this); + SetBorderColor(TextInput::GetBorderColor()); + if (parent) { + SetBackgroundColour(parent->GetBackgroundColour()); + SetForegroundColour(parent->GetForegroundColour()); } drop.Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &e) { SetSelection(e.GetInt()); @@ -61,10 +55,20 @@ ComboBox::ComboBox(wxWindow * parent, wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP); GetEventHandler()->ProcessEvent(e); }); +#ifndef _WIN32 + this->Bind(wxEVT_SYS_COLOUR_CHANGED, [this, parent](wxSysColourChangedEvent& event) { + event.Skip(); + SetBackgroundColour(parent->GetBackgroundColour()); + SetForegroundColour(parent->GetForegroundColour()); + }); +#endif for (int i = 0; i < n; ++i) Append(choices[i]); } -int ComboBox::GetSelection() const { return drop.GetSelection(); } +int ComboBox::GetSelection() const +{ + return drop.GetSelection(); +} void ComboBox::SetSelection(int n) { @@ -74,14 +78,11 @@ void ComboBox::SetSelection(int n) SetIcon(icons[drop.selection]); } -void ComboBox::SelectAndNotify(int n) { - SetSelection(n); - sendComboBoxEvent(); -} void ComboBox::Rescale() { + SetFont(Slic3r::GUI::wxGetApp().normal_font()); TextInput::Rescale(); drop.Rescale(); } @@ -127,19 +128,52 @@ wxString ComboBox::GetTextLabel() const bool ComboBox::SetFont(wxFont const& font) { + const bool set_drop_font = drop.SetFont(font); if (GetTextCtrl() && GetTextCtrl()->IsShown()) - return GetTextCtrl()->SetFont(font); - else - return TextInput::SetFont(font); + return GetTextCtrl()->SetFont(font) && set_drop_font; + return TextInput::SetFont(font) && set_drop_font; } -int ComboBox::Append(const wxString &item, const wxBitmap &bitmap) +bool ComboBox::SetBackgroundColour(const wxColour& colour) +{ + TextInput::SetBackgroundColour(colour); + + drop.SetBackgroundColour(colour); + StateColor selector_colors( std::make_pair(clr_background_focused, (int)StateColor::Checked), + Slic3r::GUI::wxGetApp().dark_mode() ? + std::make_pair(clr_background_disabled_dark, (int)StateColor::Disabled) : + std::make_pair(clr_background_disabled_light, (int)StateColor::Disabled), + Slic3r::GUI::wxGetApp().dark_mode() ? + std::make_pair(clr_background_normal_dark, (int)StateColor::Normal) : + std::make_pair(clr_background_normal_light, (int)StateColor::Normal)); + drop.SetSelectorBackgroundColor(selector_colors); + + return true; +} + +bool ComboBox::SetForegroundColour(const wxColour& colour) +{ + TextInput::SetForegroundColour(colour); + + drop.SetTextColor(TextInput::GetTextColor()); + + return true; +} + +void ComboBox::SetBorderColor(StateColor const& color) +{ + TextInput::SetBorderColor(color); + drop.SetBorderColor(color); + drop.SetSelectorBorderColor(color); +} + +int ComboBox::Append(const wxString &item, const wxBitmapBundle &bitmap) { return Append(item, bitmap, nullptr); } int ComboBox::Append(const wxString &item, - const wxBitmap &bitmap, + const wxBitmapBundle &bitmap, void * clientData) { texts.push_back(item); @@ -147,7 +181,23 @@ int ComboBox::Append(const wxString &item, datas.push_back(clientData); types.push_back(wxClientData_None); drop.Invalidate(); - return texts.size() - 1; + return int(texts.size()) - 1; +} + +int ComboBox::Insert(const wxString& item, + const wxBitmapBundle& bitmap, + unsigned int pos) +{ + return Insert(item, bitmap, pos, nullptr); +} + +int ComboBox::Insert(const wxString& item, const wxBitmapBundle& bitmap, + unsigned int pos, void* clientData) +{ + const int n = wxItemContainer::Insert(item, pos, clientData); + if (n != wxNOT_FOUND) + icons[n] = bitmap; + return n; } void ComboBox::DoClear() @@ -157,6 +207,8 @@ void ComboBox::DoClear() datas.clear(); types.clear(); drop.Invalidate(true); + if (GetTextCtrl()->IsShown() || text_off) + GetTextCtrl()->Clear(); } void ComboBox::DoDeleteOneItem(unsigned int pos) @@ -166,7 +218,9 @@ void ComboBox::DoDeleteOneItem(unsigned int pos) icons.erase(icons.begin() + pos); datas.erase(datas.begin() + pos); types.erase(types.begin() + pos); + const int selection = drop.GetSelection(); drop.Invalidate(true); + drop.SetSelection(selection); } unsigned int ComboBox::GetCount() const { return texts.size(); } @@ -181,16 +235,17 @@ void ComboBox::SetString(unsigned int n, wxString const &value) if (n >= texts.size()) return; texts[n] = value; drop.Invalidate(); - if (n == drop.GetSelection()) SetLabel(value); + if (int(n) == drop.GetSelection()) SetLabel(value); } -wxBitmap ComboBox::GetItemBitmap(unsigned int n) { return icons[n]; } - -void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap) +wxBitmap ComboBox::GetItemBitmap(unsigned int n) { - if (n >= texts.size()) return; - icons[n] = bitmap; - drop.Invalidate(); + return icons[n].GetBitmapFor(m_parent); +} + +void ComboBox::OnKeyDown(wxKeyEvent &event) +{ + keyDown(event); } int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items, @@ -199,15 +254,17 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items, wxClientDataType type) { if (pos > texts.size()) return -1; - for (int i = 0; i < items.GetCount(); ++i) { + for (size_t i = 0; i < items.GetCount(); ++i) { texts.insert(texts.begin() + pos, items[i]); icons.insert(icons.begin() + pos, wxNullBitmap); datas.insert(datas.begin() + pos, clientData ? clientData[i] : NULL); types.insert(types.begin() + pos, type); ++pos; } + const int selection = drop.GetSelection(); drop.Invalidate(true); - return pos - 1; + drop.SetSelection(selection); + return int(pos) - 1; } void *ComboBox::DoGetItemClientData(unsigned int n) const { return n < texts.size() ? datas[n] : NULL; } @@ -226,7 +283,7 @@ void ComboBox::mouseDown(wxMouseEvent &event) } else if (drop.HasDismissLongTime()) { drop.autoPosition(); drop_down = true; - drop.Popup(&drop); + drop.Popup(); wxCommandEvent e(wxEVT_COMBOBOX_DROPDOWN); GetEventHandler()->ProcessEvent(e); } @@ -236,7 +293,7 @@ void ComboBox::mouseWheelMoved(wxMouseEvent &event) { event.Skip(); if (drop_down) return; - auto delta = event.GetWheelRotation() < 0 ? 1 : -1; + auto delta = ((event.GetWheelRotation() < 0) == event.IsWheelInverted()) ? -1 : 1; unsigned int n = GetSelection() + delta; if (n < GetCount()) { SetSelection((int) n); @@ -246,11 +303,16 @@ void ComboBox::mouseWheelMoved(wxMouseEvent &event) void ComboBox::keyDown(wxKeyEvent& event) { - switch (event.GetKeyCode()) { + int key_code = event.GetKeyCode(); + switch (key_code) { case WXK_RETURN: - case WXK_SPACE: if (drop_down) { drop.DismissAndNotify(); + wxCommandEvent e(wxEVT_COMBOBOX); + e.SetEventObject(this); + e.SetId(GetId()); + e.SetInt(GetSelection()); + GetEventHandler()->ProcessEvent(e); } else if (drop.HasDismissLongTime()) { drop.autoPosition(); drop_down = true; @@ -259,27 +321,55 @@ void ComboBox::keyDown(wxKeyEvent& event) GetEventHandler()->ProcessEvent(e); } break; - case WXK_UP: - case WXK_DOWN: - case WXK_LEFT: - case WXK_RIGHT: - if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) { + case WXK_UP: { + if (GetSelection() > 0) SetSelection(GetSelection() - 1); - } else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < texts.size()) { + break; + } + case WXK_DOWN: { + if (GetSelection() + 1 < int(texts.size())) SetSelection(GetSelection() + 1); - } else { + break; + } + case WXK_LEFT: { + if (HasFlag(wxCB_READONLY)) { + if(GetSelection() > 0) + SetSelection(GetSelection() - 1); break; } - sendComboBoxEvent(); + const auto pos = GetTextCtrl()->GetInsertionPoint(); + if(pos > 0) + GetTextCtrl()->SetInsertionPoint(pos - 1); break; + } + case WXK_RIGHT: { + if (HasFlag(wxCB_READONLY)) { + if (GetSelection() + 1 < int(texts.size())) + SetSelection(GetSelection() + 1); + break; + } + const size_t pos = size_t(GetTextCtrl()->GetInsertionPoint()); + if (pos < GetLabel().Length()) + GetTextCtrl()->SetInsertionPoint(pos + 1); + break; + } case WXK_TAB: HandleAsNavigationKey(event); break; - default: + default: { + if (drop.IsShown() && HasFlag(wxCB_READONLY)) { + for (size_t n = 0; n < texts.size(); n++) { + if (texts[n].StartsWith(wxString(static_cast(key_code)))) { + SetSelection(int(n)); + break; + } + } + } event.Skip(); break; } } +} void ComboBox::OnEdit() { diff --git a/src/slic3r/GUI/Widgets/ComboBox.hpp b/src/slic3r/GUI/Widgets/ComboBox.hpp index bac3523..a719128 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.hpp +++ b/src/slic3r/GUI/Widgets/ComboBox.hpp @@ -4,13 +4,13 @@ #include "TextInput.hpp" #include "DropDown.hpp" -#define CB_NO_DROP_ICON DD_NO_CHECK_ICON +#define CB_NO_DROP_ICON DD_NO_DROP_ICON #define CB_NO_TEXT DD_NO_TEXT class ComboBox : public wxWindowWithItems { std::vector texts; - std::vector icons; + std::vector icons; std::vector datas; std::vector types; @@ -30,12 +30,19 @@ public: DropDown & GetDropDown() { return drop; } - virtual bool SetFont(wxFont const & font) override; + bool SetFont(wxFont const & font) override; -public: - int Append(const wxString &item, const wxBitmap &bitmap = wxNullBitmap); + bool SetBackgroundColour(const wxColour& colour) override; + bool SetForegroundColour(const wxColour& colour) override; - int Append(const wxString &item, const wxBitmap &bitmap, void *clientData); + void SetBorderColor(StateColor const& color); + + int Append(const wxString &item, const wxBitmapBundle &bitmap = wxNullBitmap); + int Append(const wxString &item, const wxBitmapBundle &bitmap, void *clientData); + + int Insert(const wxString& item, const wxBitmapBundle& bitmap, unsigned int pos); + int Insert(const wxString& item, const wxBitmapBundle& bitmap, + unsigned int pos, void* clientData); unsigned int GetCount() const override; @@ -43,7 +50,6 @@ public: void SetSelection(int n) override; - void SelectAndNotify(int n); virtual void Rescale() override; @@ -60,14 +66,14 @@ public: void SetString(unsigned int n, wxString const &value) override; wxBitmap GetItemBitmap(unsigned int n); - void SetItemBitmap(unsigned int n, wxBitmap const &bitmap); + void OnKeyDown(wxKeyEvent& event); protected: virtual int DoInsertItems(const wxArrayStringsAdapter &items, unsigned int pos, void ** clientData, wxClientDataType type) override; - virtual void DoClear() override; + void DoClear() override; void DoDeleteOneItem(unsigned int pos) override; @@ -76,7 +82,6 @@ protected: void OnEdit() override; - void sendComboBoxEvent(); #ifdef __WIN32__ WXLRESULT MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) override; @@ -89,6 +94,7 @@ private: void mouseWheelMoved(wxMouseEvent &event); void keyDown(wxKeyEvent &event); + void sendComboBoxEvent(); DECLARE_EVENT_TABLE() }; diff --git a/src/slic3r/GUI/Widgets/DropDown.cpp b/src/slic3r/GUI/Widgets/DropDown.cpp index 0fd1e20..f59b91f 100644 --- a/src/slic3r/GUI/Widgets/DropDown.cpp +++ b/src/slic3r/GUI/Widgets/DropDown.cpp @@ -1,7 +1,16 @@ #include "DropDown.hpp" -#include "Label.hpp" +#include "ComboBox.hpp" +#include "../GUI_App.hpp" +#include "../OptionsGroup.hpp" #include +#include +#include +#include +#include +#include + +#include #ifdef __WXGTK__ #include @@ -9,7 +18,7 @@ wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent); -BEGIN_EVENT_TABLE(DropDown, PopupWindow) +BEGIN_EVENT_TABLE(DropDown, wxPopupTransientWindow) EVT_LEFT_DOWN(DropDown::mouseDown) EVT_LEFT_UP(DropDown::mouseReleased) @@ -29,13 +38,14 @@ END_EVENT_TABLE() */ DropDown::DropDown(std::vector &texts, - std::vector &icons) + std::vector &icons) : texts(texts) , icons(icons) + , radius(Slic3r::GUI::wxGetApp().suppress_round_corners() ? 0 : 5) , state_handler(this) - , border_color(0xDBDBDB) , text_color(0x363636) - , selector_border_color(std::make_pair(0x009688, (int) StateColor::Hovered), + , border_color(0xDBDBDB) + , selector_border_color(std::make_pair(0x00AE42, (int) StateColor::Hovered), std::make_pair(*wxWHITE, (int) StateColor::Normal)) , selector_background_color(std::make_pair(0xEDFAF2, (int) StateColor::Checked), std::make_pair(*wxWHITE, (int) StateColor::Normal)) @@ -44,29 +54,76 @@ DropDown::DropDown(std::vector &texts, DropDown::DropDown(wxWindow * parent, std::vector &texts, - std::vector &icons, + std::vector &icons, long style) : DropDown(texts, icons) { Create(parent, style); } +#ifdef __WXGTK__ +static gint gtk_popup_key_press (GtkWidget *widget, GdkEvent *gdk_event, wxPopupWindow* win ) +{ + // Ignore events sent out before we connected to the signal + if (win->m_time >= ((GdkEventKey*)gdk_event)->time) + return FALSE; + + GtkWidget *child = gtk_get_event_widget (gdk_event); + + /* We don't ask for button press events on the grab widget, so + * if an event is reported directly to the grab widget, it must + * be on a window outside the application (and thus we remove + * the popup window). Otherwise, we check if the widget is a child + * of the grab widget, and only remove the popup window if it + * is not. */ + if (child != widget) { + while (child) { + if (child == widget) + return FALSE; + child = gtk_widget_get_parent(child); + } + } + + gchar* keyval = gdk_keyval_name(((GdkEventKey*)gdk_event)->keyval); + const long keyCode = strcmp(keyval, "Up") == 0 ? WXK_UP : + strcmp(keyval, "Down") == 0 ? WXK_DOWN : + strcmp(keyval, "Left") == 0 ? WXK_LEFT : + strcmp(keyval, "Right") == 0 ? WXK_RIGHT : + strcmp(keyval, "Return") == 0 ? WXK_RETURN : WXK_NONE; + + if (keyCode != WXK_NONE) { + wxKeyEvent event( wxEVT_KEY_DOWN, win->GetId()); + event.m_keyCode = keyCode; + event.SetEventObject( win ); + (void)win->HandleWindowEvent( event ); + } + + return TRUE; +} +#endif void DropDown::Create(wxWindow * parent, long style) { - PopupWindow::Create(parent, wxPU_CONTAINS_CONTROLS); - SetBackgroundStyle(wxBG_STYLE_PAINT); - SetBackgroundColour(*wxWHITE); + wxPopupTransientWindow::Create(parent); +#ifdef __WXGTK__ + g_signal_connect (m_widget, "key_press_event", G_CALLBACK (gtk_popup_key_press), this); + + Bind(wxEVT_KEY_DOWN, [parent](wxKeyEvent &e) { + if (ComboBox* cb = dynamic_cast(parent)) + cb->OnKeyDown(e); + }); +#endif + + if (!wxOSX) SetBackgroundStyle(wxBG_STYLE_PAINT); state_handler.attach({&border_color, &text_color, &selector_border_color, &selector_background_color}); state_handler.update_binds(); - if ((style & DD_NO_CHECK_ICON) == 0) + if (!(style & DD_NO_CHECK_ICON)) check_bitmap = ScalableBitmap(this, "checked", 16); text_off = style & DD_NO_TEXT; - // BBS set default font - SetFont(Label::Body_14); + SetFont(parent->GetFont()); #ifdef __WXOSX__ - // PopupWindow releases mouse on idle, which may cause various problems, + // wxPopupTransientWindow releases mouse on idle, which may cause various problems, // such as losting mouse move, and dismissing soon on first LEFT_DOWN event. Bind(wxEVT_IDLE, [] (wxIdleEvent & evt) {}); #endif @@ -84,11 +141,12 @@ void DropDown::Invalidate(bool clear) void DropDown::SetSelection(int n) { - assert(n < (int) texts.size()); if (n >= (int) texts.size()) n = -1; if (selection == n) return; selection = n; + if (IsShown()) + autoPosition(); paintNow(); } @@ -103,9 +161,9 @@ void DropDown::SetValue(const wxString &value) selection = i == texts.end() ? -1 : std::distance(texts.begin(), i); } -void DropDown::SetCornerRadius(double radius) +void DropDown::SetCornerRadius(double radius_in) { - this->radius = radius; + radius = radius_in; paintNow(); } @@ -157,7 +215,7 @@ bool DropDown::HasDismissLongTime() { auto now = boost::posix_time::microsec_clock::universal_time(); return !IsShown() && - (now - dismissTime).total_milliseconds() >= 20; + (now - dismissTime).total_milliseconds() >= 200; } void DropDown::paintEvent(wxPaintEvent& evt) @@ -183,15 +241,35 @@ void DropDown::paintNow() Refresh(); } -static wxSize GetBmpSize(wxBitmap & bmp) +void DropDown::SetTransparentBG(wxDC& dc, wxWindow* win) { -#ifdef __APPLE__ - return bmp.GetScaledSize(); + const wxSize size = win->GetSize(); + const wxPoint screen_pos = win->GetScreenPosition(); + wxScreenDC screen_dc; + +#ifdef __WXMSW__ + // Draw screen_dc to dc for transparent background + dc.Blit(0, 0, size.x, size.y, &screen_dc, screen_pos.x, screen_pos.y); #else - return bmp.GetSize(); -#endif + // See https://forums.wxwidgets.org/viewtopic.php?f=1&t=49318 + wxClientDC client_dc(win); + client_dc.Blit(0, 0, size.x, size.y, &screen_dc, screen_pos.x, screen_pos.y); + + wxBitmap bmp(size.x, size.y); + wxMemoryDC mem_dc(bmp); + mem_dc.Blit(0, 0, size.x, size.y, &client_dc, 0, 0); + mem_dc.SelectObject(wxNullBitmap); + dc.DrawBitmap(bmp, 0, 0); +#endif //__WXMSW__ } +constexpr int slider_width = 12; +#ifdef __WXOSX__ +constexpr int slider_step = 1; +#else +constexpr int slider_step = 5; +#endif +constexpr int items_padding = 2; /* * Here we do the actual rendering. I put it in a separate * method so that it can work no matter what type of DC @@ -201,25 +279,38 @@ void DropDown::render(wxDC &dc) { if (texts.size() == 0) return; int states = state_handler.states(); + const wxSize size = GetSize(); + if (radius > 0. && !wxOSX) + SetTransparentBG(dc, this); dc.SetPen(wxPen(border_color.colorForStates(states))); - dc.SetBrush(wxBrush(StateColor::darkModeColorFor(GetBackgroundColour()))); + dc.SetBrush(wxBrush(GetBackgroundColour())); // if (GetWindowStyle() & wxBORDER_NONE) // dc.SetPen(wxNullPen); + const bool is_retina = wxOSX && dc.GetContentScaleFactor() > 1.0; + + wxRect rc(0, 0, size.x, size.y); + // On Retina displays all controls are cut on 1px + if (is_retina) + rc.x = rc.y = 1; // draw background - wxSize size = GetSize(); - if (radius == 0) - dc.DrawRectangle(0, 0, size.x, size.y); + if (radius == 0.0 || wxOSX) + dc.DrawRectangle(rc); else - dc.DrawRoundedRectangle(0, 0, size.x, size.y, radius); + dc.DrawRoundedRectangle(rc, radius); // draw hover rectangle wxRect rcContent = {{0, offset.y}, rowSize}; + const int text_size = int(texts.size()); + + const bool has_bar = rowSize.y * text_size > size.y; + if (has_bar) + rcContent.width -= slider_width; if (hover_item >= 0 && (states & StateColor::Hovered)) { rcContent.y += rowSize.y * hover_item; if (rcContent.GetBottom() > 0 && rcContent.y < size.y) { if (selection == hover_item) - dc.SetBrush(wxBrush(selector_background_color.colorForStates(states | StateColor::Checked))); + dc.SetBrush(wxBrush(selector_background_color.colorForStates(StateColor::Disabled))); dc.SetPen(wxPen(selector_border_color.colorForStates(states))); rcContent.Deflate(4, 1); dc.DrawRectangle(rcContent); @@ -231,11 +322,15 @@ void DropDown::render(wxDC &dc) if (selection >= 0 && (selection != hover_item || (states & StateColor::Hovered) == 0)) { rcContent.y += rowSize.y * selection; if (rcContent.GetBottom() > 0 && rcContent.y < size.y) { - dc.SetBrush(wxBrush(selector_background_color.colorForStates(states | StateColor::Checked))); + dc.SetBrush(wxBrush(selector_background_color.colorForStates(StateColor::Disabled))); dc.SetPen(wxPen(selector_background_color.colorForStates(states))); rcContent.Deflate(4, 1); + if (is_retina) + rc.y += 1; dc.DrawRectangle(rcContent); rcContent.Inflate(4, 1); + if (is_retina) + rc.y -= 1; } rcContent.y = offset.y; } @@ -246,20 +341,19 @@ void DropDown::render(wxDC &dc) } // draw position bar - if (rowSize.y * texts.size() > size.y) { - int height = rowSize.y * texts.size(); - wxRect rect = {size.x - 6, -offset.y * size.y / height, 4, - size.y * size.y / height}; + if (has_bar) { + int height = rowSize.y * text_size; + wxRect rect = {size.x - slider_width - 2, -offset.y * size.y / height + 2, slider_width, + size.y * size.y / height - 3}; dc.SetPen(wxPen(border_color.defaultColor())); - dc.SetBrush(wxBrush(*wxLIGHT_GREY)); + dc.SetBrush(wxBrush(selector_background_color.colorForStates(states | StateColor::Checked))); dc.DrawRoundedRectangle(rect, 2); - rcContent.width -= 6; } // draw check icon rcContent.x += 5; rcContent.width -= 5; - if (check_bitmap.get_bitmap().IsOk()) { + if (check_bitmap.bmp().IsOk()) { auto szBmp = check_bitmap.GetSize(); if (selection >= 0) { wxPoint pt = rcContent.GetLeftTop(); @@ -273,7 +367,7 @@ void DropDown::render(wxDC &dc) } // draw texts & icons dc.SetTextForeground(text_color.colorForStates(states)); - for (int i = 0; i < texts.size(); ++i) { + for (size_t i = 0; i < texts.size(); ++i) { if (rcContent.GetBottom() < 0) { rcContent.y += rowSize.y; continue; @@ -281,18 +375,26 @@ void DropDown::render(wxDC &dc) if (rcContent.y > size.y) break; wxPoint pt = rcContent.GetLeftTop(); auto & icon = icons[i]; - auto size2 = GetBmpSize(icon); + const wxSize pref_icon_sz = get_preferred_size(icon, m_parent); if (iconSize.x > 0) { if (icon.IsOk()) { - pt.y += (rcContent.height - size2.y) / 2; - dc.DrawBitmap(icon, pt); + pt.y += (rcContent.height - pref_icon_sz.y) / 2; +#ifdef __WXGTK3__ + dc.DrawBitmap(icon.GetBitmap(pref_icon_sz), pt); +#else + dc.DrawBitmap(icon.GetBitmapFor(m_parent), pt); +#endif } pt.x += iconSize.x + 5; pt.y = rcContent.y; } else if (icon.IsOk()) { - pt.y += (rcContent.height - size2.y) / 2; - dc.DrawBitmap(icon, pt); - pt.x += size2.x + 5; + pt.y += (rcContent.height - pref_icon_sz.y) / 2; +#ifdef __WXGTK3__ + dc.DrawBitmap(icon.GetBitmap(pref_icon_sz), pt); +#else + dc.DrawBitmap(icon.GetBitmapFor(m_parent), pt); +#endif + pt.x += pref_icon_sz.GetWidth() + 5; pt.y = rcContent.y; } auto text = texts[i]; @@ -319,7 +421,7 @@ void DropDown::messureSize() for (size_t i = 0; i < texts.size(); ++i) { wxSize size1 = text_off ? wxSize() : dc.GetMultiLineTextExtent(texts[i]); if (icons[i].IsOk()) { - wxSize size2 = GetBmpSize(icons[i]); + wxSize size2 = get_preferred_size(icons[i], m_parent); if (size2.x > iconSize.x) iconSize = size2; if (!align_icon) { size1.x += size2.x + (text_off ? 0 : 5); @@ -330,13 +432,13 @@ void DropDown::messureSize() if (!align_icon) iconSize.x = 0; wxSize szContent = textSize; szContent.x += 10; - if (check_bitmap.get_bitmap().IsOk()) { + if (check_bitmap.bmp().IsOk()) { auto szBmp = check_bitmap.GetSize(); szContent.x += szBmp.x + 5; } if (iconSize.x > 0) szContent.x += iconSize.x + (text_off ? 0 : 5); if (iconSize.y > szContent.y) szContent.y = iconSize.y; - szContent.y += 10; + szContent.y += items_padding; if (texts.size() > 15) szContent.x += 6; if (GetParent()) { auto x = GetParent()->GetSize().x; @@ -378,15 +480,15 @@ void DropDown::autoPosition() if (use_content_width && texts.size() <= 15) size.x += 6; size.y = drect.GetBottom() - GetPosition().y - 10; wxWindow::SetSize(size); + } + } if (selection >= 0) { if (offset.y + rowSize.y * (selection + 1) > size.y) - offset.y = size.y - rowSize.y * (selection + 1); + offset.y = size.y - rowSize.y * (selection + 3); else if (offset.y + rowSize.y * selection < 0) offset.y = -rowSize.y * selection; } } - } -} void DropDown::mouseDown(wxMouseEvent& event) { @@ -395,6 +497,11 @@ void DropDown::mouseDown(wxMouseEvent& event) return; // force calc hover item again mouseMove(event); + const wxSize size = GetSize(); + const int height = rowSize.y * int(texts.size()); + const wxRect rect = { size.x - slider_width, -offset.y * size.y / height, slider_width - 2, + size.y * size.y / height }; + slider_grabbed = rect.Contains(event.GetPosition()); pressedDown = true; CaptureMouse(); dragStart = event.GetPosition(); @@ -405,9 +512,14 @@ void DropDown::mouseReleased(wxMouseEvent& event) if (pressedDown) { dragStart = wxPoint(); pressedDown = false; + slider_grabbed = false; if (HasCapture()) ReleaseMouse(); if (hover_item >= 0) { // not moved +#ifdef __WXOSX__ + // To avoid cases, when some dialog appears after item selection, but DropDown is still shown + Hide(); +#endif sendDropDownEvent(); DismissAndNotify(); } @@ -423,14 +535,17 @@ void DropDown::mouseCaptureLost(wxMouseCaptureLostEvent &event) void DropDown::mouseMove(wxMouseEvent &event) { wxPoint pt = event.GetPosition(); + int text_size = int(texts.size()); if (pressedDown) { - wxPoint pt2 = offset + pt - dragStart; - wxSize size = GetSize(); + const int height = rowSize.y * text_size; + const int y_step = slider_grabbed ? -height / GetSize().y : 1; + + wxPoint pt2 = offset + (pt - dragStart)*y_step; dragStart = pt; if (pt2.y > 0) pt2.y = 0; - else if (pt2.y + rowSize.y * int(texts.size()) < size.y) - pt2.y = size.y - rowSize.y * int(texts.size()); + else if (pt2.y + rowSize.y * text_size < GetSize().y) + pt2.y = GetSize().y - rowSize.y * text_size; if (pt2.y != offset.y) { offset = pt2; hover_item = -1; // moved @@ -440,7 +555,7 @@ void DropDown::mouseMove(wxMouseEvent &event) } if (!pressedDown || hover_item >= 0) { int hover = (pt.y - offset.y) / rowSize.y; - if (hover >= (int) texts.size()) hover = -1; + if (hover >= text_size || slider_grabbed) hover = -1; if (hover == hover_item) return; hover_item = hover; if (hover >= 0) @@ -451,20 +566,22 @@ void DropDown::mouseMove(wxMouseEvent &event) void DropDown::mouseWheelMoved(wxMouseEvent &event) { - auto delta = event.GetWheelRotation(); - wxSize size = GetSize(); - wxPoint pt2 = offset + wxPoint{0, delta}; + if (event.GetWheelRotation() == 0) + return; + auto delta = event.GetWheelRotation() > 0 ? rowSize.y : -rowSize.y; + wxPoint pt2 = offset + wxPoint{0, slider_step * delta}; + int text_size = int(texts.size()); if (pt2.y > 0) pt2.y = 0; - else if (pt2.y + rowSize.y * int(texts.size()) < size.y) - pt2.y = size.y - rowSize.y * int(texts.size()); + else if (pt2.y + rowSize.y * text_size < GetSize().y) + pt2.y = GetSize().y - rowSize.y * text_size; if (pt2.y != offset.y) { offset = pt2; } else { return; } int hover = (event.GetPosition().y - offset.y) / rowSize.y; - if (hover >= (int) texts.size()) hover = -1; + if (hover >= text_size) hover = -1; if (hover != hover_item) { hover_item = hover; if (hover >= 0) SetToolTip(texts[hover]); diff --git a/src/slic3r/GUI/Widgets/DropDown.hpp b/src/slic3r/GUI/Widgets/DropDown.hpp index 3b15c14..9fe1a33 100644 --- a/src/slic3r/GUI/Widgets/DropDown.hpp +++ b/src/slic3r/GUI/Widgets/DropDown.hpp @@ -2,31 +2,28 @@ #define slic3r_GUI_DropDown_hpp_ #include +#include + +#include #include "../wxExtensions.hpp" #include "StateHandler.hpp" -#include "PopupWindow.hpp" -//B35 -#if defined __linux__ -#include -#include "wx/dcbuffer.h" -#include "wx/display.h" -#endif #define DD_NO_CHECK_ICON 0x0001 -#define DD_NO_TEXT 0x0002 -#define DD_STYLE_MASK 0x0003 +#define DD_NO_DROP_ICON 0x0002 +#define DD_NO_TEXT 0x0004 +#define DD_STYLE_MASK 0x0008 wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent); -class DropDown : public PopupWindow +class DropDown : public wxPopupTransientWindow { std::vector & texts; - std::vector & icons; + std::vector & icons; bool need_sync = false; int selection = -1; int hover_item = -1; - double radius = 0; + double radius; bool use_content_width = false; bool align_icon = false; bool text_off = false; @@ -43,23 +40,23 @@ class DropDown : public PopupWindow ScalableBitmap check_bitmap; bool pressedDown = false; + bool slider_grabbed = false; boost::posix_time::ptime dismissTime; wxPoint offset; // x not used wxPoint dragStart; public: DropDown(std::vector &texts, - std::vector &icons); + std::vector &icons); DropDown(wxWindow * parent, std::vector &texts, - std::vector &icons, + std::vector &icons, long style = 0); void Create(wxWindow * parent, long style = 0); -public: void Invalidate(bool clear = false); int GetSelection() const { return selection; } @@ -69,7 +66,6 @@ public: wxString GetValue() const; void SetValue(const wxString &value); -public: void SetCornerRadius(double radius); void SetBorderColor(StateColor const & color); @@ -84,11 +80,11 @@ public: void SetAlignIcon(bool align); -public: void Rescale(); bool HasDismissLongTime(); + static void SetTransparentBG(wxDC& dc, wxWindow* win); protected: void OnDismiss() override; diff --git a/src/slic3r/GUI/Widgets/Label.cpp b/src/slic3r/GUI/Widgets/Label.cpp index 8d23033..35252f3 100644 --- a/src/slic3r/GUI/Widgets/Label.cpp +++ b/src/slic3r/GUI/Widgets/Label.cpp @@ -1,7 +1,7 @@ -#include "libslic3r/Utils.hpp" #include "Label.hpp" #include "StaticBox.hpp" +#include wxFont Label::sysFont(int size, bool bold) { //#ifdef __linux__ @@ -29,7 +29,6 @@ wxFont Label::Head_15; wxFont Label::Head_14; wxFont Label::Head_13; wxFont Label::Head_12; -wxFont Label::Head_11; wxFont Label::Head_10; wxFont Label::Body_16; @@ -43,17 +42,6 @@ wxFont Label::Body_9; void Label::initSysFont() { -#ifdef __linux__ - const std::string& resource_path = Slic3r::resources_dir(); - wxString font_path = wxString::FromUTF8(resource_path+"/fonts/HarmonyOS_Sans_SC_Bold.ttf"); - bool result = wxFont::AddPrivateFont(font_path); - //BOOST_LOG_TRIVIAL(info) << boost::format("add font of HarmonyOS_Sans_SC_Bold returns %1%")%result; - printf("add font of HarmonyOS_Sans_SC_Bold returns %d\n", result); - font_path = wxString::FromUTF8(resource_path+"/fonts/HarmonyOS_Sans_SC_Regular.ttf"); - result = wxFont::AddPrivateFont(font_path); - //BOOST_LOG_TRIVIAL(info) << boost::format("add font of HarmonyOS_Sans_SC_Regular returns %1%")%result; - printf("add font of HarmonyOS_Sans_SC_Regular returns %d\n", result); -#endif Head_24 = Label::sysFont(24, true); Head_20 = Label::sysFont(20, true); @@ -63,7 +51,6 @@ void Label::initSysFont() Head_14 = Label::sysFont(14, true); Head_13 = Label::sysFont(13, true); Head_12 = Label::sysFont(12, true); - Head_11 = Label::sysFont(11, true); Head_10 = Label::sysFont(10, true); Body_16 = Label::sysFont(16, false); @@ -76,126 +63,6 @@ void Label::initSysFont() Body_9 = Label::sysFont(9, false); } -class WXDLLIMPEXP_CORE wxTextWrapper2 -{ -public: - wxTextWrapper2() { m_eol = false; } - - // win is used for getting the font, text is the text to wrap, width is the - // max line width or -1 to disable wrapping - void Wrap(wxWindow *win, const wxString &text, int widthMax) - { - const wxClientDC dc(win); - - const wxArrayString ls = wxSplit(text, '\n', '\0'); - for (wxArrayString::const_iterator i = ls.begin(); i != ls.end(); ++i) { - wxString line = *i; - - if (i != ls.begin()) { - // Do this even if the line is empty, except if it's the first one. - OnNewLine(); - } - - // Is this a special case when wrapping is disabled? - if (widthMax < 0) { - DoOutputLine(line); - continue; - } - - for (bool newLine = false; !line.empty(); newLine = true) { - if (newLine) OnNewLine(); - - wxArrayInt widths; - dc.GetPartialTextExtents(line, widths); - - const size_t posEnd = std::lower_bound(widths.begin(), widths.end(), widthMax) - widths.begin(); - - // Does the entire remaining line fit? - if (posEnd == line.length()) { - DoOutputLine(line); - break; - } - - // Find the last word to chop off. - size_t lastSpace = posEnd; - while (lastSpace > 0) { - auto c = line[lastSpace]; - if (c == ' ') - break; - if (c > 0x4E00) { - if (lastSpace != posEnd) - ++lastSpace; - break; - } - --lastSpace; - } - if (lastSpace == 0) { - // No spaces, so can't wrap. - lastSpace = posEnd; - } - - // Output the part that fits. - DoOutputLine(line.substr(0, lastSpace)); - - // And redo the layout with the rest. - if (line[lastSpace] == ' ') ++lastSpace; - line = line.substr(lastSpace); - } - } - } - - // we don't need it, but just to avoid compiler warnings - virtual ~wxTextWrapper2() {} - -protected: - // line may be empty - virtual void OnOutputLine(const wxString &line) = 0; - - // called at the start of every new line (except the very first one) - virtual void OnNewLine() {} - -private: - // call OnOutputLine() and set m_eol to true - void DoOutputLine(const wxString &line) - { - OnOutputLine(line); - - m_eol = true; - } - - // this function is a destructive inspector: when it returns true it also - // resets the flag to false so calling it again wouldn't return true any - // more - bool IsStartOfNewLine() - { - if (!m_eol) return false; - - m_eol = false; - - return true; - } - - bool m_eol; -}; - -class wxLabelWrapper2 : public wxTextWrapper2 -{ -public: - void WrapLabel(wxWindow *text, int widthMax) - { - m_text.clear(); - Wrap(text, text->GetLabel(), widthMax); - text->SetLabel(m_text); - } - -protected: - virtual void OnOutputLine(const wxString &line) wxOVERRIDE { m_text += line; } - - virtual void OnNewLine() wxOVERRIDE { m_text += wxT('\n'); } - -private: - wxString m_text; -}; wxSize Label::split_lines(wxDC &dc, int width, const wxString &text, wxString &multiline_text) @@ -231,27 +98,18 @@ Label::Label(wxWindow *parent, wxFont const &font, wxString const &text, long st { this->font = font; SetFont(font); - SetForegroundColour(wxColour("#262E30")); SetBackgroundColour(StaticBox::GetParentBackgroundColor(parent)); - SetForegroundColour("#262E30"); - if (style & LB_PROPAGATE_MOUSE_EVENT) { - for (auto evt : { - wxEVT_LEFT_UP, wxEVT_LEFT_DOWN}) - Bind(evt, [this] (auto & e) { GetParent()->GetEventHandler()->ProcessEventLocally(e); }); - }; + Bind(wxEVT_ENTER_WINDOW, [this](auto &e) { + if (GetWindowStyle() & LB_HYPERLINK) { + SetFont(this->font.Underlined()); + Refresh(); } -void Label::SetLabel(const wxString& label) -{ - if (GetLabel() == label) - return; - wxStaticText::SetLabel(label); -#ifdef __WXOSX__ - if ((GetWindowStyle() & LB_HYPERLINK)) { - SetLabelMarkup(label); - return; - } -#endif + }); + Bind(wxEVT_LEAVE_WINDOW, [this](auto &e) { + SetFont(this->font); + Refresh(); + }); } void Label::SetWindowStyleFlag(long style) @@ -262,27 +120,12 @@ void Label::SetWindowStyleFlag(long style) if (style & LB_HYPERLINK) { this->color = GetForegroundColour(); static wxColor clr_url("#00AE42"); - SetFont(this->font.Underlined()); SetForegroundColour(clr_url); - SetCursor(wxCURSOR_HAND); -#ifdef __WXOSX__ - SetLabelMarkup(GetLabel()); -#endif } else { SetForegroundColour(this->color); SetFont(this->font); - SetCursor(wxCURSOR_ARROW); -#ifdef __WXOSX__ - auto label = GetLabel(); - wxStaticText::SetLabel({}); - wxStaticText::SetLabel(label); -#endif } Refresh(); } -void Label::Wrap(int width) -{ - wxLabelWrapper2 wrapper; - wrapper.WrapLabel(this, width); -} + diff --git a/src/slic3r/GUI/Widgets/Label.hpp b/src/slic3r/GUI/Widgets/Label.hpp index 7e71517..fb327d9 100644 --- a/src/slic3r/GUI/Widgets/Label.hpp +++ b/src/slic3r/GUI/Widgets/Label.hpp @@ -2,15 +2,10 @@ #define slic3r_GUI_Label_hpp_ #include +#include -#define LB_HYPERLINK 0x0020 -#define LB_PROPAGATE_MOUSE_EVENT 0x0040 +#define LB_HYPERLINK 0x0001 -//B35 -#if defined __linux__ -#include -#include "wx/dcclient.h" -#endif class Label : public wxStaticText { @@ -19,11 +14,9 @@ public: Label(wxWindow *parent, wxFont const &font, wxString const &text = {}, long style = 0); - void SetLabel(const wxString& label) override; void SetWindowStyleFlag(long style) override; - void Wrap(int width); private: wxFont font; @@ -38,7 +31,6 @@ public: static wxFont Head_14; static wxFont Head_13; static wxFont Head_12; - static wxFont Head_11; static wxFont Head_10; static wxFont Body_16; diff --git a/src/slic3r/GUI/Widgets/SpinInput.cpp b/src/slic3r/GUI/Widgets/SpinInput.cpp new file mode 100644 index 0000000..0456d1a --- /dev/null +++ b/src/slic3r/GUI/Widgets/SpinInput.cpp @@ -0,0 +1,633 @@ +#include "SpinInput.hpp" +#include "Button.hpp" + +#include "UIColors.hpp" + +#include "../GUI_App.hpp" + +#include +#include +#include +#include + +BEGIN_EVENT_TABLE(SpinInputBase, wxPanel) + +EVT_KEY_DOWN(SpinInputBase::keyPressed) +EVT_MOUSEWHEEL(SpinInputBase::mouseWheelMoved) + +EVT_PAINT(SpinInputBase::paintEvent) + +END_EVENT_TABLE() + + +/* + * Called by the system of by wxWidgets when the panel needs + * to be redrawn. You can also trigger this call by + * calling Refresh()/Update(). + */ + +SpinInputBase::SpinInputBase() + : label_color(std::make_pair(0x909090, (int) StateColor::Disabled), std::make_pair(0x6B6B6B, (int) StateColor::Normal)) + , text_color(std::make_pair(0x909090, (int) StateColor::Disabled), std::make_pair(0x262E30, (int) StateColor::Normal)) +{ + if (Slic3r::GUI::wxGetApp().suppress_round_corners()) + radius = 0; + border_width = 1; +} + +Button * SpinInputBase::create_button(ButtonId id) +{ + auto btn = new Button(this, "", id == ButtonId::btnIncrease ? "spin_inc_act" : "spin_dec_act", wxBORDER_NONE, wxSize(12, 7)); + btn->SetCornerRadius(0); + btn->SetInactiveIcon(id == ButtonId::btnIncrease ? "spin_inc" : "spin_dec"); + btn->DisableFocusFromKeyboard(); + btn->SetSelected(false); + + bind_inc_dec_button(btn, id); + + return btn; +} + +void SpinInputBase::SetCornerRadius(double radius) +{ + this->radius = radius; + Refresh(); +} + +void SpinInputBase::SetLabel(const wxString &label) +{ + wxWindow::SetLabel(label); + messureSize(); + Refresh(); +} + +void SpinInputBase::SetLabelColor(StateColor const &color) +{ + label_color = color; + state_handler.update_binds(); +} + +void SpinInputBase::SetTextColor(StateColor const &color) +{ + text_color = color; + state_handler.update_binds(); +} + +void SpinInputBase::SetSize(wxSize const &size) +{ + wxWindow::SetSize(size); + Rescale(); +} + +wxString SpinInputBase::GetTextValue() const +{ + return text_ctrl->GetValue(); +} + +void SpinInput::SetRange(int min, int max) +{ + this->min = min; + this->max = max; +} + +void SpinInputBase::SetSelection(long from, long to) +{ + if (text_ctrl) + text_ctrl->SetSelection(from, to); +} + +bool SpinInputBase::SetFont(wxFont const& font) +{ + if (text_ctrl) + return text_ctrl->SetFont(font); + return StaticBox::SetFont(font); +} + +bool SpinInputBase::SetBackgroundColour(const wxColour& colour) +{ + const int clr_background_disabled = Slic3r::GUI::wxGetApp().dark_mode() ? clr_background_disabled_dark : clr_background_disabled_light; + StateColor clr_state(std::make_pair(clr_background_disabled, (int)StateColor::Disabled), + std::make_pair(clr_background_focused, (int)StateColor::Checked), + std::make_pair(colour, (int)StateColor::Focused), + std::make_pair(colour, (int)StateColor::Normal)); + + StaticBox::SetBackgroundColor(clr_state); + if (text_ctrl) + text_ctrl->SetBackgroundColour(colour); + if (button_inc) + button_inc->SetBackgroundColor(clr_state); + if (button_dec) + button_dec->SetBackgroundColor(clr_state); + + return true; +} + +bool SpinInputBase::SetForegroundColour(const wxColour& colour) +{ + StateColor clr_state(std::make_pair(clr_foreground_disabled, (int)StateColor::Disabled), + std::make_pair(colour, (int)StateColor::Normal)); + + SetLabelColor(clr_state); + SetTextColor(clr_state); + + if (text_ctrl) + text_ctrl->SetForegroundColour(colour); + if (button_inc) + button_inc->SetTextColor(clr_state); + if (button_dec) + button_dec->SetTextColor(clr_state); + + return true; +} + +void SpinInputBase::SetBorderColor(StateColor const &color) +{ + StaticBox::SetBorderColor(color); + if (button_inc) + button_inc->SetBorderColor(color); + if (button_dec) + button_dec->SetBorderColor(color); +} + +void SpinInputBase::DoSetToolTipText(wxString const &tip) +{ + wxWindow::DoSetToolTipText(tip); + text_ctrl->SetToolTip(tip); +} + +void SpinInputBase::Rescale() +{ + SetFont(Slic3r::GUI::wxGetApp().normal_font()); + text_ctrl->SetInitialSize(text_ctrl->GetBestSize()); + + button_inc->Rescale(); + button_dec->Rescale(); + messureSize(); +} + +bool SpinInputBase::Enable(bool enable) +{ + bool result = text_ctrl->Enable(enable) && wxWindow::Enable(enable); + if (result) { + wxCommandEvent e(EVT_ENABLE_CHANGED); + e.SetEventObject(this); + GetEventHandler()->ProcessEvent(e); + text_ctrl->SetBackgroundColour(background_color.colorForStates(state_handler.states())); + text_ctrl->SetForegroundColour(text_color.colorForStates(state_handler.states())); + button_inc->Enable(enable); + button_dec->Enable(enable); + } + return result; +} + +void SpinInputBase::paintEvent(wxPaintEvent& evt) +{ + // depending on your system you may need to look at double-buffered dcs + wxPaintDC dc(this); + render(dc); +} + +/* + * Here we do the actual rendering. I put it in a separate + * method so that it can work no matter what type of DC + * (e.g. wxPaintDC or wxClientDC) is used. + */ +void SpinInputBase::render(wxDC& dc) +{ + StaticBox::render(dc); + int states = state_handler.states(); + wxSize size = GetSize(); + // draw seperator of buttons + wxPoint pt = button_inc->GetPosition(); + pt.y = size.y / 2; + dc.SetPen(wxPen(border_color.defaultColor())); + + const double scale = dc.GetContentScaleFactor(); + const int btn_w = button_inc->GetSize().GetWidth(); + dc.DrawLine(pt, pt + wxSize{ btn_w - int(scale), 0}); + // draw label + auto label = GetLabel(); + if (!label.IsEmpty()) { + pt.x = size.x - labelSize.x - 5; + pt.y = (size.y - labelSize.y) / 2; + dc.SetFont(GetFont()); + dc.SetTextForeground(label_color.colorForStates(states)); + dc.DrawText(label, pt); + } +} + +void SpinInputBase::messureSize() +{ + wxSize size = GetSize(); + wxSize textSize = text_ctrl->GetSize(); + int h = textSize.y + 8; + if (size.y != h) { + size.y = h; + SetSize(size); + SetMinSize(size); + } + + wxSize btnSize = {14, (size.y - 4) / 2}; + btnSize.x = btnSize.x * btnSize.y / 10; + + const double scale = this->GetContentScaleFactor(); + + wxClientDC dc(this); + labelSize = dc.GetMultiLineTextExtent(GetLabel()); + textSize.x = size.x - labelSize.x - btnSize.x - 16; + text_ctrl->SetSize(textSize); + text_ctrl->SetPosition({int(3. * scale), (size.y - textSize.y) / 2}); + button_inc->SetSize(btnSize); + button_dec->SetSize(btnSize); + button_inc->SetPosition({size.x - btnSize.x - int(3. * scale), size.y / 2 - btnSize.y/* - 1*/}); + button_dec->SetPosition({size.x - btnSize.x - int(3. * scale), size.y / 2 + 1}); +} + +void SpinInputBase::onText(wxCommandEvent &event) +{ + sendSpinEvent(); + event.SetId(GetId()); + ProcessEventLocally(event); +} + +void SpinInputBase::sendSpinEvent() +{ + wxCommandEvent event(wxEVT_SPINCTRL, GetId()); + event.SetEventObject(this); + GetEventHandler()->ProcessEvent(event); +} + + +// SpinInput + +SpinInput::SpinInput(wxWindow *parent, + wxString text, + wxString label, + const wxPoint &pos, + const wxSize & size, + long style, + int min, int max, int initial) + : SpinInputBase() +{ + Create(parent, text, label, pos, size, style, min, max, initial); +} + +void SpinInput::Create(wxWindow *parent, + wxString text, + wxString label, + const wxPoint &pos, + const wxSize & size, + long style, + int min, int max, int initial) +{ + StaticBox::Create(parent, wxID_ANY, pos, size); + wxWindow::SetLabel(label); + + state_handler.attach({&label_color, &text_color}); + state_handler.update_binds(); + + text_ctrl = new wxTextCtrl(this, wxID_ANY, text, {20, 4}, wxDefaultSize, style | wxBORDER_NONE | wxTE_PROCESS_ENTER, wxTextValidator(wxFILTER_NUMERIC)); +#ifdef __WXOSX__ + text_ctrl->OSXDisableAllSmartSubstitutions(); +#endif // __WXOSX__ + text_ctrl->SetInitialSize(text_ctrl->GetBestSize()); + state_handler.attach_child(text_ctrl); + + text_ctrl->Bind(wxEVT_KILL_FOCUS, &SpinInput::onTextLostFocus, this); + text_ctrl->Bind(wxEVT_TEXT, &SpinInput::onText, this); + text_ctrl->Bind(wxEVT_TEXT_ENTER, &SpinInput::onTextEnter, this); + text_ctrl->Bind(wxEVT_KEY_DOWN, &SpinInput::keyPressed, this); + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu + button_inc = create_button(ButtonId::btnIncrease); + button_dec = create_button(ButtonId::btnDecrease); + delta = 0; + timer.Bind(wxEVT_TIMER, &SpinInput::onTimer, this); + + SetFont(Slic3r::GUI::wxGetApp().normal_font()); + if (parent) { + SetBackgroundColour(parent->GetBackgroundColour()); + SetForegroundColour(parent->GetForegroundColour()); + } + + long initialFromText; + if (text.ToLong(&initialFromText)) initial = initialFromText; + SetRange(min, max); + SetValue(initial); + messureSize(); +} + +void SpinInput::bind_inc_dec_button(Button *btn, ButtonId id) +{ + btn->Bind(wxEVT_LEFT_DOWN, [this, btn, id](auto& e) { + delta = id == ButtonId::btnIncrease ? 1 : -1; + SetValue(val + delta); + text_ctrl->SetFocus(); + btn->CaptureMouse(); + delta *= 8; + timer.Start(100); + sendSpinEvent(); + }); + btn->Bind(wxEVT_LEFT_DCLICK, [this, btn, id](auto& e) { + delta = id == ButtonId::btnIncrease ? 1 : -1; + btn->CaptureMouse(); + SetValue(val + delta); + sendSpinEvent(); + }); + btn->Bind(wxEVT_LEFT_UP, [this, btn](auto& e) { + btn->ReleaseMouse(); + timer.Stop(); + text_ctrl->SelectAll(); + delta = 0; + }); +} + +void SpinInput::SetValue(const wxString &text) +{ + long value; + if ( text.ToLong(&value) ) + SetValue(value); + else + text_ctrl->SetValue(text); +} + +void SpinInput::SetValue(int value) +{ + if (value < min) value = min; + else if (value > max) value = max; + this->val = value; + text_ctrl->SetValue(wxString::FromDouble(value)); +} + +int SpinInput::GetValue()const +{ + return val; +} + +void SpinInput::onTimer(wxTimerEvent &evnet) { + if (delta < -1 || delta > 1) { + delta /= 2; + return; + } + SetValue(val + delta); + sendSpinEvent(); +} + +void SpinInput::onTextLostFocus(wxEvent &event) +{ + timer.Stop(); + for (auto * child : GetChildren()) + if (auto btn = dynamic_cast(child)) + if (btn->HasCapture()) + btn->ReleaseMouse(); + wxCommandEvent e; + onTextEnter(e); + // pass to outer + event.SetId(GetId()); + ProcessEventLocally(event); + event.Skip(); +} + +void SpinInput::onTextEnter(wxCommandEvent &event) +{ + long value; + if (!text_ctrl->GetValue().ToLong(&value)) + value = val; + + if (value != val) { + SetValue(value); + sendSpinEvent(); + } + event.SetId(GetId()); + ProcessEventLocally(event); +} + +void SpinInput::mouseWheelMoved(wxMouseEvent &event) +{ + auto delta = ((event.GetWheelRotation() < 0) == event.IsWheelInverted()) ? 1 : -1; + SetValue(val + delta); + sendSpinEvent(); + text_ctrl->SetFocus(); +} + +void SpinInput::keyPressed(wxKeyEvent &event) +{ + switch (event.GetKeyCode()) { + case WXK_UP: + case WXK_DOWN: + long value; + if (!text_ctrl->GetValue().ToLong(&value)) { value = val; } + if (event.GetKeyCode() == WXK_DOWN && value > min) { + --value; + } else if (event.GetKeyCode() == WXK_UP && value + 1 < max) { + ++value; + } + if (value != val) { + SetValue(value); + sendSpinEvent(); + } + break; + default: event.Skip(); break; + } +} + + + +// SpinInputDouble + +SpinInputDouble::SpinInputDouble(wxWindow * parent, + wxString text, + wxString label, + const wxPoint &pos, + const wxSize & size, + long style, + double min, double max, double initial, + double inc) + : SpinInputBase() +{ + Create(parent, text, label, pos, size, style, min, max, initial, inc); +} + +void SpinInputDouble::Create(wxWindow *parent, + wxString text, + wxString label, + const wxPoint &pos, + const wxSize & size, + long style, + double min, double max, double initial, + double inc) +{ + StaticBox::Create(parent, wxID_ANY, pos, size); + wxWindow::SetLabel(label); + + state_handler.attach({&label_color, &text_color}); + state_handler.update_binds(); + + text_ctrl = new wxTextCtrl(this, wxID_ANY, text, {20, 4}, wxDefaultSize, style | wxBORDER_NONE | wxTE_PROCESS_ENTER, wxTextValidator(wxFILTER_NUMERIC)); +#ifdef __WXOSX__ + text_ctrl->OSXDisableAllSmartSubstitutions(); +#endif // __WXOSX__ + text_ctrl->SetInitialSize(text_ctrl->GetBestSize()); + state_handler.attach_child(text_ctrl); + + text_ctrl->Bind(wxEVT_KILL_FOCUS, &SpinInputDouble::onTextLostFocus, this); + text_ctrl->Bind(wxEVT_TEXT, &SpinInputDouble::onText, this); + text_ctrl->Bind(wxEVT_TEXT_ENTER, &SpinInputDouble::onTextEnter, this); + text_ctrl->Bind(wxEVT_KEY_DOWN, &SpinInputDouble::keyPressed, this); + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu + button_inc = create_button(ButtonId::btnIncrease); + button_dec = create_button(ButtonId::btnDecrease); + delta = 0; + timer.Bind(wxEVT_TIMER, &SpinInputDouble::onTimer, this); + + SetFont(Slic3r::GUI::wxGetApp().normal_font()); + if (parent) { + SetBackgroundColour(parent->GetBackgroundColour()); + SetForegroundColour(parent->GetForegroundColour()); + } + + double initialFromText; + if (text.ToDouble(&initialFromText)) initial = initialFromText; + SetRange(min, max); + SetIncrement(inc); + SetValue(initial); + messureSize(); +} + +void SpinInputDouble::bind_inc_dec_button(Button *btn, ButtonId id) +{ + btn->Bind(wxEVT_LEFT_DOWN, [this, btn, id](auto& e) { + delta = id == ButtonId::btnIncrease ? inc : -inc; + SetValue(val + delta); + text_ctrl->SetFocus(); + btn->CaptureMouse(); + delta *= 8; + timer.Start(100); + sendSpinEvent(); + }); + btn->Bind(wxEVT_LEFT_DCLICK, [this, btn, id](auto& e) { + delta = id == ButtonId::btnIncrease ? inc : -inc; + btn->CaptureMouse(); + SetValue(val + delta); + sendSpinEvent(); + }); + btn->Bind(wxEVT_LEFT_UP, [this, btn](auto& e) { + btn->ReleaseMouse(); + timer.Stop(); + text_ctrl->SelectAll(); + delta = 0; + }); +} + +void SpinInputDouble::SetValue(const wxString &text) +{ + double value; + if ( text.ToDouble(&value) ) + SetValue(value); + else + text_ctrl->SetValue(text); +} + +void SpinInputDouble::SetValue(double value) +{ + if (Slic3r::is_approx(value, val)) + return; + + if (value < min) value = min; + else if (value > max) value = max; + this->val = value; + wxString str_val = wxString::FromDouble(value, digits); + text_ctrl->SetValue(str_val); +} + +double SpinInputDouble::GetValue()const +{ + return val; +} + +void SpinInputDouble::SetRange(double min, double max) +{ + this->min = min; + this->max = max; +} + +void SpinInputDouble::SetIncrement(double inc_in) +{ + inc = inc_in; +} + +void SpinInputDouble::SetDigits(unsigned digits_in) +{ + digits = int(digits_in); +} + +void SpinInputDouble::onTimer(wxTimerEvent &evnet) { + if (delta < -inc || delta > inc) { + delta /= 2; + return; + } + SetValue(val + delta); + sendSpinEvent(); +} + +void SpinInputDouble::onTextLostFocus(wxEvent &event) +{ + timer.Stop(); + for (auto * child : GetChildren()) + if (auto btn = dynamic_cast(child)) + if (btn->HasCapture()) + btn->ReleaseMouse(); + wxCommandEvent e; + onTextEnter(e); + // pass to outer + event.SetId(GetId()); + ProcessEventLocally(event); + event.Skip(); +} + +void SpinInputDouble::onTextEnter(wxCommandEvent &event) +{ + double value; + if (!text_ctrl->GetValue().ToDouble(&value)) + val = value; + + if (!Slic3r::is_approx(value, val)) { + SetValue(value); + sendSpinEvent(); + } + event.SetId(GetId()); + ProcessEventLocally(event); +} + +void SpinInputDouble::mouseWheelMoved(wxMouseEvent &event) +{ + auto delta = ((event.GetWheelRotation() < 0) == event.IsWheelInverted()) ? inc : -inc; + SetValue(val + delta); + sendSpinEvent(); + text_ctrl->SetFocus(); +} + +void SpinInputDouble::keyPressed(wxKeyEvent &event) +{ + switch (event.GetKeyCode()) { + case WXK_UP: + case WXK_DOWN: + double value; + if (!text_ctrl->GetValue().ToDouble(&value)) + val = value; + + if (event.GetKeyCode() == WXK_DOWN && value > min) { + value -= inc; + } else if (event.GetKeyCode() == WXK_UP && value + inc < max) { + value += inc; + } + if (!Slic3r::is_approx(value, val)) { + SetValue(value); + sendSpinEvent(); + } + break; + default: event.Skip(); break; + } +} + + + diff --git a/src/slic3r/GUI/Widgets/SpinInput.hpp b/src/slic3r/GUI/Widgets/SpinInput.hpp new file mode 100644 index 0000000..f81ec0b --- /dev/null +++ b/src/slic3r/GUI/Widgets/SpinInput.hpp @@ -0,0 +1,190 @@ +#ifndef slic3r_GUI_SpinInput_hpp_ +#define slic3r_GUI_SpinInput_hpp_ + +#include +#include "StaticBox.hpp" + +class Button; + +class SpinInputBase : public wxNavigationEnabled +{ +protected: + wxSize labelSize; + StateColor label_color; + StateColor text_color; + wxTextCtrl * text_ctrl{nullptr}; + Button * button_inc {nullptr}; + Button * button_dec {nullptr}; + wxTimer timer; + + static const int SpinInputWidth = 200; + static const int SpinInputHeight = 50; + + enum class ButtonId + { + btnIncrease, + btnDecrease + }; + +public: + SpinInputBase(); + + void SetCornerRadius(double radius); + + void SetLabel(const wxString &label) wxOVERRIDE; + + void SetLabelColor(StateColor const &color); + + void SetTextColor(StateColor const &color); + + void SetSize(wxSize const &size); + + void Rescale(); + + virtual bool Enable(bool enable = true) wxOVERRIDE; + + wxTextCtrl * GetText() { return text_ctrl; } + + virtual void SetValue(const wxString &text) = 0; + + wxString GetTextValue() const; + + bool SetFont(wxFont const& font) override; + + bool SetBackgroundColour(const wxColour& colour) override; + bool SetForegroundColour(const wxColour& colour) override; + void SetBorderColor(StateColor const& color); + void SetSelection(long from, long to); + +protected: + void DoSetToolTipText(wxString const &tip) override; + + void paintEvent(wxPaintEvent& evt); + + void render(wxDC& dc); + + void messureSize(); + + Button *create_button(ButtonId id); + virtual void bind_inc_dec_button(Button *btn, ButtonId id) = 0; + + // some useful events + virtual void mouseWheelMoved(wxMouseEvent& event) = 0; + virtual void keyPressed(wxKeyEvent& event) = 0; + virtual void onTimer(wxTimerEvent &evnet) = 0; + virtual void onTextLostFocus(wxEvent &event) = 0; + virtual void onTextEnter(wxCommandEvent &event) = 0; + + void onText(wxCommandEvent &event); + + void sendSpinEvent(); + + DECLARE_EVENT_TABLE() +}; + +class SpinInput : public SpinInputBase +{ + int val; + int min; + int max; + int delta; + +public: + SpinInput(wxWindow * parent, + wxString text, + wxString label = "", + const wxPoint &pos = wxDefaultPosition, + const wxSize & size = wxDefaultSize, + long style = 0, + int min = 0, int max = 100, int initial = 0); + + void Create(wxWindow * parent, + wxString text, + wxString label = "", + const wxPoint &pos = wxDefaultPosition, + const wxSize & size = wxDefaultSize, + long style = 0, + int min = 0, + int max = 100, + int initial = 0); + + void SetValue(const wxString &text) override; + + void SetValue (int value); + int GetValue () const; + + void SetRange(int min, int max); + + int GetMin() const { return this->min; } + int GetMax() const { return this->max; } + +protected: + void bind_inc_dec_button(Button* btn, ButtonId id) override; + // some useful events + void mouseWheelMoved(wxMouseEvent& event) override; + void keyPressed(wxKeyEvent& event) override; + void onTimer(wxTimerEvent& evnet) override; + void onTextLostFocus(wxEvent& event) override; + void onTextEnter(wxCommandEvent& event) override; +}; + +class SpinInputDouble : public SpinInputBase +{ + double val; + double min; + double max; + double inc; + double delta; + int digits {-1}; + +public: + + SpinInputDouble() : SpinInputBase() {} + + SpinInputDouble(wxWindow* parent, + wxString text, + wxString label = "", + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0, + double min = 0., + double max = 100., + double initial = 0., + double inc = 1.); + + void Create(wxWindow* parent, + wxString text, + wxString label = "", + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0, + double min = 0., + double max = 100., + double initial = 0., + double inc = 1.); + + void SetValue(const wxString& text) override; + + void SetValue(double value); + double GetValue() const; + + //wxString GetTextValue() const override; + + void SetRange(double min, double max); + void SetIncrement(double inc); + void SetDigits(unsigned digits); + + double GetMin() const { return this->min; } + double GetMax() const { return this->max; } + +protected: + void bind_inc_dec_button(Button* btn, ButtonId id) override; + // some useful events + void mouseWheelMoved(wxMouseEvent& event) override; + void keyPressed(wxKeyEvent& event) override; + void onTimer(wxTimerEvent& evnet) override; + void onTextLostFocus(wxEvent& event) override; + void onTextEnter(wxCommandEvent& event) override; +}; + +#endif // !slic3r_GUI_SpinInput_hpp_ diff --git a/src/slic3r/GUI/Widgets/StateColor.cpp b/src/slic3r/GUI/Widgets/StateColor.cpp index 61cc393..5b9e725 100644 --- a/src/slic3r/GUI/Widgets/StateColor.cpp +++ b/src/slic3r/GUI/Widgets/StateColor.cpp @@ -1,80 +1,5 @@ #include "StateColor.hpp" -static bool gDarkMode = false; - -static bool operator<(wxColour const &l, wxColour const &r) { return l.GetRGBA() < r.GetRGBA(); } - -static std::map gDarkColors{ - {"#00AE42", "#21A452"}, - {"#1F8EEA", "#2778D2"}, - {"#FF6F00", "#D15B00"}, - {"#D01B1B", "#BB2A3A"}, - {"#262E30", "#EFEFF0"}, - {"#2C2C2E", "#B3B3B4"}, - {"#6B6B6B", "#818183"}, - {"#ACACAC", "#54545A"}, - {"#EEEEEE", "#4C4C55"}, - {"#E8E8E8", "#3E3E45"}, - {"#323A3D", "#E5E5E4"}, - {"#FFFFFF", "#2D2D31"}, - {"#F8F8F8", "#36363C"}, - {"#F1F1F1", "#36363B"}, - {"#3B4446", "#2D2D30"}, - {"#CECECE", "#54545B"}, - {"#DBFDD5", "#3B3B40"}, - {"#000000", "#FFFFFE"}, - {"#F4F4F4", "#36363D"}, - {"#DBDBDB", "#4A4A51"}, - {"#EDFAF2", "#283232"}, - {"#323A3C", "#E5E5E6"}, - {"#6B6B6A", "#B3B3B5"}, - {"#303A3C", "#E5E5E5"}, - {"#FEFFFF", "#242428"}, - {"#A6A9AA", "#2D2D29"}, - {"#363636", "#B2B3B5"}, - {"#F0F0F1", "#404040"}, - {"#9E9E9E", "#53545A"}, - {"#D7E8DE", "#1F2B27"}, - {"#2B3436", "#808080"}, - {"#ABABAB", "#ABABAB"}, - {"#D9D9D9", "#2D2D32"}, - //{"#F0F0F0", "#4C4C54"}, -}; - -std::map const & StateColor::GetDarkMap() -{ - return gDarkColors; -} - -void StateColor::SetDarkMode(bool dark) { gDarkMode = dark; } - -inline wxColour darkModeColorFor2(wxColour const &color) -{ - if (!gDarkMode) - return color; - auto iter = gDarkColors.find(color); - wxFAIL(iter != gDarkColors.end()); - if (iter != gDarkColors.end()) return iter->second; - return color; -} - -std::map revert(std::map const & map) -{ - std::map map2; - for (auto &p : map) map2.emplace(p.second, p.first); - return map2; -} - -wxColour StateColor::lightModeColorFor(wxColour const &color) -{ - static std::map gLightColors = revert(gDarkColors); - auto iter = gLightColors.find(color); - wxFAIL(iter != gLightColors.end()); - if (iter != gLightColors.end()) return iter->second; - return color; -} - -wxColour StateColor::darkModeColorFor(wxColour const &color) { return darkModeColorFor2(color); } StateColor::StateColor(wxColour const &color) { append(color, 0); } @@ -98,7 +23,7 @@ void StateColor::append(unsigned long color, int states) { if ((color & 0xff000000) == 0) color |= 0xff000000; - wxColour cl; cl.SetRGBA(color & 0xff00ff00 | ((color & 0xff) << 16) | ((color >> 16) & 0xff)); + wxColour cl; cl.SetRGBA((color & 0xff00ff00) | ((color & 0xff) << 16) | ((color >> 16) & 0xff)); append(cl, states); } @@ -125,28 +50,7 @@ wxColour StateColor::defaultColor() { wxColour StateColor::colorForStates(int states) { bool focused = takeFocusedAsHovered_ && (states & Focused); - for (int i = 0; i < statesList_.size(); ++i) { - int s = statesList_[i]; - int on = s & 0xffff; - int off = s >> 16; - if ((on & states) == on && (off & ~states) == off) { - return darkModeColorFor2(colors_[i]); - } - if (focused && (on & Hovered)) { - on |= Focused; - on &= ~Hovered; - if ((on & states) == on && (off & ~states) == off) { - return darkModeColorFor2(colors_[i]); - } - } - } - return wxColour(0, 0, 0, 0); -} - -wxColour StateColor::colorForStatesNoDark(int states) -{ - bool focused = takeFocusedAsHovered_ && (states & Focused); - for (int i = 0; i < statesList_.size(); ++i) { + for (size_t i = 0; i < statesList_.size(); ++i) { int s = statesList_[i]; int on = s & 0xffff; int off = s >> 16; @@ -166,18 +70,18 @@ wxColour StateColor::colorForStatesNoDark(int states) int StateColor::colorIndexForStates(int states) { - for (int i = 0; i < statesList_.size(); ++i) { + for (size_t i = 0; i < statesList_.size(); ++i) { int s = statesList_[i]; int on = s & 0xffff; int off = s >> 16; - if ((on & states) == on && (off & ~states) == off) { return i; } + if ((on & states) == on && (off & ~states) == off) { return int(i); } } return -1; } bool StateColor::setColorForStates(wxColour const &color, int states) { - for (int i = 0; i < statesList_.size(); ++i) { + for (size_t i = 0; i < statesList_.size(); ++i) { if (statesList_[i] == states) { colors_[i] = color; return true; diff --git a/src/slic3r/GUI/Widgets/StateColor.hpp b/src/slic3r/GUI/Widgets/StateColor.hpp index 1f7799f..c75aa07 100644 --- a/src/slic3r/GUI/Widgets/StateColor.hpp +++ b/src/slic3r/GUI/Widgets/StateColor.hpp @@ -3,7 +3,6 @@ #include -#include class StateColor { @@ -22,13 +21,6 @@ public: NotPressed = 16 << 16, }; -public: - static void SetDarkMode(bool dark); - - static std::map const & GetDarkMap(); - static wxColour darkModeColorFor(wxColour const &color); - static wxColour lightModeColorFor(wxColour const &color); - public: template StateColor(std::pair... colors) { @@ -63,7 +55,6 @@ public: wxColour colorForStates(int states); - wxColour colorForStatesNoDark(int states); int colorIndexForStates(int states); diff --git a/src/slic3r/GUI/Widgets/StateHandler.cpp b/src/slic3r/GUI/Widgets/StateHandler.cpp index f66585f..d2f2cc1 100644 --- a/src/slic3r/GUI/Widgets/StateHandler.cpp +++ b/src/slic3r/GUI/Widgets/StateHandler.cpp @@ -1,5 +1,6 @@ #include "StateHandler.hpp" +#include wxDEFINE_EVENT(EVT_ENABLE_CHANGED, wxCommandEvent); StateHandler::StateHandler(wxWindow * owner) @@ -50,7 +51,7 @@ void StateHandler::update_binds() int diff = bind_states ^ bind_states_; State states[] = {Enabled, Checked, Focused, Hovered, Pressed}; wxEventType events[] = {EVT_ENABLE_CHANGED, wxEVT_CHECKBOX, wxEVT_SET_FOCUS, wxEVT_ENTER_WINDOW, wxEVT_LEFT_DOWN}; - wxEventType events2[] = {{0}, {0}, wxEVT_KILL_FOCUS, wxEVT_LEAVE_WINDOW, wxEVT_LEFT_UP}; + wxEventType events2[] = {0, 0, wxEVT_KILL_FOCUS, wxEVT_LEAVE_WINDOW, wxEVT_LEFT_UP}; for (int i = 0; i < 5; ++i) { int s = states[i]; if (diff & s) { @@ -69,18 +70,6 @@ void StateHandler::update_binds() for (auto &c : children_) c->update_binds(); } -void StateHandler::set_state(int state, int mask) -{ - if (states_ & mask == state & mask) return; - int old = states_; - states_ = states_ & ~mask | state & mask; - if (old != states_ && (old | states2_) != (states_ | states2_)) { - if (parent_) - parent_->changed(states_ | states2_); - else - owner_->Refresh(); - } -} StateHandler::StateHandler(StateHandler *parent, wxWindow *owner) : StateHandler(owner) @@ -93,7 +82,7 @@ void StateHandler::changed(wxEvent &event) { event.Skip(); wxEventType events[] = {EVT_ENABLE_CHANGED, wxEVT_CHECKBOX, wxEVT_SET_FOCUS, wxEVT_ENTER_WINDOW, wxEVT_LEFT_DOWN}; - wxEventType events2[] = {{0}, {0}, wxEVT_KILL_FOCUS, wxEVT_LEAVE_WINDOW, wxEVT_LEFT_UP}; + wxEventType events2[] = { 0, 0, wxEVT_KILL_FOCUS, wxEVT_LEAVE_WINDOW, wxEVT_LEFT_UP}; int old = states_; // some events are from another window (ex: text_ctrl of TextInput), save state in states2_ to avoid conflicts for (int i = 0; i < 5; ++i) { diff --git a/src/slic3r/GUI/Widgets/StateHandler.hpp b/src/slic3r/GUI/Widgets/StateHandler.hpp index 8aded43..0a27776 100644 --- a/src/slic3r/GUI/Widgets/StateHandler.hpp +++ b/src/slic3r/GUI/Widgets/StateHandler.hpp @@ -2,14 +2,10 @@ #define slic3r_GUI_StateHandler_hpp_ #include +#include #include "StateColor.hpp" -//B35 -#if defined __linux__ -#include -#include -#endif wxDECLARE_EVENT(EVT_ENABLE_CHANGED, wxCommandEvent); @@ -48,7 +44,6 @@ public: int states() const { return states_ | states2_; } - void set_state(int state, int mask); private: StateHandler(StateHandler * parent, wxWindow *owner); diff --git a/src/slic3r/GUI/Widgets/StaticBox.cpp b/src/slic3r/GUI/Widgets/StaticBox.cpp index 6fde9bb..9780905 100644 --- a/src/slic3r/GUI/Widgets/StaticBox.cpp +++ b/src/slic3r/GUI/Widgets/StaticBox.cpp @@ -1,11 +1,13 @@ #include "StaticBox.hpp" #include "../GUI.hpp" #include +#include + +#include "DropDown.hpp" +#include "UIColors.hpp" BEGIN_EVENT_TABLE(StaticBox, wxWindow) -// catch paint events -//EVT_ERASE_BACKGROUND(StaticBox::eraseEvent) EVT_PAINT(StaticBox::paintEvent) END_EVENT_TABLE() @@ -20,9 +22,15 @@ StaticBox::StaticBox() : state_handler(this) , radius(8) { - border_color = StateColor( - std::make_pair(0xF0F0F1, (int) StateColor::Disabled), - std::make_pair(0x303A3C, (int) StateColor::Normal)); + border_color = StateColor(std::make_pair(clr_border_disabled, (int) StateColor::Disabled), +#ifndef __WXMSW__ + std::make_pair(clr_border_normal, (int) StateColor::Focused), +#endif + std::make_pair(clr_border_hovered, (int) StateColor::Hovered), + std::make_pair(clr_border_normal, (int) StateColor::Normal)); +#ifndef __WXMSW__ + border_color.setTakeFocusedAsHovered(false); +#endif } StaticBox::StaticBox(wxWindow* parent, @@ -41,7 +49,8 @@ bool StaticBox::Create(wxWindow* parent, wxWindowID id, const wxPoint& pos, cons wxWindow::Create(parent, id, pos, size, style); state_handler.attach({&border_color, &background_color, &background_color2}); state_handler.update_binds(); - SetBackgroundColour(GetParentBackgroundColor(parent)); +// SetBackgroundColour(GetParentBackgroundColor(parent)); +// SetForegroundColour(parent->GetParent()->GetForegroundColour()); return true; } @@ -109,21 +118,11 @@ wxColor StaticBox::GetParentBackgroundColor(wxWindow* parent) return *wxWHITE; } -void StaticBox::eraseEvent(wxEraseEvent& evt) -{ - // for transparent background, but not work -#ifdef __WXMSW__ - wxDC *dc = evt.GetDC(); - wxSize size = GetSize(); - wxClientDC dc2(GetParent()); - dc->Blit({0, 0}, size, &dc2, GetPosition()); -#endif -} void StaticBox::paintEvent(wxPaintEvent& evt) { // depending on your system you may need to look at double-buffered dcs - wxPaintDC dc(this); + wxBufferedPaintDC dc(this);//wxPaintDC dc(this); render(dc); } @@ -134,31 +133,7 @@ void StaticBox::paintEvent(wxPaintEvent& evt) */ void StaticBox::render(wxDC& dc) { -#ifdef __WXMSW__ - if (radius == 0) { - doRender(dc); - return; - } - - wxSize size = GetSize(); - if (size.x <= 0 || size.y <= 0) - return; - wxMemoryDC memdc; - wxBitmap bmp(size.x, size.y); - memdc.SelectObject(bmp); - //memdc.Blit({0, 0}, size, &dc, {0, 0}); - memdc.SetBackground(wxBrush(GetBackgroundColour())); - memdc.Clear(); - { - wxGCDC dc2(memdc); - doRender(dc2); - } - - memdc.SelectObject(wxNullBitmap); - dc.DrawBitmap(bmp, 0, 0); -#else doRender(dc); -#endif } void StaticBox::doRender(wxDC& dc) @@ -168,37 +143,53 @@ void StaticBox::doRender(wxDC& dc) if (background_color2.count() == 0) { if ((border_width && border_color.count() > 0) || background_color.count() > 0) { wxRect rc(0, 0, size.x, size.y); - if (border_width && border_color.count() > 0) { - if (dc.GetContentScaleFactor() == 1.0) { - int d = floor(border_width / 2.0); - int d2 = floor(border_width - 1); - rc.x += d; - rc.width -= d2; - rc.y += d; - rc.height -= d2; - } else { - int d = 1; - rc.x += d; - rc.width -= d; - rc.y += d; - rc.height -= d; - } - dc.SetPen(wxPen(border_color.colorForStates(states), border_width)); - } else { - dc.SetPen(wxPen(background_color.colorForStates(states))); +#ifdef __WXOSX__ + // On Retina displays all controls are cut on 1px + if (dc.GetContentScaleFactor() > 1.) + rc.Deflate(1, 1); +#endif //__WXOSX__ + + if (radius > 0.) { +#if 0 + DropDown::SetTransparentBG(dc, this); +#else +#ifdef __WXMSW__ + wxColour bg_clr = GetParentBackgroundColor(m_parent); + dc.SetBrush(wxBrush(bg_clr)); + dc.SetPen(wxPen(bg_clr)); + dc.DrawRectangle(rc); +#endif +#endif } + if (background_color.count() > 0) dc.SetBrush(wxBrush(background_color.colorForStates(states))); else dc.SetBrush(wxBrush(GetBackgroundColour())); - if (radius == 0) { - dc.DrawRectangle(rc); + if (border_width && border_color.count() > 0) { +#ifdef __WXOSX__ + const double bw = (double)border_width; +#else + const double bw = dc.GetContentScaleFactor() * (double)border_width; +#endif //__WXOSX__ + { + int d = floor(bw / 2.0); + int d2 = floor(bw - 1); + rc.x += d; + rc.width -= d2; + rc.y += d; + rc.height -= d2; + } + dc.SetPen(wxPen(border_color.colorForStates(states), bw)); + } else { + dc.SetPen(wxPen(background_color.colorForStates(states))); } - else { + if (radius == 0.) + dc.DrawRectangle(rc); + else dc.DrawRoundedRectangle(rc, radius - border_width); } } - } else { wxColor start = background_color.colorForStates(states); wxColor stop = background_color2.colorForStates(states); diff --git a/src/slic3r/GUI/Widgets/StaticBox.hpp b/src/slic3r/GUI/Widgets/StaticBox.hpp index 871c565..a2ad531 100644 --- a/src/slic3r/GUI/Widgets/StaticBox.hpp +++ b/src/slic3r/GUI/Widgets/StaticBox.hpp @@ -31,7 +31,7 @@ public: void SetBorderColorNormal(wxColor const &color); - void SetBackgroundColor(StateColor const &color); + virtual void SetBackgroundColor(StateColor const &color); void SetBackgroundColorNormal(wxColor const &color); @@ -40,7 +40,6 @@ public: static wxColor GetParentBackgroundColor(wxWindow * parent); protected: - void eraseEvent(wxEraseEvent& evt); void paintEvent(wxPaintEvent& evt); @@ -48,7 +47,6 @@ protected: virtual void doRender(wxDC& dc); -protected: double radius; int border_width = 1; StateHandler state_handler; diff --git a/src/slic3r/GUI/Widgets/SwitchButton.cpp b/src/slic3r/GUI/Widgets/SwitchButton.cpp new file mode 100644 index 0000000..c87d302 --- /dev/null +++ b/src/slic3r/GUI/Widgets/SwitchButton.cpp @@ -0,0 +1,136 @@ +#include "SwitchButton.hpp" + +#include "../wxExtensions.hpp" +#include "../../Utils/MacDarkMode.hpp" + +#include +#include +#include + +SwitchButton::SwitchButton(wxWindow* parent, const wxString& name, wxWindowID id) + : BitmapToggleButton(parent, name, id) + , m_on(this, "toggle_on", 28, 16) + , m_off(this, "toggle_off", 28, 16) + , text_color(std::pair{*wxWHITE, (int) StateColor::Checked}, std::pair{0x6B6B6B, (int) StateColor::Normal}) + , track_color(0xD9D9D9) + , thumb_color(std::pair{0x00AE42, (int) StateColor::Checked}, std::pair{0xD9D9D9, (int) StateColor::Normal}) +{ + Rescale(); +} + +void SwitchButton::SetLabels(wxString const& lbl_on, wxString const& lbl_off) +{ + labels[0] = lbl_on; + labels[1] = lbl_off; + Rescale(); +} + +void SwitchButton::SetTextColor(StateColor const& color) +{ + text_color = color; +} + +void SwitchButton::SetTrackColor(StateColor const& color) +{ + track_color = color; +} + +void SwitchButton::SetThumbColor(StateColor const& color) +{ + thumb_color = color; +} + +void SwitchButton::SetValue(bool value) +{ + if (value != GetValue()) + wxBitmapToggleButton::SetValue(value); + update(); +} + +void SwitchButton::Rescale() +{ + if (!labels[0].IsEmpty()) { +#ifdef __WXOSX__ + auto scale = Slic3r::GUI::mac_max_scaling_factor(); + int BS = (int) scale; +#else + constexpr int BS = 1; +#endif + wxSize thumbSize; + wxSize trackSize; + wxClientDC dc(this); +#ifdef __WXOSX__ + dc.SetFont(dc.GetFont().Scaled(scale)); +#endif + wxSize textSize[2]; + { + textSize[0] = dc.GetTextExtent(labels[0]); + textSize[1] = dc.GetTextExtent(labels[1]); + } + { + thumbSize = textSize[0]; + auto size = textSize[1]; + if (size.x > thumbSize.x) thumbSize.x = size.x; + else size.x = thumbSize.x; + thumbSize.x += BS * 12; + thumbSize.y += BS * 6; + trackSize.x = thumbSize.x + size.x + BS * 10; + trackSize.y = thumbSize.y + BS * 2; + auto maxWidth = GetMaxWidth(); +#ifdef __WXOSX__ + maxWidth *= scale; +#endif + if (trackSize.x > maxWidth) { + thumbSize.x -= (trackSize.x - maxWidth) / 2; + trackSize.x = maxWidth; + } + } + for (int i = 0; i < 2; ++i) { + wxMemoryDC memdc(&dc); + wxBitmap bmp(trackSize.x, trackSize.y); + memdc.SelectObject(bmp); + memdc.SetBackground(wxBrush(GetBackgroundColour())); + memdc.Clear(); + memdc.SetFont(dc.GetFont()); + auto state = i == 0 ? StateColor::Enabled : (StateColor::Checked | StateColor::Enabled); + { +#ifdef __WXMSW__ + wxGCDC dc2(memdc); +#else + wxDC &dc2(memdc); +#endif + dc2.SetBrush(wxBrush(track_color.colorForStates(state))); + dc2.SetPen(wxPen(track_color.colorForStates(state))); + dc2.DrawRoundedRectangle(wxRect({0, 0}, trackSize), trackSize.y / 2); + dc2.SetBrush(wxBrush(thumb_color.colorForStates(StateColor::Checked | StateColor::Enabled))); + dc2.SetPen(wxPen(thumb_color.colorForStates(StateColor::Checked | StateColor::Enabled))); + dc2.DrawRoundedRectangle(wxRect({ i == 0 ? BS : (trackSize.x - thumbSize.x - BS), BS}, thumbSize), thumbSize.y / 2); + } + memdc.SetTextForeground(text_color.colorForStates(state ^ StateColor::Checked)); + memdc.DrawText(labels[0], {BS + (thumbSize.x - textSize[0].x) / 2, BS + (thumbSize.y - textSize[0].y) / 2}); + memdc.SetTextForeground(text_color.colorForStates(state)); + memdc.DrawText(labels[1], {trackSize.x - thumbSize.x - BS + (thumbSize.x - textSize[1].x) / 2, BS + (thumbSize.y - textSize[1].y) / 2}); + memdc.SelectObject(wxNullBitmap); +#ifdef __WXOSX__ + bmp = wxBitmap(bmp.ConvertToImage(), -1, scale); +#endif + (i == 0 ? m_off : m_on).SetBitmap(bmp); + } + } + + update(); +} + +void SwitchButton::SysColorChange() +{ + m_on.sys_color_changed(); + m_off.sys_color_changed(); + + update(); +} + +void SwitchButton::update() +{ + SetBitmap((GetValue() ? m_on : m_off).bmp()); + update_size(); +} diff --git a/src/slic3r/GUI/Widgets/SwitchButton.hpp b/src/slic3r/GUI/Widgets/SwitchButton.hpp new file mode 100644 index 0000000..5c1859e --- /dev/null +++ b/src/slic3r/GUI/Widgets/SwitchButton.hpp @@ -0,0 +1,42 @@ +#ifndef slic3r_GUI_SwitchButton_hpp_ +#define slic3r_GUI_SwitchButton_hpp_ + +#include "../wxExtensions.hpp" +#include "StateColor.hpp" + +#include "BitmapToggleButton.hpp" + +class SwitchButton : public BitmapToggleButton +{ +public: + SwitchButton(wxWindow * parent = NULL, const wxString& name = wxEmptyString, wxWindowID id = wxID_ANY); + +public: + void SetLabels(wxString const & lbl_on, wxString const & lbl_off); + + void SetTextColor(StateColor const &color); + + void SetTrackColor(StateColor const &color); + + void SetThumbColor(StateColor const &color); + + void SetValue(bool value) override; + + void Rescale(); + + void SysColorChange(); + +private: + void update() override; + +private: + ScalableBitmap m_on; + ScalableBitmap m_off; + + wxString labels[2]; + StateColor text_color; + StateColor track_color; + StateColor thumb_color; +}; + +#endif // !slic3r_GUI_SwitchButton_hpp_ diff --git a/src/slic3r/GUI/Widgets/TextInput.cpp b/src/slic3r/GUI/Widgets/TextInput.cpp index 08bfb4f..f31f348 100644 --- a/src/slic3r/GUI/Widgets/TextInput.cpp +++ b/src/slic3r/GUI/Widgets/TextInput.cpp @@ -1,9 +1,10 @@ #include "TextInput.hpp" -#include "Label.hpp" -#include "TextCtrl.h" -#include "slic3r/GUI/Widgets/Label.hpp" +#include "UIColors.hpp" #include +#include + +#include "slic3r/GUI/GUI_App.hpp" BEGIN_EVENT_TABLE(TextInput, wxPanel) @@ -23,12 +24,9 @@ TextInput::TextInput() , text_color(std::make_pair(0x909090, (int) StateColor::Disabled), std::make_pair(0x262E30, (int) StateColor::Normal)) { + if (Slic3r::GUI::wxGetApp().suppress_round_corners()) radius = 0; border_width = 1; - border_color = StateColor(std::make_pair(0xDBDBDB, (int) StateColor::Disabled), std::make_pair(0x009688, (int) StateColor::Hovered), - std::make_pair(0xDBDBDB, (int) StateColor::Normal)); - background_color = StateColor(std::make_pair(0xF0F0F1, (int) StateColor::Disabled), std::make_pair(*wxWHITE, (int) StateColor::Normal)); - SetFont(Label::Body_12); } TextInput::TextInput(wxWindow * parent, @@ -54,14 +52,17 @@ void TextInput::Create(wxWindow * parent, text_ctrl = nullptr; StaticBox::Create(parent, wxID_ANY, pos, size, style); wxWindow::SetLabel(label); - style &= ~wxRIGHT; state_handler.attach({&label_color, & text_color}); state_handler.update_binds(); - text_ctrl = new TextCtrl(this, wxID_ANY, text, {4, 4}, wxDefaultSize, style | wxBORDER_NONE | wxTE_PROCESS_ENTER); - text_ctrl->SetFont(Label::Body_14); + text_ctrl = new wxTextCtrl(this, wxID_ANY, text, {4, 4}, size, style | wxBORDER_NONE); +#ifdef __WXOSX__ + text_ctrl->OSXDisableAllSmartSubstitutions(); +#endif // __WXOSX__ text_ctrl->SetInitialSize(text_ctrl->GetBestSize()); - text_ctrl->SetBackgroundColour(background_color.colorForStates(state_handler.states())); - text_ctrl->SetForegroundColour(text_color.colorForStates(state_handler.states())); + if (parent) { + SetBackgroundColour(parent->GetBackgroundColour()); + SetForegroundColour(parent->GetForegroundColour()); + } state_handler.attach_child(text_ctrl); text_ctrl->Bind(wxEVT_KILL_FOCUS, [this](auto &e) { OnEdit(); @@ -74,18 +75,17 @@ void TextInput::Create(wxWindow * parent, e.SetId(GetId()); ProcessEventLocally(e); }); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu + text_ctrl->Bind(wxEVT_TEXT, [this](auto &e) { + e.SetId(GetId()); + ProcessEventLocally(e); + }); + text_ctrl->Bind(wxEVT_RIGHT_DOWN, [](auto &e) {}); // disable context menu if (!icon.IsEmpty()) { - this->icon = ScalableBitmap(this, icon.ToStdString(), 16); + this->drop_down_icon = ScalableBitmap(this, icon.ToStdString(), 16); } messureSize(); } -void TextInput::SetCornerRadius(double radius) -{ - this->radius = radius; - Refresh(); -} void TextInput::SetLabel(const wxString& label) { @@ -94,10 +94,54 @@ void TextInput::SetLabel(const wxString& label) Refresh(); } -void TextInput::SetIcon(const wxBitmap &icon) +bool TextInput::SetBackgroundColour(const wxColour& colour) { - this->icon.get_bitmap() = icon; - Rescale(); + const int clr_background_disabled = Slic3r::GUI::wxGetApp().dark_mode() ? clr_background_disabled_dark : clr_background_disabled_light; + const StateColor clr_state( std::make_pair(clr_background_disabled, (int)StateColor::Disabled), + std::make_pair(clr_background_focused, (int)StateColor::Checked), + std::make_pair(colour, (int)StateColor::Focused), + std::make_pair(colour, (int)StateColor::Normal)); + + SetBackgroundColor(clr_state); + if (text_ctrl) + text_ctrl->SetBackgroundColour(colour); + + return true; +} + +bool TextInput::SetForegroundColour(const wxColour& colour) +{ + const StateColor clr_state( std::make_pair(clr_foreground_disabled, (int)StateColor::Disabled), + std::make_pair(colour, (int)StateColor::Normal)); + + SetLabelColor(clr_state); + SetTextColor (clr_state); + + return true; +} + +void TextInput::SetValue(const wxString& value) +{ + if (text_ctrl) + text_ctrl->SetValue(value); +} + +wxString TextInput::GetValue() +{ + if (text_ctrl) + return text_ctrl->GetValue(); + return wxEmptyString; +} + +void TextInput::SetSelection(long from, long to) +{ + if (text_ctrl) + text_ctrl->SetSelection(from, to); +} + +void TextInput::SetIcon(const wxBitmapBundle& icon_in) +{ + icon = icon_in; } void TextInput::SetLabelColor(StateColor const &color) @@ -110,16 +154,37 @@ void TextInput::SetTextColor(StateColor const& color) { text_color= color; state_handler.update_binds(); + if (text_ctrl) + text_ctrl->SetForegroundColour(text_color.colorForStates(state_handler.states())); +} + +void TextInput::SetBGColor(StateColor const& color) +{ + background_color = color; + state_handler.update_binds(); +} + +void TextInput::SetCtrlSize(wxSize const& size) +{ + StaticBox::SetInitialSize(size); + Rescale(); } void TextInput::Rescale() { - if (!this->icon.name().empty()) - this->icon.msw_rescale(); + if (text_ctrl) + text_ctrl->SetInitialSize(text_ctrl->GetBestSize()); messureSize(); Refresh(); } +bool TextInput::SetFont(const wxFont& font) +{ + bool ret = StaticBox::SetFont(font); + if (text_ctrl) + return ret && text_ctrl->SetFont(font); + return ret; +} bool TextInput::Enable(bool enable) { bool result = text_ctrl->Enable(enable) && wxWindow::Enable(enable); @@ -151,16 +216,21 @@ void TextInput::DoSetSize(int x, int y, int width, int height, int sizeFlags) if (sizeFlags & wxSIZE_USE_EXISTING) return; wxSize size = GetSize(); wxPoint textPos = {5, 0}; - if (this->icon.get_bitmap().IsOk()) { - wxSize szIcon = this->icon.GetSize(); + if (this->icon.IsOk()) { + wxSize szIcon = get_preferred_size(icon, m_parent); textPos.x += szIcon.x; } + wxSize dd_icon_size = wxSize(0,0); + if (this->drop_down_icon.bmp().IsOk()) + dd_icon_size = this->drop_down_icon.GetSize(); bool align_right = GetWindowStyle() & wxRIGHT; if (align_right) textPos.x += labelSize.x; if (text_ctrl) { wxSize textSize = text_ctrl->GetSize(); - textSize.x = size.x - textPos.x - labelSize.x - 10; + wxClientDC dc(this); + const int r_shift = int((dd_icon_size.x == 0 ? 3. : 2.) * dc.GetContentScaleFactor()); + textSize.x = size.x - textPos.x - labelSize.x - dd_icon_size.x - r_shift; text_ctrl->SetSize(textSize); text_ctrl->SetPosition({textPos.x, (size.y - textSize.y) / 2}); } @@ -192,28 +262,38 @@ void TextInput::render(wxDC& dc) bool align_right = GetWindowStyle() & wxRIGHT; // start draw wxPoint pt = {5, 0}; - if (icon.get_bitmap().IsOk()) { - wxSize szIcon = icon.GetSize(); + if (icon.IsOk()) { + wxSize szIcon = get_preferred_size(icon, m_parent); pt.y = (size.y - szIcon.y) / 2; - dc.DrawBitmap(icon.get_bitmap(), pt); - pt.x += szIcon.x + 0; +#ifdef __WXGTK3__ + dc.DrawBitmap(icon.GetBitmap(szIcon), pt); +#else + dc.DrawBitmap(icon.GetBitmapFor(m_parent), pt); +#endif + pt.x += szIcon.x + 5; + } + + // drop_down_icon draw + wxPoint pt_r = {size.x, 0}; + if (drop_down_icon.bmp().IsOk()) { + wxSize szIcon = drop_down_icon.GetSize(); + pt_r.x -= szIcon.x + 2; + pt_r.y = (size.y - szIcon.y) / 2; + dc.DrawBitmap(drop_down_icon.get_bitmap(), pt_r); } auto text = wxWindow::GetLabel(); if (!text.IsEmpty()) { wxSize textSize = text_ctrl->GetSize(); if (align_right) { - if (pt.x + labelSize.x > size.x) - text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - pt.x); - pt.y = (size.y - labelSize.y) / 2; - } else { pt.x += textSize.x; pt.y = (size.y + textSize.y) / 2 - labelSize.y; + } else { + if (pt.x + labelSize.x > pt_r.x) + text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, pt_r.x - pt.x); + pt.y = (size.y - labelSize.y) / 2; } dc.SetTextForeground(label_color.colorForStates(states)); - if(align_right) dc.SetFont(GetFont()); - else - dc.SetFont(Label::Body_12); dc.DrawText(text, pt); } } @@ -222,17 +302,10 @@ void TextInput::messureSize() { wxSize size = GetSize(); wxClientDC dc(this); - bool align_right = GetWindowStyle() & wxRIGHT; - if (align_right) - dc.SetFont(GetFont()); - else - dc.SetFont(Label::Body_12); labelSize = dc.GetTextExtent(wxWindow::GetLabel()); - wxSize textSize = text_ctrl->GetSize(); - int h = textSize.y + 8; - if (size.y < h) { - size.y = h; - } + const wxSize textSize = text_ctrl->GetSize(); + const wxSize iconSize = drop_down_icon.bmp().IsOk() ? drop_down_icon.GetSize() : wxSize(0, 0); + size.y = ((textSize.y > iconSize.y) ? textSize.y : iconSize.y) + 8; wxSize minSize = size; minSize.x = GetMinWidth(); SetMinSize(minSize); diff --git a/src/slic3r/GUI/Widgets/TextInput.hpp b/src/slic3r/GUI/Widgets/TextInput.hpp index 1013e7c..b35a6ee 100644 --- a/src/slic3r/GUI/Widgets/TextInput.hpp +++ b/src/slic3r/GUI/Widgets/TextInput.hpp @@ -8,10 +8,11 @@ class TextInput : public wxNavigationEnabled { wxSize labelSize; - ScalableBitmap icon; + wxBitmapBundle icon; + ScalableBitmap drop_down_icon; StateColor label_color; StateColor text_color; - wxTextCtrl * text_ctrl; + wxTextCtrl* text_ctrl{nullptr}; static const int TextInputWidth = 200; static const int TextInputHeight = 50; @@ -36,34 +37,44 @@ public: const wxSize & size = wxDefaultSize, long style = 0); - void SetCornerRadius(double radius); + void SetLabel(const wxString& label) wxOVERRIDE; - void SetLabel(const wxString& label); - - void SetIcon(const wxBitmap &icon); + void SetIcon(const wxBitmapBundle& icon); void SetLabelColor(StateColor const &color); + void SetBGColor(StateColor const &color); void SetTextColor(StateColor const &color); + void SetCtrlSize(wxSize const& size); virtual void Rescale(); + bool SetFont(const wxFont &font) override; virtual bool Enable(bool enable = true) override; virtual void SetMinSize(const wxSize& size) override; + bool SetBackgroundColour(const wxColour &colour) override; + + bool SetForegroundColour(const wxColour &colour) override; wxTextCtrl *GetTextCtrl() { return text_ctrl; } wxTextCtrl const *GetTextCtrl() const { return text_ctrl; } + void SetValue(const wxString& value); + + wxString GetValue(); + + void SetSelection(long from, long to); protected: virtual void OnEdit() {} - virtual void DoSetSize( - int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); + void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) wxOVERRIDE; void DoSetToolTipText(wxString const &tip) override; + StateColor GetTextColor() const { return text_color; } + StateColor GetBorderColor() const { return border_color; } private: void paintEvent(wxPaintEvent& evt); diff --git a/src/slic3r/GUI/Widgets/UIColors.hpp b/src/slic3r/GUI/Widgets/UIColors.hpp new file mode 100644 index 0000000..14ecb6f --- /dev/null +++ b/src/slic3r/GUI/Widgets/UIColors.hpp @@ -0,0 +1,18 @@ +#ifndef slic3r_UI_Colors_hpp_ +#define slic3r_UI_Colors_hpp_ + +static const int clr_border_normal = 0x646464;//0xDBDBDB; +static const int clr_border_hovered = 0xED6B21;//0x00AE42; +static const int clr_border_disabled = 0x646464;//0xDBDBDB; + +static const int clr_background_normal_light = 0xFFFFFF; +static const int clr_background_normal_dark = 0x2B2B2B;//0x434343; +static const int clr_background_focused = 0xED6B21;//0xEDFAF2; +static const int clr_background_disabled_dark = 0x404040;//0xF0F0F0; +static const int clr_background_disabled_light = 0xD9D9D9;//0xF0F0F0; + +static const int clr_foreground_normal = 0x262E30; +static const int clr_foreground_focused = 0x00AE42; +static const int clr_foreground_disabled = 0x909090; + +#endif // !slic3r_UI_Colors_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/WifiConfigDialog.cpp b/src/slic3r/GUI/WifiConfigDialog.cpp new file mode 100644 index 0000000..6b088d8 --- /dev/null +++ b/src/slic3r/GUI/WifiConfigDialog.cpp @@ -0,0 +1,304 @@ +#include "WifiConfigDialog.hpp" + +#include "GUI_App.hpp" +#include "GUI.hpp" +#include "I18N.hpp" +#include "format.hpp" +#include "RemovableDriveManager.hpp" +#include "MsgDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "Widgets/ComboBox.hpp" + +namespace Slic3r { +namespace GUI { + +const char* WIFI_CONFIGFILE_NAME = "prusa_printer_settings.ini"; + +WifiConfigDialog::WifiConfigDialog(wxWindow* parent, std::string& file_path, RemovableDriveManager* removable_manager, const wxString& preffered_drive) + // TRN: This is the dialog title. + : DPIDialog(parent, wxID_ANY, _L("Wi-Fi Configuration File Generator"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , m_wifi_scanner(new WifiScanner()) + , out_file_path(file_path) + , m_removable_manager(removable_manager) +{ + wxPanel* panel = new wxPanel(this); + wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL); + panel->SetSizer(vsizer); + + // TRN Wifi config dialog explanation line 1. + wxStaticText* explain_label1 = new wxStaticText(panel, wxID_ANY, _L("Generate a file to be loaded by a Prusa printer to configure its Wi-Fi connection.")); + // TRN Wifi config dialog explanation line 2. + wxStaticText* explain_label2 = new wxStaticText(panel, wxID_ANY, GUI::format_wxstr(_L("Write this file on the USB flash drive. Its name will be %1%."), WIFI_CONFIGFILE_NAME)); + // TRN Wifi config dialog explanation line 3. + wxStaticText* explain_label3 = new wxStaticText(panel, wxID_ANY, _L("Your Prusa printer should load this file automatically.")); + // TRN Wifi config dialog explanation line 4. + wxStaticText* explain_label4 = new wxStaticText(panel, wxID_ANY, _L("Note: This file will contain the SSID and password in plain text.")); + + auto* ssid_sizer = new wxBoxSizer(wxHORIZONTAL); + // TRN SSID of WiFi network. It is a standard abbreviation which should probably not change in most languages. + wxStaticText* ssid_label = new wxStaticText(panel, wxID_ANY, GUI::format_wxstr("%1%:", _L("SSID"))); + m_ssid_combo = new ::ComboBox(panel, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON); +#if __APPLE__ + m_ssid_combo->SetToolTip(_L("On some versions of MacOS, this only loads SSID of connected network.")); +#endif // __APPLE__ + rescan_networks(false); + m_ssid_button_id = NewControlId(); + // TRN Text of button to rescan visible networks in Wifi Config dialog. + wxButton* ssid_button = new wxButton(panel, m_ssid_button_id, _(L("Rescan"))); + ssid_sizer->Add(m_ssid_combo, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10); + ssid_sizer->Add(ssid_button, 0); + + auto* pass_sizer = new wxBoxSizer(wxHORIZONTAL); + // TRN Password of WiFi network. + wxStaticText* password_label = new wxStaticText(panel, wxID_ANY, GUI::format_wxstr("%1%:", _L("Password"))); + m_pass_textctrl = new ::TextInput(panel, "", "", "", wxDefaultPosition, wxDefaultSize); +#if __APPLE__ + pass_sizer->Add(m_pass_textctrl, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10); + m_pass_button_id = NewControlId(); + // TRN Text of button to retrieve password from keychain in Wifi Config dialog. Only on Mac. + wxButton* pass_button = new wxButton(panel, m_pass_button_id, _(L("Retrieve"))); + pass_sizer->Add(pass_button, 0); + pass_button->Bind(wxEVT_BUTTON, &WifiConfigDialog::on_retrieve_password, this); +#else + pass_sizer->Add(m_pass_textctrl, 1, wxALIGN_CENTER_VERTICAL, 10); +#endif // __APPLE__ + // show password if current ssid was selected already + fill_password(); + + auto* drive_sizer = new wxBoxSizer(wxHORIZONTAL); + // TRN description of Combo Box with path to USB drive. + wxStaticText* drive_label = new wxStaticText(panel, wxID_ANY, GUI::format_wxstr("%1%:", _L("Drive"))); + m_drive_combo = new ::ComboBox(panel, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON); + rescan_drives(preffered_drive); + m_drive_button_id = NewControlId(); + // TRN Text of button to rescan connect usb drives in Wifi Config dialog. + wxButton* drive_button = new wxButton(panel, m_drive_button_id, _(L("Rescan"))); + drive_sizer->Add(m_drive_combo, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10); + drive_sizer->Add(drive_button, 0); + + // TRN Text of button to write config file in Wifi Config dialog. + wxButton* ok_button = new wxButton(panel, wxID_OK, _L("Write")); + wxButton* cancel_button = new wxButton(panel, wxID_CANCEL); + + auto* grid = new wxFlexGridSizer(2, 15, 15); + grid->AddGrowableCol(1); + + grid->Add(ssid_label, 0, wxALIGN_CENTER_VERTICAL); + grid->Add(ssid_sizer, 0, wxEXPAND); + + grid->Add(password_label, 0, wxALIGN_CENTER_VERTICAL); + grid->Add(pass_sizer, 0, wxEXPAND); + + grid->Add(drive_label, 0, wxALIGN_CENTER_VERTICAL); + grid->Add(drive_sizer, 0, wxEXPAND); + + vsizer->Add(explain_label1, 0, wxALIGN_CENTER_VERTICAL); + vsizer->Add(explain_label2, 0, wxALIGN_CENTER_VERTICAL); + vsizer->Add(explain_label3, 0, wxALIGN_CENTER_VERTICAL); + vsizer->Add(explain_label4, 0, wxALIGN_CENTER_VERTICAL); + vsizer->Add(grid, 0, wxEXPAND | wxTOP | wxBOTTOM, 15); + + wxBoxSizer* buttons_sizer = new wxBoxSizer(wxHORIZONTAL); + buttons_sizer->Add(ok_button, 1, wxLEFT); + buttons_sizer->AddStretchSpacer(); + buttons_sizer->Add(cancel_button, 1, wxRIGHT); + + vsizer->Add(buttons_sizer, 0, wxEXPAND); + + wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); + topsizer->Add(panel, 1, wxEXPAND | wxALL, 15); + SetSizerAndFit(topsizer); + + ok_button->Bind(wxEVT_BUTTON, &WifiConfigDialog::on_ok, this); + m_ssid_combo->Bind(wxEVT_TEXT, &WifiConfigDialog::on_combo, this); + drive_button->Bind(wxEVT_BUTTON, &WifiConfigDialog::on_rescan_drives, this); + ssid_button->Bind(wxEVT_BUTTON, &WifiConfigDialog::on_rescan_networks, this); + + wxGetApp().UpdateDlgDarkUI(this); +} + +WifiConfigDialog::~WifiConfigDialog() +{ + if (m_wifi_scanner) + delete m_wifi_scanner; +} + +void WifiConfigDialog::on_combo(wxCommandEvent& e) +{ + fill_password(); +} + +void WifiConfigDialog::fill_password() +{ + assert(m_ssid_combo && m_pass_textctrl && m_wifi_scanner); + if (auto it = m_wifi_scanner->get_map().find(m_ssid_combo->GetValue()); it != m_wifi_scanner->get_map().end()) + m_pass_textctrl->SetValue(boost::nowide::widen(it->second)); +} + +void WifiConfigDialog::on_retrieve_password(wxCommandEvent& e) +{ + if (m_ssid_combo->GetValue().empty()) { + return; + } + + std::string psk = m_wifi_scanner->get_psk(boost::nowide::narrow(m_ssid_combo->GetValue())); + if (psk.empty()) { + // TRN Alert message when retrieving password for wifi from keychain. Probably will display only on Apple so keychain is MacOS term. + wxString msg = _L("No password in the keychain for given SSID."); + show_info(nullptr, msg); + return; + } + m_pass_textctrl->SetValue(boost::nowide::widen(psk)); +} + +void WifiConfigDialog::on_rescan_drives(wxCommandEvent& e) +{ + rescan_drives({}); +} + +void WifiConfigDialog::rescan_drives(const wxString& preffered_drive) +{ + assert(m_drive_combo && m_removable_manager); + m_drive_combo->Clear(); + std::vector ddata = m_removable_manager->get_drive_list(); + for (const auto& data : ddata) { + wxString item = boost::nowide::widen(data.path); + m_drive_combo->Append(item); + if (preffered_drive == item) + m_ssid_combo->Select(m_ssid_combo->GetCount() - 1); + } + if (m_drive_combo->GetSelection() == -1 && m_drive_combo->GetCount() > 0) { + m_drive_combo->Select(0); + } +} + +void WifiConfigDialog::on_rescan_networks(wxCommandEvent& e) +{ + rescan_networks(true); +} + +void WifiConfigDialog::rescan_networks(bool select) +{ + assert(m_ssid_combo && m_wifi_scanner); + m_wifi_scanner->scan(); + std::string current = m_wifi_scanner->get_current_ssid(); + const auto& map = m_wifi_scanner->get_map(); + m_ssid_combo->Clear(); + for (const auto pair : map) { + m_ssid_combo->Append(pair.first); + // select ssid of current network (if connected) + if (current == pair.first) + m_ssid_combo->Select(m_ssid_combo->GetCount() - 1); + } + if (m_ssid_combo->GetSelection() == -1 && m_ssid_combo->GetCount() > 0) + m_ssid_combo->Select(0); + if (select && m_ssid_combo->GetSelection() != -1) + fill_password(); +} + +void WifiConfigDialog::on_ok(wxCommandEvent& e) +{ + assert(m_ssid_combo && m_pass_textctrl); + if (m_ssid_combo->GetValue().empty()) { + // TRN Alert message when writing WiFi configuration file to usb drive. + wxString msg = _L("SSID field is empty."); + show_info(nullptr, msg); + return; + } + + std::string selected_path = boost::nowide::narrow(m_drive_combo->GetValue()); + + if (selected_path.empty()) { + // TRN Alert message when writing WiFi configuration file to usb drive. + wxString msg = _L("Drive field is empty."); + show_info(nullptr, msg); + return; + } + + boost::filesystem::path file_path = boost::filesystem::path(selected_path) / WIFI_CONFIGFILE_NAME; + + bool path_on_removable_media = m_removable_manager->set_and_verify_last_save_path(file_path.string()); + if (!path_on_removable_media) { + // TRN Alert message when writing WiFi configuration file to usb drive. + wxString msg = _L("Selected path is not on removable media."); + show_info(nullptr, msg); + return; + } + + if (boost::filesystem::exists(file_path)) { + // TRN placeholder 1 is path to file + wxString msg_text = GUI::format_wxstr(_L("%1% already exists. Do you want to rewrite it?\n(Other items than Wi-Fi credentials will stay unchanged)"), file_path.string()); + WarningDialog dialog(m_parent, msg_text, _L("Warning"), wxYES | wxNO); + if (dialog.ShowModal() == wxID_NO) { + return; + } + } + + std::string data; + namespace pt = boost::property_tree; + pt::ptree tree; + // File already exist and we only need to add data to it rather than rewrite it. + if (boost::filesystem::exists(file_path)) { + + boost::nowide::ifstream ifs(file_path.string()); + try { + pt::read_ini(ifs, tree); + } + catch (const boost::property_tree::ini_parser::ini_parser_error& err) { + throw Slic3r::RuntimeError(format("Failed loading ini file \"%1%\"\nError: \"%2%\" at line %3%", file_path, err.message(), err.line()).c_str()); + } + + if (auto sub = tree.get_optional("wifi"); sub) { + tree.erase("wifi"); + } + } + + pt::ptree subtree; + subtree.put("ssid", m_ssid_combo->GetValue().utf8_string()); + subtree.put("psk", m_pass_textctrl->GetValue().utf8_string()); + tree.add_child("wifi", subtree); + + data.clear(); + // manually write ini string (so there is extra line between sections) + for (const auto& section : tree) { + data += "[" + section.first + "]" + "\n"; + for (const auto& entry : section.second) { + data += entry.first + " = " + entry.second.get_value() + "\n"; + } + data += "\n"; + } + + m_used_path = boost::nowide::widen(file_path.string()); + FILE* file; + file = fopen(file_path.string().c_str(), "w"); + if (file == NULL) { + BOOST_LOG_TRIVIAL(error) << "Failed to write to file " << file_path; + // TODO show error + show_error(nullptr, _L("Failed to open file for writing.")); + return; + } + fwrite(data.c_str(), 1, data.size(), file); + fclose(file); + out_file_path = file_path.string(); + this->EndModal(wxID_OK); +} + +void WifiConfigDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + SetFont(wxGetApp().normal_font()); + + const int em = em_unit(); + msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL, m_ssid_button_id, m_pass_button_id, m_drive_button_id }); + + Fit(); + Refresh(); +} +}}// Slicer::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/WifiConfigDialog.hpp b/src/slic3r/GUI/WifiConfigDialog.hpp new file mode 100644 index 0000000..532c8d4 --- /dev/null +++ b/src/slic3r/GUI/WifiConfigDialog.hpp @@ -0,0 +1,55 @@ +#ifndef slic3r_WifiConfigDialog_hpp_ +#define slic3r_WifiConfigDialog_hpp_ + +#include "GUI_Utils.hpp" + +#include "../Utils/WifiScanner.hpp" + +#include +#include +#include +#include + +#include "Widgets/ComboBox.hpp" +#include "Widgets/TextInput.hpp" + +namespace Slic3r { +namespace GUI { + +class RemovableDriveManager; +class WifiConfigDialog : public DPIDialog +{ +public: + WifiConfigDialog(wxWindow* parent, std::string& file_path, RemovableDriveManager* removable_manager, const wxString& preffered_drive ); + ~WifiConfigDialog(); + wxString get_used_path() const { return m_used_path; } + +private: + ::ComboBox* m_ssid_combo {nullptr}; + ::TextInput* m_pass_textctrl {nullptr}; + ::ComboBox* m_drive_combo {nullptr}; + // reference to string that is filled after ShowModal is called from owner + std::string& out_file_path; + WifiScanner* m_wifi_scanner; + RemovableDriveManager* m_removable_manager; + wxString m_used_path; + int m_ssid_button_id {wxID_ANY}; + int m_pass_button_id {wxID_ANY}; + int m_drive_button_id {wxID_ANY}; + + void on_ok(wxCommandEvent& e); + void on_combo(wxCommandEvent& e); + void on_rescan_drives(wxCommandEvent& e); + void on_rescan_networks(wxCommandEvent& e); + void on_retrieve_password(wxCommandEvent& e); + void rescan_drives(const wxString& preffered_drive); + void rescan_networks(bool select); + void fill_password(); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + void on_sys_color_changed() override {} +}; + +}} // Slicer::GUI +#endif \ No newline at end of file diff --git a/src/slic3r/GUI/WipeTowerDialog.cpp b/src/slic3r/GUI/WipeTowerDialog.cpp index 4d117ff..6f5915d 100644 --- a/src/slic3r/GUI/WipeTowerDialog.cpp +++ b/src/slic3r/GUI/WipeTowerDialog.cpp @@ -11,17 +11,20 @@ #include -int scale(const int val) { return val * Slic3r::GUI::wxGetApp().em_unit(); } +using namespace Slic3r::GUI; + +int scale(const int val) { return val * wxGetApp().em_unit(); } int ITEM_WIDTH() { return scale(6); } static void update_ui(wxWindow* window) { - Slic3r::GUI::wxGetApp().UpdateDarkUI(window); + wxGetApp().UpdateDarkUI(window); } RammingDialog::RammingDialog(wxWindow* parent,const std::string& parameters) : wxDialog(parent, wxID_ANY, _(L("Ramming customization")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE/* | wxRESIZE_BORDER*/) { + SetFont(wxGetApp().normal_font()); update_ui(this); m_panel_ramming = new RammingPanel(this,parameters); @@ -38,7 +41,10 @@ RammingDialog::RammingDialog(wxWindow* parent,const std::string& parameters) auto main_sizer = new wxBoxSizer(wxVERTICAL); main_sizer->Add(m_panel_ramming, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); - main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxBOTTOM, 10); + auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxGetApp().SetWindowVariantForButton(buttons->GetAffirmativeButton()); + wxGetApp().SetWindowVariantForButton(buttons->GetCancelButton()); + main_sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxBOTTOM, 10); SetSizer(main_sizer); main_sizer->SetSizeHints(this); @@ -52,8 +58,7 @@ RammingDialog::RammingDialog(wxWindow* parent,const std::string& parameters) EndModal(wxID_OK); },wxID_OK); this->Show(); -// wxMessageDialog dlg(this, _(L("Ramming denotes the rapid extrusion just before a tool change in a single-extruder MM printer. Its purpose is to " - Slic3r::GUI::MessageDialog dlg(this, _(L("Ramming denotes the rapid extrusion just before a tool change in a single-extruder MM printer. Its purpose is to " + MessageDialog dlg(this, _(L("Ramming denotes the rapid extrusion just before a tool change in a single-extruder MM printer. Its purpose is to " "properly shape the end of the unloaded filament so it does not prevent insertion of the new filament and can itself " "be reinserted later. This phase is important and different materials can require different extrusion speeds to get " "the good shape. For this reason, the extrusion rates during ramming are adjustable.\n\nThis is an expert-level " @@ -100,10 +105,11 @@ RammingPanel::RammingPanel(wxWindow* parent, const std::string& parameters) #endif sizer_chart->Add(m_chart, 0, wxALL, 5); - m_widget_time = new wxSpinCtrlDouble(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(ITEM_WIDTH(), -1),style,0.,5.0,3.,0.5); - m_widget_volume = new wxSpinCtrl(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(ITEM_WIDTH(), -1),style,0,10000,0); - m_widget_ramming_line_width_multiplicator = new wxSpinCtrl(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(ITEM_WIDTH(), -1),style,10,200,100); - m_widget_ramming_step_multiplicator = new wxSpinCtrl(this,wxID_ANY,wxEmptyString,wxDefaultPosition,wxSize(ITEM_WIDTH(), -1),style,10,200,100); + m_widget_time = new ::SpinInputDouble(this,"", wxEmptyString, wxDefaultPosition, wxSize(ITEM_WIDTH(), -1), style, 0., 5., 3., 0.5); + m_widget_time->SetDigits(2); + m_widget_volume = new ::SpinInput(this,"",wxEmptyString,wxDefaultPosition,wxSize(ITEM_WIDTH(), -1),style,0,10000,0); + m_widget_ramming_line_width_multiplicator = new ::SpinInput(this,"",wxEmptyString,wxDefaultPosition,wxSize(ITEM_WIDTH(), -1),style,10,200,100); + m_widget_ramming_step_multiplicator = new ::SpinInput(this,"",wxEmptyString,wxDefaultPosition,wxSize(ITEM_WIDTH(), -1),style,10,200,100); #ifdef _WIN32 update_ui(m_widget_time->GetText()); @@ -127,7 +133,6 @@ RammingPanel::RammingPanel(wxWindow* parent, const std::string& parameters) sizer_param->Add(gsizer_param, 0, wxTOP, scale(10)); m_widget_time->SetValue(m_chart->get_time()); - m_widget_time->SetDigits(2); m_widget_volume->SetValue(m_chart->get_volume()); m_widget_volume->Disable(); m_widget_ramming_line_width_multiplicator->SetValue(m_ramming_line_width_multiplicator); @@ -143,8 +148,9 @@ RammingPanel::RammingPanel(wxWindow* parent, const std::string& parameters) sizer->SetSizeHints(this); SetSizer(sizer); - m_widget_time->Bind(wxEVT_TEXT,[this](wxCommandEvent&) {m_chart->set_xy_range(m_widget_time->GetValue(),-1);}); + m_widget_time->Bind(wxEVT_SPINCTRL,[this](wxCommandEvent&) { m_chart->set_xy_range(m_widget_time->GetValue(),-1); }); m_widget_time->Bind(wxEVT_CHAR,[](wxKeyEvent&){}); // do nothing - prevents the user to change the value + m_widget_time->GetText()->Bind(wxEVT_CHAR,[](wxKeyEvent&){}); // do nothing - prevents the user to change the value m_widget_volume->Bind(wxEVT_CHAR,[](wxKeyEvent&){}); // do nothing - prevents the user to change the value Bind(EVT_WIPE_TOWER_CHART_CHANGED,[this](wxCommandEvent&) {m_widget_volume->SetValue(m_chart->get_volume()); m_widget_time->SetValue(m_chart->get_time());} ); Refresh(true); // erase background @@ -174,9 +180,11 @@ std::string RammingPanel::get_parameters() WipingDialog::WipingDialog(wxWindow* parent, const std::vector& matrix, const std::vector& extruders, const std::vector& extruder_colours) : wxDialog(parent, wxID_ANY, _(L("Wipe tower - Purging volume adjustment")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE/* | wxRESIZE_BORDER*/) { + SetFont(wxGetApp().normal_font()); update_ui(this); auto widget_button = new wxButton(this,wxID_ANY,"-",wxPoint(0,0),wxDefaultSize); update_ui(widget_button); + wxGetApp().SetWindowVariantForButton(widget_button); m_panel_wiping = new WipingPanel(this,matrix,extruders, extruder_colours, widget_button); auto main_sizer = new wxBoxSizer(wxVERTICAL); @@ -187,7 +195,10 @@ WipingDialog::WipingDialog(wxWindow* parent, const std::vector& matrix, c main_sizer->Add(m_panel_wiping, 0, wxEXPAND | wxALL, 5); main_sizer->Add(widget_button, 0, wxALIGN_CENTER_HORIZONTAL | wxCENTER | wxBOTTOM, 5); - main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10); + auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxGetApp().SetWindowVariantForButton(buttons->GetAffirmativeButton()); + wxGetApp().SetWindowVariantForButton(buttons->GetCancelButton()); + main_sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10); SetSizer(main_sizer); main_sizer->SetSizeHints(this); @@ -310,35 +321,12 @@ WipingPanel::WipingPanel(wxWindow* parent, const std::vector& matrix, con gridsizer_simple->Add(new wxStaticText(m_page_simple, wxID_ANY, wxString(_(L("unloaded")))), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); gridsizer_simple->Add(new wxStaticText(m_page_simple,wxID_ANY,wxString(_(L("loaded")))), 0, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); - auto add_spin_ctrl = [this](std::vector& vec, float initial) + auto add_spin_ctrl = [this](std::vector<::SpinInput*>& vec, float initial) { - wxSpinCtrl* spin_ctrl = new wxSpinCtrl(m_page_simple, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(ITEM_WIDTH(), -1), style | wxALIGN_RIGHT, 0, 300, (int)initial); + ::SpinInput* spin_ctrl = new ::SpinInput(m_page_simple, "", wxEmptyString, wxDefaultPosition, wxSize(ITEM_WIDTH(), -1), style | wxALIGN_RIGHT, 0, 300, (int)initial); update_ui(spin_ctrl); vec.push_back(spin_ctrl); -#ifdef __WXOSX__ - // On OSX / Cocoa, wxSpinCtrl::GetValue() doesn't return the new value - // when it was changed from the text control, so the on_change callback - // gets the old one, and on_kill_focus resets the control to the old value. - // As a workaround, we get the new value from $event->GetString and store - // here temporarily so that we can return it from get_value() - spin_ctrl->Bind(wxEVT_TEXT, ([spin_ctrl](wxCommandEvent e) - { - long value; - const bool parsed = e.GetString().ToLong(&value); - int tmp_value = parsed && value >= INT_MIN && value <= INT_MAX ? (int)value : INT_MIN; - - // Forcibly set the input value for SpinControl, since the value - // inserted from the keyboard or clipboard is not updated under OSX - if (tmp_value != INT_MIN) { - spin_ctrl->SetValue(tmp_value); - - // But in SetValue() is executed m_text_ctrl->SelectAll(), so - // discard this selection and set insertion point to the end of string - spin_ctrl->GetText()->SetInsertionPointEnd(); - } - }), spin_ctrl->GetId()); -#endif }; for (unsigned int i=0;i& matrix, con hsizer->AddSpacer(10); hsizer->Add(new wxStaticText(m_page_simple, wxID_ANY, wxString(_(L("Tool #"))) << i + 1 << ": "), 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - gridsizer_simple->Add(hsizer, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL); + gridsizer_simple->Add(hsizer, 1, wxEXPAND); gridsizer_simple->Add(m_old.back(),0); gridsizer_simple->Add(m_new.back(),0); } @@ -440,8 +428,7 @@ bool WipingPanel::advanced_matches_simple() { // Switches the dialog from simple to advanced mode and vice versa void WipingPanel::toggle_advanced(bool user_action) { if (m_advanced && !advanced_matches_simple() && user_action) { -// if (wxMessageDialog(this,wxString(_(L("Switching to simple settings will discard changes done in the advanced mode!\n\nDo you want to proceed?"))), - if (Slic3r::GUI::MessageDialog(this, _L("Switching to simple settings will discard changes done in the advanced mode!\n\nDo you want to proceed?"), + if (MessageDialog(this, _L("Switching to simple settings will discard changes done in the advanced mode!\n\nDo you want to proceed?"), _L("Warning"),wxYES_NO|wxICON_EXCLAMATION).ShowModal() != wxID_YES) return; } diff --git a/src/slic3r/GUI/WipeTowerDialog.hpp b/src/slic3r/GUI/WipeTowerDialog.hpp index 84b9921..46d4e5c 100644 --- a/src/slic3r/GUI/WipeTowerDialog.hpp +++ b/src/slic3r/GUI/WipeTowerDialog.hpp @@ -8,6 +8,7 @@ #include #include "RammingChart.hpp" +#include "Widgets/SpinInput.hpp" class RammingPanel : public wxPanel { @@ -18,10 +19,10 @@ public: private: Chart* m_chart = nullptr; - wxSpinCtrl* m_widget_volume = nullptr; - wxSpinCtrl* m_widget_ramming_line_width_multiplicator = nullptr; - wxSpinCtrl* m_widget_ramming_step_multiplicator = nullptr; - wxSpinCtrlDouble* m_widget_time = nullptr; + ::SpinInput* m_widget_volume = nullptr; + ::SpinInput* m_widget_ramming_line_width_multiplicator = nullptr; + ::SpinInput* m_widget_ramming_step_multiplicator = nullptr; + ::SpinInputDouble* m_widget_time = nullptr; int m_ramming_step_multiplicator; int m_ramming_line_width_multiplicator; @@ -56,8 +57,8 @@ private: void fill_in_matrix(); bool advanced_matches_simple(); - std::vector m_old; - std::vector m_new; + std::vector<::SpinInput*> m_old; + std::vector<::SpinInput*> m_new; std::vector> edit_boxes; std::vector m_colours; unsigned int m_number_of_extruders = 0; diff --git a/src/slic3r/GUI/calib_dlg.hpp b/src/slic3r/GUI/calib_dlg.hpp index 9eb9a36..6ab4cc2 100644 --- a/src/slic3r/GUI/calib_dlg.hpp +++ b/src/slic3r/GUI/calib_dlg.hpp @@ -5,9 +5,9 @@ #include "GUI_Utils.hpp" #include "Widgets/Button.hpp" #include "Widgets/Label.hpp" -#include "Widgets/RadioBox.hpp" +// #include "Widgets/RadioBox.hpp" #include "Widgets/RoundedRectangle.hpp" -#include "Widgets/CheckBoxInWT.hpp" +// #include "Widgets/CheckBoxInWT.hpp" #include "Widgets/ComboBox.hpp" #include "Widgets/TextInput.hpp" #include "GUI_App.hpp" diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index e48d323..a11cc20 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -367,9 +367,9 @@ void edit_tooltip(wxString& tooltip) /* Function for rescale of buttons in Dialog under MSW if dpi is changed. * btn_ids - vector of buttons identifiers */ -void msw_buttons_rescale(wxDialog* dlg, const int em_unit, const std::vector& btn_ids) +void msw_buttons_rescale(wxDialog* dlg, const int em_unit, const std::vector& btn_ids, double height_koef/* = 1.*/) { - const wxSize& btn_size = wxSize(-1, int(2.5f * em_unit + 0.5f)); + const wxSize& btn_size = wxSize(-1, int(2.5 * em_unit * height_koef + 0.5f)); for (int btn_id : btn_ids) { // There is a case [FirmwareDialog], when we have wxControl instead of wxButton @@ -417,10 +417,12 @@ static int scale() } #endif // __WXGTK2__ -wxBitmapBundle* get_bmp_bundle(const std::string& bmp_name_in, int px_cnt/* = 16*/, const std::string& new_color/* = std::string()*/) +wxBitmapBundle* get_bmp_bundle(const std::string& bmp_name_in, int width/* = 16*/, int height/* = -1*/, const std::string& new_color/* = std::string()*/) { #ifdef __WXGTK2__ - px_cnt *= scale(); + width *= scale(); + if (height > 0) + height *= scale(); #endif // __WXGTK2__ static Slic3r::GUI::BitmapCache cache; @@ -428,10 +430,12 @@ wxBitmapBundle* get_bmp_bundle(const std::string& bmp_name_in, int px_cnt/* = 16 std::string bmp_name = bmp_name_in; boost::replace_last(bmp_name, ".png", ""); + if (height < 0) + height = width; // Try loading an SVG first, then PNG if SVG is not found: - wxBitmapBundle* bmp = cache.from_svg(bmp_name, px_cnt, px_cnt, Slic3r::GUI::wxGetApp().dark_mode(), new_color); + wxBitmapBundle* bmp = cache.from_svg(bmp_name, width, height, Slic3r::GUI::wxGetApp().dark_mode(), new_color); if (bmp == nullptr) { - bmp = cache.from_png(bmp_name, px_cnt, px_cnt); + bmp = cache.from_png(bmp_name, width, height); if (!bmp) // Neither SVG nor PNG has been found, raise error throw Slic3r::RuntimeError("Could not load bitmap: " + bmp_name); @@ -653,7 +657,7 @@ void ModeButton::SetState(const bool state) void ModeButton::update_bitmap() { - m_bmp = *get_bmp_bundle("mode", m_px_cnt, Slic3r::GUI::wxGetApp().get_mode_btn_color(m_mode_id)); + m_bmp = *get_bmp_bundle("mode", m_bmp_width, m_bmp_height, Slic3r::GUI::wxGetApp().get_mode_btn_color(m_mode_id)); SetBitmap(m_bmp); SetBitmapCurrent(m_bmp); @@ -780,69 +784,37 @@ void MenuWithSeparators::SetSecondSeparator() // QIDIBitmap // ---------------------------------------------------------------------------- ScalableBitmap::ScalableBitmap( wxWindow *parent, - const std::string& icon_name/* = ""*/, - const int px_cnt/* = 16*/, + const std::string& icon_name, + const int width/* = 16*/, + const int height/* = -1*/, const bool grayscale/* = false*/): m_parent(parent), m_icon_name(icon_name), - m_px_cnt(px_cnt) + m_bmp_width(width), m_bmp_height(height) { - m_bmp = *get_bmp_bundle(icon_name, px_cnt); + m_bmp = *get_bmp_bundle(icon_name, width, height); m_bitmap = m_bmp.GetBitmapFor(m_parent); } +ScalableBitmap::ScalableBitmap( wxWindow* parent, + const std::string& icon_name, + const wxSize icon_size, + const bool grayscale/* = false*/) : +ScalableBitmap(parent, icon_name, icon_size.x, icon_size.y, grayscale) +{ +} + void ScalableBitmap::sys_color_changed() { - m_bmp = *get_bmp_bundle(m_icon_name, m_px_cnt); + m_bmp = *get_bmp_bundle(m_icon_name, m_bmp_width, m_bmp_height); } -//B34 -// win is used to get a correct em_unit value -// It's important for bitmaps of dialogs. -// if win == nullptr, em_unit value of MainFrame will be used -wxBitmap create_scaled_bitmap(const std::string &bmp_name_in, - wxWindow * win /* = nullptr*/, - const int px_cnt /* = 16*/, - const bool grayscale /* = false*/, - const std::string &new_color /* = std::string()*/, // color witch will used instead of orange - const bool menu_bitmap /* = false*/, - const bool resize /* = false*/) -{ - static Slic3r::GUI::BitmapCache cache; - - unsigned int width = 0; - unsigned int height = (unsigned int) (win->FromDIP(px_cnt) + 0.5f); - - std::string bmp_name = bmp_name_in; - boost::replace_last(bmp_name, ".png", ""); - - bool dark_mode = -#ifdef _WIN32 - menu_bitmap ? Slic3r::GUI::check_dark_mode() : -#endif - Slic3r::GUI::wxGetApp().dark_mode(); - - // Try loading an SVG first, then PNG if SVG is not found: - wxBitmap *bmp = cache.load_svg(bmp_name, width, height, grayscale, dark_mode, new_color); - if (bmp == nullptr) { - bmp = cache.load_png(bmp_name, width, height, grayscale); - } - - if (bmp == nullptr) { - // Neither SVG nor PNG has been found, raise error - throw Slic3r::RuntimeError("Could not load bitmap: " + bmp_name); - } - - return *bmp; -} - - //B34 -void ScalableBitmap::msw_rescale() -{ - - m_bmp = create_scaled_bitmap(m_icon_name, m_parent, m_px_cnt, m_grayscale, std::string(), false, m_resize); -} +//void ScalableBitmap::msw_rescale() +//{ +// +// m_bmp = create_scaled_bitmap(m_icon_name, m_parent, m_px_cnt, m_grayscale, std::string(), false, m_resize); +//} // ---------------------------------------------------------------------------- @@ -856,17 +828,19 @@ ScalableButton::ScalableButton( wxWindow * parent, const wxSize& size /* = wxDefaultSize*/, const wxPoint& pos /* = wxDefaultPosition*/, long style /*= wxBU_EXACTFIT | wxNO_BORDER*/, - int bmp_px_cnt/* = 16*/) : + int width/* = 16*/, + int height/* = -1*/) : m_parent(parent), m_current_icon_name(icon_name), - m_px_cnt(bmp_px_cnt), + m_bmp_width(width), + m_bmp_height(height), m_has_border(!(style & wxNO_BORDER)) { Create(parent, id, label, pos, size, style); Slic3r::GUI::wxGetApp().UpdateDarkUI(this); if (!icon_name.empty()) { - SetBitmap(*get_bmp_bundle(icon_name, m_px_cnt)); + SetBitmap(*get_bmp_bundle(icon_name, width, height)); if (!label.empty()) SetBitmapMargins(int(0.5* em_unit(parent)), 0); } @@ -887,7 +861,8 @@ ScalableButton::ScalableButton( wxWindow * parent, long style /*= wxBU_EXACTFIT | wxNO_BORDER*/) : m_parent(parent), m_current_icon_name(bitmap.name()), - m_px_cnt(bitmap.px_cnt()), + m_bmp_width(bitmap.px_size().x), + m_bmp_height(bitmap.px_size().y), m_has_border(!(style& wxNO_BORDER)) { Create(parent, id, label, wxDefaultPosition, wxDefaultSize, style); @@ -908,7 +883,7 @@ bool ScalableButton::SetBitmap_(const std::string& bmp_name) if (m_current_icon_name.empty()) return false; - wxBitmapBundle bmp = *get_bmp_bundle(m_current_icon_name, m_px_cnt); + wxBitmapBundle bmp = *get_bmp_bundle(m_current_icon_name, m_bmp_width, m_bmp_height); SetBitmap(bmp); SetBitmapCurrent(bmp); SetBitmapPressed(bmp); @@ -936,13 +911,13 @@ void ScalableButton::sys_color_changed() { Slic3r::GUI::wxGetApp().UpdateDarkUI(this, m_has_border); - wxBitmapBundle bmp = *get_bmp_bundle(m_current_icon_name, m_px_cnt); + wxBitmapBundle bmp = *get_bmp_bundle(m_current_icon_name, m_bmp_width, m_bmp_height); SetBitmap(bmp); SetBitmapCurrent(bmp); SetBitmapPressed(bmp); SetBitmapFocus(bmp); if (!m_disabled_icon_name.empty()) - SetBitmapDisabled(*get_bmp_bundle(m_disabled_icon_name, m_px_cnt)); + SetBitmapDisabled(*get_bmp_bundle(m_disabled_icon_name, m_bmp_width, m_bmp_height)); if (!GetLabelText().IsEmpty()) SetBitmapMargins(int(0.5 * em_unit(m_parent)), 0); } diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index aad5aea..63d95d3 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -46,11 +46,11 @@ void enable_menu_item(wxUpdateUIEvent& evt, std::function const cb_condi class wxDialog; void edit_tooltip(wxString& tooltip); -void msw_buttons_rescale(wxDialog* dlg, const int em_unit, const std::vector& btn_ids); +void msw_buttons_rescale(wxDialog* dlg, const int em_unit, const std::vector& btn_ids, double height_koef = 1.); int em_unit(wxWindow* win); int mode_icon_px_size(); -wxBitmapBundle* get_bmp_bundle(const std::string& bmp_name, int px_cnt = 16, const std::string& new_color_rgb = std::string()); +wxBitmapBundle* get_bmp_bundle(const std::string& bmp_name, int width = 16, int height = -1, const std::string& new_color_rgb = std::string()); wxBitmapBundle* get_empty_bmp_bundle(int width, int height); wxBitmapBundle* get_solid_bmp_bundle(int width, int height, const std::string& color); @@ -127,6 +127,16 @@ public: void SetItemsCnt(int cnt) { m_cnt_open_items = cnt; } }; +inline wxSize get_preferred_size(const wxBitmapBundle& bmp, wxWindow* parent) +{ + if (!bmp.IsOk()) + return wxSize(0,0); +#ifdef __WIN32__ + return bmp.GetPreferredBitmapSizeFor(parent); +#else + return bmp.GetDefaultSize(); +#endif +} // ---------------------------------------------------------------------------- // ScalableBitmap @@ -137,8 +147,14 @@ class ScalableBitmap public: ScalableBitmap() {}; ScalableBitmap( wxWindow *parent, - const std::string& icon_name = "", - const int px_cnt = 16, + const std::string& icon_name, + const int width = 16, + const int height = -1 , + const bool grayscale = false); + + ScalableBitmap( wxWindow *parent, + const std::string& icon_name, + const wxSize icon_size, const bool grayscale = false); ~ScalableBitmap() {} @@ -149,18 +165,12 @@ public: wxBitmap get_bitmap() { return m_bmp.GetBitmapFor(m_parent); } wxWindow* parent() const { return m_parent;} const std::string& name() const { return m_icon_name; } - int px_cnt() const { return m_px_cnt;} + wxSize px_size() const { return wxSize(m_bmp_width, m_bmp_height);} - wxSize GetSize() const { -#ifdef __WIN32__ - return m_bmp.GetPreferredBitmapSizeFor(m_parent); -#else - return m_bmp.GetDefaultSize(); -#endif - } + void SetBitmap(const wxBitmapBundle& bmp) { m_bmp = bmp; } + wxSize GetSize() const { return get_preferred_size(m_bmp, m_parent); } //B34 - void msw_rescale(); - + //void msw_rescale(); int GetWidth() const { return GetSize().GetWidth(); } int GetHeight() const { return GetSize().GetHeight(); } @@ -169,7 +179,8 @@ private: wxBitmapBundle m_bmp = wxBitmapBundle(); wxBitmap m_bitmap = wxBitmap(); std::string m_icon_name = ""; - int m_px_cnt {16}; + int m_bmp_width{ 16 }; + int m_bmp_height{ -1 }; //B34 bool m_grayscale{false}; bool m_resize{false}; @@ -231,7 +242,8 @@ public: const wxSize& size = wxDefaultSize, const wxPoint& pos = wxDefaultPosition, long style = wxBU_EXACTFIT | wxNO_BORDER, - int bmp_px_cnt = 16); + int width = 16, + int height = -1); ScalableButton( wxWindow * parent, @@ -258,7 +270,8 @@ private: protected: // bitmap dimensions - int m_px_cnt{ 16 }; + int m_bmp_width{ 16 }; + int m_bmp_height{ -1 }; bool m_has_border {false}; }; diff --git a/src/slic3r/Utils/Duet.cpp b/src/slic3r/Utils/Duet.cpp index e77ebc8..9d0ce29 100644 --- a/src/slic3r/Utils/Duet.cpp +++ b/src/slic3r/Utils/Duet.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -23,10 +24,27 @@ #include "slic3r/GUI/format.hpp" #include "Http.hpp" +#include namespace fs = boost::filesystem; namespace pt = boost::property_tree; namespace Slic3r { +namespace { +std::string escape_string(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} +} Duet::Duet(DynamicPrintConfig *config) : host(config->opt_string("print_host")), @@ -75,6 +93,8 @@ bool Duet::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn e auto http = (dsf ? Http::put(std::move(upload_cmd)) : Http::post(std::move(upload_cmd))); if (dsf) { http.set_put_body(upload_data.source_path); + if (connect_msg.empty()) + http.header("X-Session-Key", boost::nowide::narrow(connect_msg)); } else { http.set_post_body(upload_data.source_path); } @@ -134,7 +154,20 @@ Duet::ConnectionType Duet::connect(wxString &msg) const msg = format_error(body, error, status); }) .on_complete([&](std::string body, unsigned) { + try { + pt::ptree root; + std::istringstream iss(body); + pt::read_json(iss, root); + auto key = root.get_optional("sessionKey"); + if (key) + msg = boost::nowide::widen(*key); res = ConnectionType::dsf; + } + catch (const std::exception&) { + BOOST_LOG_TRIVIAL(error) << "Failed to parse serverKey from Duet reply to Connect request: " << body; + msg = format_error(body, L("Failed to parse a Connect reply"), 0); + res = ConnectionType::error; + } }) .perform_sync(); }) @@ -199,12 +232,13 @@ std::string Duet::get_upload_url(const std::string &filename, ConnectionType con std::string Duet::get_connect_url(const bool dsfUrl) const { if (dsfUrl) { - return (boost::format("%1%machine/status") - % get_base_url()).str(); + return (boost::format("%1%machine/connect?password=%2%") + % get_base_url() + % (password.empty() ? "reprap" : escape_string(password))).str(); } else { return (boost::format("%1%rr_connect?password=%2%&%3%") % get_base_url() - % (password.empty() ? "reprap" : password) + % (password.empty() ? "reprap" : escape_string(password)) % timestamp_str()).str(); } } diff --git a/src/slic3r/Utils/Duet.hpp b/src/slic3r/Utils/Duet.hpp index 24c6c92..b09c8d4 100644 --- a/src/slic3r/Utils/Duet.hpp +++ b/src/slic3r/Utils/Duet.hpp @@ -33,7 +33,7 @@ public: std::string get_host() const override { return host; } private: - enum class ConnectionType { rrf, dsf, error }; + enum class ConnectionType { rrf, dsf, error }; // rrf = RepRapFirmware, dsf = DuetSoftwareFramework std::string host; std::string password; diff --git a/src/slic3r/Utils/EmbossStyleManager.cpp b/src/slic3r/Utils/EmbossStyleManager.cpp index e33484d..fef70da 100644 --- a/src/slic3r/Utils/EmbossStyleManager.cpp +++ b/src/slic3r/Utils/EmbossStyleManager.cpp @@ -1,26 +1,23 @@ #include "EmbossStyleManager.hpp" +#include #include // Imgui texture #include // ImTextCharFromUtf8 -#include "WxFontUtils.hpp" -#include "libslic3r/Utils.hpp" // ScopeGuard +#include +#include // ScopeGuard +#include "WxFontUtils.hpp" #include "slic3r/GUI/3DScene.hpp" // ::glsafe #include "slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" // check of font ranges -#include "slic3r/Utils/EmbossStylesSerializable.hpp" using namespace Slic3r; using namespace Slic3r::Emboss; using namespace Slic3r::GUI::Emboss; -StyleManager::StyleManager(const ImWchar *language_glyph_range, std::function create_default_styles) - : m_imgui_init_glyph_range(language_glyph_range) - , m_create_default_styles(create_default_styles) - , m_exist_style_images(false) - , m_temp_style_images(nullptr) - , m_app_config(nullptr) - , m_last_style_index(std::numeric_limits::max()) +StyleManager::StyleManager(const ImWchar *language_glyph_range, const std::function& create_default_styles) + : m_create_default_styles(create_default_styles) + , m_imgui_init_glyph_range(language_glyph_range) {} StyleManager::~StyleManager() { @@ -28,53 +25,65 @@ StyleManager::~StyleManager() { free_style_images(); } +/// +/// For store/load emboss style to/from AppConfig +/// +namespace { +void store_style_index(AppConfig &cfg, size_t index); +::std::optional load_style_index(const AppConfig &cfg); + +StyleManager::Styles load_styles(const AppConfig &cfg); +void store_styles(AppConfig &cfg, const StyleManager::Styles &styles); +void make_unique_name(const StyleManager::Styles &styles, std::string &name); +} // namespace void StyleManager::init(AppConfig *app_config) { + assert(app_config != nullptr); m_app_config = app_config; - EmbossStyles styles = (app_config != nullptr) ? - EmbossStylesSerializable::load_styles(*app_config) : - EmbossStyles{}; - if (styles.empty()) - styles = m_create_default_styles(); + m_styles = ::load_styles(*app_config); + + if (m_styles.empty()) { + // No styles loaded from ini file so use default + EmbossStyles styles = m_create_default_styles(); for (EmbossStyle &style : styles) { - make_unique_name(style.name); - m_style_items.push_back({style}); + ::make_unique_name(m_styles, style.name); + m_styles.push_back({style}); + } } std::optional active_index_opt = (app_config != nullptr) ? - EmbossStylesSerializable::load_style_index(*app_config) : + ::load_style_index(*app_config) : std::optional{}; size_t active_index = 0; if (active_index_opt.has_value()) active_index = *active_index_opt; - if (active_index >= m_style_items.size()) active_index = 0; + if (active_index >= m_styles.size()) active_index = 0; // find valid font item if (load_style(active_index)) return; // style is loaded // Try to fix that style can't be loaded - m_style_items.erase(m_style_items.begin() + active_index); + m_styles.erase(m_styles.begin() + active_index); load_valid_style(); } -bool StyleManager::store_styles_to_app_config(bool use_modification, - bool store_active_index) +bool StyleManager::store_styles_to_app_config(bool use_modification, bool store_active_index) { assert(m_app_config != nullptr); if (m_app_config == nullptr) return false; if (use_modification) { if (exist_stored_style()) { // update stored item - m_style_items[m_style_cache.style_index].style = m_style_cache.style; + m_styles[m_style_cache.style_index] = m_style_cache.style; } else { // add new into stored list EmbossStyle &style = m_style_cache.style; - make_unique_name(style.name); + ::make_unique_name(m_styles, style.name); m_style_cache.truncated_name.clear(); - m_style_cache.style_index = m_style_items.size(); - m_style_items.push_back({style}); + m_style_cache.style_index = m_styles.size(); + m_styles.push_back({style}); } m_style_cache.stored_wx_font = m_style_cache.wx_font; } @@ -84,30 +93,27 @@ bool StyleManager::store_styles_to_app_config(bool use_modification, size_t style_index = exist_stored_style() ? m_style_cache.style_index : m_last_style_index; - EmbossStylesSerializable::store_style_index(*m_app_config, style_index); + store_style_index(*m_app_config, style_index); } - EmbossStyles styles; - styles.reserve(m_style_items.size()); - for (const Item &item : m_style_items) styles.push_back(item.style); - EmbossStylesSerializable::store_styles(*m_app_config, styles); + store_styles(*m_app_config, m_styles); return true; } void StyleManager::add_style(const std::string &name) { EmbossStyle& style = m_style_cache.style; style.name = name; - make_unique_name(style.name); - m_style_cache.style_index = m_style_items.size(); + ::make_unique_name(m_styles, style.name); + m_style_cache.style_index = m_styles.size(); m_style_cache.stored_wx_font = m_style_cache.wx_font; m_style_cache.truncated_name.clear(); - m_style_items.push_back({style}); + m_styles.push_back({style}); } void StyleManager::swap(size_t i1, size_t i2) { - if (i1 >= m_style_items.size() || - i2 >= m_style_items.size()) return; - std::swap(m_style_items[i1], m_style_items[i2]); + if (i1 >= m_styles.size() || + i2 >= m_styles.size()) return; + std::swap(m_styles[i1], m_styles[i2]); // fix selected index if (!exist_stored_style()) return; if (m_style_cache.style_index == i1) { @@ -131,7 +137,7 @@ void StyleManager::discard_style_changes() { } void StyleManager::erase(size_t index) { - if (index >= m_style_items.size()) return; + if (index >= m_styles.size()) return; // fix selected index if (exist_stored_style()) { @@ -140,15 +146,15 @@ void StyleManager::erase(size_t index) { else if (index == i) i = std::numeric_limits::max(); } - m_style_items.erase(m_style_items.begin() + index); + m_styles.erase(m_styles.begin() + index); } void StyleManager::rename(const std::string& name) { m_style_cache.style.name = name; m_style_cache.truncated_name.clear(); if (exist_stored_style()) { - Item &it = m_style_items[m_style_cache.style_index]; - it.style.name = name; + Style &it = m_styles[m_style_cache.style_index]; + it.name = name; it.truncated_name.clear(); } } @@ -156,28 +162,28 @@ void StyleManager::rename(const std::string& name) { void StyleManager::load_valid_style() { // iterate over all known styles - while (!m_style_items.empty()) { + while (!m_styles.empty()) { if (load_style(0)) return; // can't load so erase it from list - m_style_items.erase(m_style_items.begin()); + m_styles.erase(m_styles.begin()); } // no one style is loadable // set up default font list EmbossStyles def_style = m_create_default_styles(); for (EmbossStyle &style : def_style) { - make_unique_name(style.name); - m_style_items.push_back({std::move(style)}); + ::make_unique_name(m_styles, style.name); + m_styles.push_back({std::move(style)}); } // iterate over default styles // There have to be option to use build in font - while (!m_style_items.empty()) { + while (!m_styles.empty()) { if (load_style(0)) return; // can't load so erase it from list - m_style_items.erase(m_style_items.begin()); + m_styles.erase(m_styles.begin()); } // This OS doesn't have TTF as default font, @@ -187,15 +193,15 @@ void StyleManager::load_valid_style() bool StyleManager::load_style(size_t style_index) { - if (style_index >= m_style_items.size()) return false; - if (!load_style(m_style_items[style_index].style)) return false; + if (style_index >= m_styles.size()) return false; + if (!load_style(m_styles[style_index])) return false; m_style_cache.style_index = style_index; m_style_cache.stored_wx_font = m_style_cache.wx_font; // copy m_last_style_index = style_index; return true; } -bool StyleManager::load_style(const EmbossStyle &style) { +bool StyleManager::load_style(const Style &style) { if (style.type == EmbossStyle::Type::file_path) { std::unique_ptr font_ptr = create_font_file(style.path.c_str()); @@ -208,13 +214,13 @@ bool StyleManager::load_style(const EmbossStyle &style) { m_style_cache.stored_wx_font = {}; return true; } - if (style.type != WxFontUtils::get_actual_type()) return false; + if (style.type != WxFontUtils::get_current_type()) return false; std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); if (!wx_font_opt.has_value()) return false; return load_style(style, *wx_font_opt); } -bool StyleManager::load_style(const EmbossStyle &style, const wxFont &font) +bool StyleManager::load_style(const Style &style, const wxFont &font) { m_style_cache.style = style; // copy @@ -265,12 +271,18 @@ bool StyleManager::is_font_changed() const return is_bold != is_stored_bold; } +bool StyleManager::is_unique_style_name(const std::string &name) const { + for (const StyleManager::Style &style : m_styles) + if (style.name == name) + return false; + return true; +} bool StyleManager::is_active_font() { return m_style_cache.font_file.has_value(); } -const EmbossStyle* StyleManager::get_stored_style() const +const StyleManager::Style *StyleManager::get_stored_style() const { - if (m_style_cache.style_index >= m_style_items.size()) return nullptr; - return &m_style_items[m_style_cache.style_index].style; + if (m_style_cache.style_index >= m_styles.size()) return nullptr; + return &m_styles[m_style_cache.style_index]; } void StyleManager::clear_glyphs_cache() @@ -282,24 +294,6 @@ void StyleManager::clear_glyphs_cache() void StyleManager::clear_imgui_font() { m_style_cache.atlas.Clear(); } -#include "slic3r/GUI/TextLines.hpp" -double StyleManager::get_line_height() -{ - assert(is_active_font()); - if (!is_active_font()) - return -1; - const auto &ffc = get_font_file_with_cache(); - assert(ffc.has_value()); - if (!ffc.has_value()) - return -1; - const auto &ff_ptr = ffc.font_file; - assert(ff_ptr != nullptr); - if (ff_ptr == nullptr) - return -1; - const FontProp &fp = get_font_prop(); - const FontFile &ff = *ff_ptr; - return TextLinesModel::calc_line_height(ff, fp); -} ImFont *StyleManager::get_imgui_font() { @@ -317,44 +311,12 @@ ImFont *StyleManager::get_imgui_font() return font; } -const std::vector &StyleManager::get_styles() const{ return m_style_items; } - -void StyleManager::make_unique_name(std::string &name) -{ - auto is_unique = [&](const std::string &name) -> bool { - for (const Item &it : m_style_items) - if (it.style.name == name) return false; - return true; - }; - - // Style name can't be empty so default name is set - if (name.empty()) name = "Text style"; - - // When name is already unique, nothing need to be changed - if (is_unique(name)) return; - - // when there is previous version of style name only find number - const char *prefix = " ("; - const char suffix = ')'; - auto pos = name.find_last_of(prefix); - if (name.c_str()[name.size() - 1] == suffix && - pos != std::string::npos) { - // short name by ord number - name = name.substr(0, pos); - } - - int order = 1; // start with value 2 to represents same font name - std::string new_name; - do { - new_name = name + prefix + std::to_string(++order) + suffix; - } while (!is_unique(new_name)); - name = new_name; -} +const StyleManager::Styles &StyleManager::get_styles() const{ return m_styles; } void StyleManager::init_trunc_names(float max_width) { - for (auto &s : m_style_items) + for (auto &s : m_styles) if (s.truncated_name.empty()) { - std::string name = s.style.name; + std::string name = s.name; ImGuiWrapper::escape_double_hash(name); s.truncated_name = ImGuiWrapper::trunc(name, max_width); } @@ -387,9 +349,9 @@ void StyleManager::init_style_images(const Vec2i &max_size, StyleImagesData::Item &style = m_temp_style_images->styles[index]; // find style in font list and copy to it - for (auto &it : m_style_items) { - if (it.style.name != style.text || - !(it.style.prop == style.prop)) + for (auto &it : m_styles) { + if (it.name != style.text || + !(it.prop == style.prop)) continue; it.image = image; break; @@ -406,9 +368,8 @@ void StyleManager::init_style_images(const Vec2i &max_size, // create job for init images m_temp_style_images = std::make_shared(); StyleImagesData::Items styles; - styles.reserve(m_style_items.size()); - for (const Item &item : m_style_items) { - const EmbossStyle &style = item.style; + styles.reserve(m_styles.size()); + for (const Style &style : m_styles) { std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); if (!wx_font_opt.has_value()) continue; std::unique_ptr font_file = @@ -435,7 +396,7 @@ void StyleManager::init_style_images(const Vec2i &max_size, void StyleManager::free_style_images() { if (!m_exist_style_images) return; GLuint tex_id = 0; - for (Item &it : m_style_items) { + for (Style &it : m_styles) { if (tex_id == 0 && it.image.has_value()) tex_id = (GLuint)(intptr_t) it.image->texture_id; it.image.reset(); @@ -547,10 +508,278 @@ bool StyleManager::set_wx_font(const wxFont &wx_font, std::unique_ptr FontFileWithCache(std::move(font_file)); EmbossStyle &style = m_style_cache.style; - style.type = WxFontUtils::get_actual_type(); + style.type = WxFontUtils::get_current_type(); // update string path style.path = WxFontUtils::store_wxFont(wx_font); WxFontUtils::update_property(style.prop, wx_font); clear_imgui_font(); return true; } + +#include +#include "WxFontUtils.hpp" +#include "fast_float/fast_float.h" + +// StylesSerializable +namespace { + +using namespace Slic3r; +using namespace Slic3r::GUI; +using Section = std::map; + +const std::string APP_CONFIG_FONT_NAME = "name"; +const std::string APP_CONFIG_FONT_DESCRIPTOR = "descriptor"; +const std::string APP_CONFIG_FONT_LINE_HEIGHT = "line_height"; +const std::string APP_CONFIG_FONT_DEPTH = "depth"; +const std::string APP_CONFIG_FONT_USE_SURFACE = "use_surface"; +const std::string APP_CONFIG_FONT_BOLDNESS = "boldness"; +const std::string APP_CONFIG_FONT_SKEW = "skew"; +const std::string APP_CONFIG_FONT_DISTANCE = "distance"; +const std::string APP_CONFIG_FONT_ANGLE = "angle"; +const std::string APP_CONFIG_FONT_COLLECTION = "collection"; +const std::string APP_CONFIG_FONT_CHAR_GAP = "char_gap"; +const std::string APP_CONFIG_FONT_LINE_GAP = "line_gap"; + +const std::string APP_CONFIG_ACTIVE_FONT = "active_font"; + +std::string create_section_name(unsigned index) +{ + return AppConfig::SECTION_EMBOSS_STYLE + ':' + std::to_string(index); +} + +// check only existence of flag +bool read(const Section §ion, const std::string &key, bool &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + + value = true; + return true; +} + +bool read(const Section §ion, const std::string &key, float &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) + return false; + + value = value_; + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + int value_ = std::atoi(data.c_str()); + if (value_ == 0) + return false; + + value = value_; + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + int value_ = std::atoi(data.c_str()); + if (value_ <= 0) + return false; + + value = static_cast(value_); + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) + return false; + + value = value_; + return true; +} + +std::optional load_style(const Section &app_cfg_section) +{ + auto path_it = app_cfg_section.find(APP_CONFIG_FONT_DESCRIPTOR); + if (path_it == app_cfg_section.end()) + return {}; + + StyleManager::Style s; + EmbossProjection& ep = s.projection; + FontProp& fp = s.prop; + + s.path = path_it->second; + s.type = WxFontUtils::get_current_type(); + auto name_it = app_cfg_section.find(APP_CONFIG_FONT_NAME); + const std::string default_name = "font_name"; + s.name = (name_it == app_cfg_section.end()) ? default_name : name_it->second; + + read(app_cfg_section, APP_CONFIG_FONT_LINE_HEIGHT, fp.size_in_mm); + float depth = 1.; + read(app_cfg_section, APP_CONFIG_FONT_DEPTH, depth); + ep.depth = depth; + read(app_cfg_section, APP_CONFIG_FONT_USE_SURFACE, ep.use_surface); + read(app_cfg_section, APP_CONFIG_FONT_BOLDNESS, fp.boldness); + read(app_cfg_section, APP_CONFIG_FONT_SKEW, fp.skew); + read(app_cfg_section, APP_CONFIG_FONT_DISTANCE, s.distance); + read(app_cfg_section, APP_CONFIG_FONT_ANGLE, s.angle); + read(app_cfg_section, APP_CONFIG_FONT_COLLECTION, fp.collection_number); + read(app_cfg_section, APP_CONFIG_FONT_CHAR_GAP, fp.char_gap); + read(app_cfg_section, APP_CONFIG_FONT_LINE_GAP, fp.line_gap); + return s; +} + +void store_style(AppConfig &cfg, const StyleManager::Style &s, unsigned index) +{ + const EmbossProjection &ep = s.projection; + Section data; + data[APP_CONFIG_FONT_NAME] = s.name; + data[APP_CONFIG_FONT_DESCRIPTOR] = s.path; + const FontProp &fp = s.prop; + data[APP_CONFIG_FONT_LINE_HEIGHT] = std::to_string(fp.size_in_mm); + data[APP_CONFIG_FONT_DEPTH] = std::to_string(ep.depth); + if (ep.use_surface) + data[APP_CONFIG_FONT_USE_SURFACE] = "true"; + if (fp.boldness.has_value()) + data[APP_CONFIG_FONT_BOLDNESS] = std::to_string(*fp.boldness); + if (fp.skew.has_value()) + data[APP_CONFIG_FONT_SKEW] = std::to_string(*fp.skew); + if (s.distance.has_value()) + data[APP_CONFIG_FONT_DISTANCE] = std::to_string(*s.distance); + if (s.angle.has_value()) + data[APP_CONFIG_FONT_ANGLE] = std::to_string(*s.angle); + if (fp.collection_number.has_value()) + data[APP_CONFIG_FONT_COLLECTION] = std::to_string(*fp.collection_number); + if (fp.char_gap.has_value()) + data[APP_CONFIG_FONT_CHAR_GAP] = std::to_string(*fp.char_gap); + if (fp.line_gap.has_value()) + data[APP_CONFIG_FONT_LINE_GAP] = std::to_string(*fp.line_gap); + cfg.set_section(create_section_name(index), std::move(data)); +} + +void store_style_index(AppConfig &cfg, size_t index) +{ + // store actual font index + // active font first index is +1 to correspond with section name + Section data; + data[APP_CONFIG_ACTIVE_FONT] = std::to_string(index); + cfg.set_section(AppConfig::SECTION_EMBOSS_STYLE, std::move(data)); +} + +std::optional load_style_index(const AppConfig &cfg) +{ + if (!cfg.has_section(AppConfig::SECTION_EMBOSS_STYLE)) + return {}; + + auto section = cfg.get_section(AppConfig::SECTION_EMBOSS_STYLE); + auto it = section.find(APP_CONFIG_ACTIVE_FONT); + if (it == section.end()) + return {}; + + size_t active_font = static_cast(std::atoi(it->second.c_str())); + // order in config starts with number 1 + return active_font - 1; +} + +::StyleManager::Styles load_styles(const AppConfig &cfg) +{ + StyleManager::Styles result; + // human readable index inside of config starts from 1 !! + unsigned index = 1; + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + std::optional style_opt = load_style(cfg.get_section(section_name)); + if (style_opt.has_value()) { + make_unique_name(result, style_opt->name); + result.emplace_back(*style_opt); + } + + section_name = create_section_name(++index); + } + return result; +} + +void store_styles(AppConfig &cfg, const StyleManager::Styles &styles) +{ + EmbossStyle::Type current_type = WxFontUtils::get_current_type(); + // store styles + unsigned index = 1; + for (const StyleManager::Style &style : styles) { + // skip file paths + fonts from other OS(loaded from .3mf) + assert(style.type == current_type); + if (style.type != current_type) + continue; + store_style(cfg, style, index); + ++index; + } + + // remove rest of font sections (after deletation) + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + cfg.clear_section(section_name); + section_name = create_section_name(index); + ++index; + } +} + +void make_unique_name(const StyleManager::Styles& styles, std::string &name) +{ + auto is_unique = [&styles](const std::string &name){ + for (const StyleManager::Style &it : styles) + if (it.name == name) return false; + return true; + }; + + // Style name can't be empty so default name is set + if (name.empty()) name = "Text style"; + + // When name is already unique, nothing need to be changed + if (is_unique(name)) return; + + // when there is previous version of style name only find number + const char *prefix = " ("; + const char suffix = ')'; + auto pos = name.find_last_of(prefix); + if (name.c_str()[name.size() - 1] == suffix && + pos != std::string::npos) { + // short name by ord number + name = name.substr(0, pos); + } + + int order = 1; // start with value 2 to represents same font name + std::string new_name; + do { + new_name = name + prefix + std::to_string(++order) + suffix; + } while (!is_unique(new_name)); + name = new_name; +} + +} // namespace \ No newline at end of file diff --git a/src/slic3r/Utils/EmbossStyleManager.hpp b/src/slic3r/Utils/EmbossStyleManager.hpp index 397954f..9a79263 100644 --- a/src/slic3r/Utils/EmbossStyleManager.hpp +++ b/src/slic3r/Utils/EmbossStyleManager.hpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include namespace Slic3r::GUI::Emboss { @@ -26,7 +28,7 @@ class StyleManager public: /// Character to load for imgui when initialize imgui font /// Function to create default styles - StyleManager(const ImWchar *language_glyph_range, std::function create_default_styles); + StyleManager(const ImWchar *language_glyph_range, const std::function& create_default_styles); /// /// Release imgui font and style images from GPU @@ -57,11 +59,11 @@ public: void add_style(const std::string& name); /// - /// Change order of style item in m_style_items. + /// Change order of style item in m_styles. /// Fix selected font index when (i1 || i2) == m_font_selected /// - /// First index to m_style_items - /// Second index to m_style_items + /// First index to m_styles + /// Second index to m_styles void swap(size_t i1, size_t i2); /// @@ -71,7 +73,7 @@ public: void discard_style_changes(); /// - /// Remove style from m_style_items. + /// Remove style from m_styles. /// Fix selected font index when index is under m_font_selected /// /// Index of style to be removed @@ -92,13 +94,14 @@ public: /// Change active font /// When font not loaded roll back activ font /// - /// New font index(from m_style_items range) + /// New font index(from m_styles range) /// True on succes. False on fail load font bool load_style(size_t font_index); // load font style not stored in list - bool load_style(const EmbossStyle &style); + struct Style; + bool load_style(const Style &style); // fastering load font on index by wxFont, ignore type and descriptor - bool load_style(const EmbossStyle &style, const wxFont &font); + bool load_style(const Style &style, const wxFont &font); // clear actual selected glyphs cache void clear_glyphs_cache(); @@ -106,15 +109,12 @@ public: // remove cached imgui font for actual selected font void clear_imgui_font(); - // calculate line height - // not const because access to font file which could be created. - double get_line_height(); /* const */ // getters for private data - const EmbossStyle *get_stored_style() const; + const Style *get_stored_style() const; - const EmbossStyle &get_style() const { return m_style_cache.style; } - EmbossStyle &get_style() { return m_style_cache.style; } + const Style &get_style() const { return m_style_cache.style; } + Style &get_style() { return m_style_cache.style; } size_t get_style_index() const { return m_style_cache.style_index; } std::string &get_truncated_name() { return m_style_cache.truncated_name; } const ImFontAtlas &get_atlas() const { return m_style_cache.atlas; } @@ -135,6 +135,7 @@ public: /// bool is_font_changed() const; + bool is_unique_style_name(const std::string &name) const; /// /// Setter on wx_font when changed /// @@ -169,30 +170,49 @@ public: void init_style_images(const Vec2i& max_size, const std::string &text); void free_style_images(); - struct Item; // access to all managed font styles - const std::vector &get_styles() const; + const std::vector