diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c7fc12df..93339eb0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,7 @@ set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux") # Proposal for C++ unit tests and sandboxes option(SLIC3R_BUILD_SANDBOXES "Build development sandboxes" OFF) -option(SLIC3R_BUILD_TESTS "Build unit tests" OFF) +option(SLIC3R_BUILD_TESTS "Build unit tests" ON) # Print out the SLIC3R_* cache options get_cmake_property(_cache_vars CACHE_VARIABLES) diff --git a/cmake/modules/Catch2/Catch.cmake b/cmake/modules/Catch2/Catch.cmake new file mode 100644 index 000000000..0ffe978dc --- /dev/null +++ b/cmake/modules/Catch2/Catch.cmake @@ -0,0 +1,175 @@ +# 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 new file mode 100644 index 000000000..ca5ebc17e --- /dev/null +++ b/cmake/modules/Catch2/CatchAddTests.cmake @@ -0,0 +1,106 @@ +# 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 new file mode 100644 index 000000000..925d93281 --- /dev/null +++ b/cmake/modules/Catch2/ParseAndAddCatchTests.cmake @@ -0,0 +1,225 @@ +#==================================================================================================# +# 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/sandboxes/slabasebed/slabasebed.cpp b/sandboxes/slabasebed/slabasebed.cpp index b8b94d86f..1996a1eb8 100644 --- a/sandboxes/slabasebed/slabasebed.cpp +++ b/sandboxes/slabasebed/slabasebed.cpp @@ -16,9 +16,9 @@ const std::string USAGE_STR = { namespace Slic3r { namespace sla { -Contour3D create_base_pool(const Polygons &ground_layer, +Contour3D create_pad(const Polygons &ground_layer, const ExPolygons &holes = {}, - const PoolConfig& cfg = PoolConfig()); + const PadConfig& cfg = PadConfig()); Contour3D walls(const Polygon& floor_plate, const Polygon& ceiling, double floor_z_mm, double ceiling_z_mm, @@ -45,7 +45,7 @@ int main(const int argc, const char *argv[]) { model.align_to_origin(); ExPolygons ground_slice; - sla::base_plate(model, ground_slice, 0.1f); + sla::pad_plate(model, ground_slice, 0.1f); if(ground_slice.empty()) return EXIT_FAILURE; ground_slice = offset_ex(ground_slice, 0.5); @@ -56,10 +56,10 @@ int main(const int argc, const char *argv[]) { bench.start(); - sla::PoolConfig cfg; + sla::PadConfig cfg; cfg.min_wall_height_mm = 0; cfg.edge_radius_mm = 0; - mesh = sla::create_base_pool(to_polygons(ground_slice), {}, cfg); + mesh = sla::create_pad(to_polygons(ground_slice), {}, cfg); bench.stop(); diff --git a/src/agg/agg_rasterizer_scanline_aa.h b/src/agg/agg_rasterizer_scanline_aa.h index ffc2ddf94..4925ca209 100644 --- a/src/agg/agg_rasterizer_scanline_aa.h +++ b/src/agg/agg_rasterizer_scanline_aa.h @@ -156,7 +156,7 @@ namespace agg //------------------------------------------------------------------- template - void add_path(VertexSource& vs, unsigned path_id=0) + void add_path(VertexSource &&vs, unsigned path_id=0) { double x; double y; diff --git a/src/libnest2d/CMakeLists.txt b/src/libnest2d/CMakeLists.txt index 587b814b2..0d091c171 100644 --- a/src/libnest2d/CMakeLists.txt +++ b/src/libnest2d/CMakeLists.txt @@ -13,11 +13,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED) # Add our own cmake module path. list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules/) -option(LIBNEST2D_UNITTESTS "If enabled, googletest framework will be downloaded - and the provided unit tests will be included in the build." OFF) - -option(LIBNEST2D_BUILD_EXAMPLES "If enabled, examples will be built." OFF) - option(LIBNEST2D_HEADER_ONLY "If enabled static library will not be built." ON) set(GEOMETRY_BACKENDS clipper boost eigen) @@ -109,26 +104,3 @@ if(NOT LIBNEST2D_HEADER_ONLY) target_link_libraries(${LIBNAME} PUBLIC libnest2d) target_compile_definitions(${LIBNAME} PUBLIC LIBNEST2D_STATIC) endif() - -if(LIBNEST2D_BUILD_EXAMPLES) - - add_executable(example examples/main.cpp - # tools/libnfpglue.hpp - # tools/libnfpglue.cpp - tools/nfp_svgnest.hpp - tools/nfp_svgnest_glue.hpp - tools/svgtools.hpp - tests/printer_parts.cpp - tests/printer_parts.h - ) - - if(NOT LIBNEST2D_HEADER_ONLY) - target_link_libraries(example ${LIBNAME}) - else() - target_link_libraries(example libnest2d) - endif() -endif() - -if(LIBNEST2D_UNITTESTS) - add_subdirectory(${PROJECT_SOURCE_DIR}/tests) -endif() diff --git a/src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/src/libnest2d/include/libnest2d/placers/nfpplacer.hpp index 686857a87..003028758 100644 --- a/src/libnest2d/include/libnest2d/placers/nfpplacer.hpp +++ b/src/libnest2d/include/libnest2d/placers/nfpplacer.hpp @@ -1122,8 +1122,6 @@ private: sl::rotate(sh, item.rotation()); Box bb = sl::boundingBox(sh); - bb.minCorner() += item.translation(); - bb.maxCorner() += item.translation(); Vertex ci, cb; auto bbin = sl::boundingBox(bin_); diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index b1ebdcfbc..4fbe72163 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -15,7 +15,7 @@ public: PointClass max; bool defined; - BoundingBoxBase() : defined(false), min(PointClass::Zero()), max(PointClass::Zero()) {} + BoundingBoxBase() : min(PointClass::Zero()), max(PointClass::Zero()), defined(false) {} BoundingBoxBase(const PointClass &pmin, const PointClass &pmax) : min(pmin), max(pmax), defined(pmin(0) < pmax(0) && pmin(1) < pmax(1)) {} BoundingBoxBase(const std::vector& points) : min(PointClass::Zero()), max(PointClass::Zero()) @@ -59,7 +59,7 @@ template class BoundingBox3Base : public BoundingBoxBase { public: - BoundingBox3Base() : BoundingBoxBase() {}; + BoundingBox3Base() : BoundingBoxBase() {} BoundingBox3Base(const PointClass &pmin, const PointClass &pmax) : BoundingBoxBase(pmin, pmax) { if (pmin(2) >= pmax(2)) BoundingBoxBase::defined = false; } @@ -100,6 +100,33 @@ public: } }; +// Will prevent warnings caused by non existing definition of template in hpp +extern template void BoundingBoxBase::scale(double factor); +extern template void BoundingBoxBase::scale(double factor); +extern template void BoundingBoxBase::scale(double factor); +extern template void BoundingBoxBase::offset(coordf_t delta); +extern template void BoundingBoxBase::offset(coordf_t delta); +extern template void BoundingBoxBase::merge(const Point &point); +extern template void BoundingBoxBase::merge(const Vec2d &point); +extern template void BoundingBoxBase::merge(const Points &points); +extern template void BoundingBoxBase::merge(const Pointfs &points); +extern template void BoundingBoxBase::merge(const BoundingBoxBase &bb); +extern template void BoundingBoxBase::merge(const BoundingBoxBase &bb); +extern template Point BoundingBoxBase::size() const; +extern template Vec2d BoundingBoxBase::size() const; +extern template double BoundingBoxBase::radius() const; +extern template double BoundingBoxBase::radius() const; +extern template Point BoundingBoxBase::center() const; +extern template Vec2d BoundingBoxBase::center() const; +extern template void BoundingBox3Base::merge(const Vec3d &point); +extern template void BoundingBox3Base::merge(const Pointf3s &points); +extern template void BoundingBox3Base::merge(const BoundingBox3Base &bb); +extern template Vec3d BoundingBox3Base::size() const; +extern template double BoundingBox3Base::radius() const; +extern template void BoundingBox3Base::offset(coordf_t delta); +extern template Vec3d BoundingBox3Base::center() const; +extern template coordf_t BoundingBox3Base::max_size() const; + class BoundingBox : public BoundingBoxBase { public: @@ -113,9 +140,9 @@ public: // to encompass the original bounding box. void align_to_grid(const coord_t cell_size); - BoundingBox() : BoundingBoxBase() {}; - BoundingBox(const Point &pmin, const Point &pmax) : BoundingBoxBase(pmin, pmax) {}; - BoundingBox(const Points &points) : BoundingBoxBase(points) {}; + BoundingBox() : BoundingBoxBase() {} + BoundingBox(const Point &pmin, const Point &pmax) : BoundingBoxBase(pmin, pmax) {} + BoundingBox(const Points &points) : BoundingBoxBase(points) {} BoundingBox(const Lines &lines); friend BoundingBox get_extents_rotated(const Points &points, double angle); @@ -124,25 +151,25 @@ public: class BoundingBox3 : public BoundingBox3Base { public: - BoundingBox3() : BoundingBox3Base() {}; - BoundingBox3(const Vec3crd &pmin, const Vec3crd &pmax) : BoundingBox3Base(pmin, pmax) {}; - BoundingBox3(const Points3& points) : BoundingBox3Base(points) {}; + BoundingBox3() : BoundingBox3Base() {} + BoundingBox3(const Vec3crd &pmin, const Vec3crd &pmax) : BoundingBox3Base(pmin, pmax) {} + BoundingBox3(const Points3& points) : BoundingBox3Base(points) {} }; class BoundingBoxf : public BoundingBoxBase { public: - BoundingBoxf() : BoundingBoxBase() {}; - BoundingBoxf(const Vec2d &pmin, const Vec2d &pmax) : BoundingBoxBase(pmin, pmax) {}; - BoundingBoxf(const std::vector &points) : BoundingBoxBase(points) {}; + BoundingBoxf() : BoundingBoxBase() {} + BoundingBoxf(const Vec2d &pmin, const Vec2d &pmax) : BoundingBoxBase(pmin, pmax) {} + BoundingBoxf(const std::vector &points) : BoundingBoxBase(points) {} }; class BoundingBoxf3 : public BoundingBox3Base { public: - BoundingBoxf3() : BoundingBox3Base() {}; - BoundingBoxf3(const Vec3d &pmin, const Vec3d &pmax) : BoundingBox3Base(pmin, pmax) {}; - BoundingBoxf3(const std::vector &points) : BoundingBox3Base(points) {}; + BoundingBoxf3() : BoundingBox3Base() {} + BoundingBoxf3(const Vec3d &pmin, const Vec3d &pmax) : BoundingBox3Base(pmin, pmax) {} + BoundingBoxf3(const std::vector &points) : BoundingBox3Base(points) {} BoundingBoxf3 transformed(const Transform3d& matrix) const; }; diff --git a/src/libslic3r/BridgeDetector.cpp b/src/libslic3r/BridgeDetector.cpp index ccc3505ce..ce7c960fa 100644 --- a/src/libslic3r/BridgeDetector.cpp +++ b/src/libslic3r/BridgeDetector.cpp @@ -6,9 +6,9 @@ namespace Slic3r { BridgeDetector::BridgeDetector( - ExPolygon _expolygon, - const ExPolygonCollection &_lower_slices, - coord_t _spacing) : + ExPolygon _expolygon, + const ExPolygons &_lower_slices, + coord_t _spacing) : // The original infill polygon, not inflated. expolygons(expolygons_owned), // All surfaces of the object supporting this region. @@ -20,9 +20,9 @@ BridgeDetector::BridgeDetector( } BridgeDetector::BridgeDetector( - const ExPolygons &_expolygons, - const ExPolygonCollection &_lower_slices, - coord_t _spacing) : + const ExPolygons &_expolygons, + const ExPolygons &_lower_slices, + coord_t _spacing) : // The original infill polygon, not inflated. expolygons(_expolygons), // All surfaces of the object supporting this region. @@ -46,7 +46,11 @@ void BridgeDetector::initialize() // Detect what edges lie on lower slices by turning bridge contour and holes // into polylines and then clipping them with each lower slice's contour. // Currently _edges are only used to set a candidate direction of the bridge (see bridge_direction_candidates()). - this->_edges = intersection_pl(to_polylines(grown), this->lower_slices.contours()); + Polygons contours; + contours.reserve(this->lower_slices.size()); + for (const ExPolygon &expoly : this->lower_slices) + contours.push_back(expoly.contour); + this->_edges = intersection_pl(to_polylines(grown), contours); #ifdef SLIC3R_DEBUG printf(" bridge has " PRINTF_ZU " support(s)\n", this->_edges.size()); @@ -54,7 +58,7 @@ void BridgeDetector::initialize() // detect anchors as intersection between our bridge expolygon and the lower slices // safety offset required to avoid Clipper from detecting empty intersection while Boost actually found some edges - this->_anchor_regions = intersection_ex(grown, to_polygons(this->lower_slices.expolygons), true); + this->_anchor_regions = intersection_ex(grown, to_polygons(this->lower_slices), true); /* if (0) { @@ -271,7 +275,7 @@ BridgeDetector::unsupported_edges(double angle, Polylines* unsupported) const if (angle == -1) angle = this->angle; if (angle == -1) return; - Polygons grown_lower = offset(this->lower_slices.expolygons, float(this->spacing)); + Polygons grown_lower = offset(this->lower_slices, float(this->spacing)); for (ExPolygons::const_iterator it_expoly = this->expolygons.begin(); it_expoly != this->expolygons.end(); ++ it_expoly) { // get unsupported bridge edges (both contour and holes) diff --git a/src/libslic3r/BridgeDetector.hpp b/src/libslic3r/BridgeDetector.hpp index 5c55276be..f876f83c7 100644 --- a/src/libslic3r/BridgeDetector.hpp +++ b/src/libslic3r/BridgeDetector.hpp @@ -3,7 +3,6 @@ #include "libslic3r.h" #include "ExPolygon.hpp" -#include "ExPolygonCollection.hpp" #include namespace Slic3r { @@ -21,7 +20,7 @@ public: // In case the caller gaves us the input polygons by a value, make a copy. ExPolygons expolygons_owned; // Lower slices, all regions. - const ExPolygonCollection &lower_slices; + const ExPolygons &lower_slices; // Scaled extrusion width of the infill. coord_t spacing; // Angle resolution for the brute force search of the best bridging angle. @@ -29,8 +28,8 @@ public: // The final optimal angle. double angle; - BridgeDetector(ExPolygon _expolygon, const ExPolygonCollection &_lower_slices, coord_t _extrusion_width); - BridgeDetector(const ExPolygons &_expolygons, const ExPolygonCollection &_lower_slices, coord_t _extrusion_width); + BridgeDetector(ExPolygon _expolygon, const ExPolygons &_lower_slices, coord_t _extrusion_width); + BridgeDetector(const ExPolygons &_expolygons, const ExPolygons &_lower_slices, coord_t _extrusion_width); // If bridge_direction_override != 0, then the angle is used instead of auto-detect. bool detect_angle(double bridge_direction_override = 0.); Polygons coverage(double angle = -1) const; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 5232be2b6..fe8b0f71d 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -176,8 +176,13 @@ add_library(libslic3r STATIC miniz_extension.cpp SLA/SLACommon.hpp SLA/SLABoilerPlate.hpp - SLA/SLABasePool.hpp - SLA/SLABasePool.cpp + SLA/SLAPad.hpp + SLA/SLAPad.cpp + SLA/SLASupportTreeBuilder.hpp + SLA/SLASupportTreeBuildsteps.hpp + SLA/SLASupportTreeBuildsteps.cpp + SLA/SLASupportTreeBuilder.cpp + SLA/SLAConcurrency.hpp SLA/SLASupportTree.hpp SLA/SLASupportTree.cpp SLA/SLASupportTreeIGL.cpp @@ -215,6 +220,7 @@ target_link_libraries(libslic3r qhull semver tbb + ${CMAKE_DL_LIBS} ) if(WIN32) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index c3183bd8e..b863b4712 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -194,6 +194,19 @@ ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polygons &input) return retval; } +ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const ExPolygons &input) +{ + ClipperLib::Paths retval; + for (auto &ep : input) { + retval.emplace_back(Slic3rMultiPoint_to_ClipperPath(ep.contour)); + + for (auto &h : ep.holes) + retval.emplace_back(Slic3rMultiPoint_to_ClipperPath(h)); + } + + return retval; +} + ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polylines &input) { ClipperLib::Paths retval; @@ -472,14 +485,16 @@ ExPolygons offset2_ex(const ExPolygons &expolygons, const float delta1, return union_ex(polys); } -template -T -_clipper_do(const ClipperLib::ClipType clipType, const Polygons &subject, - const Polygons &clip, const ClipperLib::PolyFillType fillType, const bool safety_offset_) +template +T _clipper_do(const ClipperLib::ClipType clipType, + TSubj && subject, + TClip && clip, + const ClipperLib::PolyFillType fillType, + const bool safety_offset_) { // read input - ClipperLib::Paths input_subject = Slic3rMultiPoints_to_ClipperPaths(subject); - ClipperLib::Paths input_clip = Slic3rMultiPoints_to_ClipperPaths(clip); + ClipperLib::Paths input_subject = Slic3rMultiPoints_to_ClipperPaths(std::forward(subject)); + ClipperLib::Paths input_clip = Slic3rMultiPoints_to_ClipperPaths(std::forward(clip)); // perform safety offset if (safety_offset_) { @@ -648,12 +663,26 @@ _clipper_ln(ClipperLib::ClipType clipType, const Lines &subject, const Polygons return retval; } -ClipperLib::PolyTree -union_pt(const Polygons &subject, bool safety_offset_) +ClipperLib::PolyTree union_pt(const Polygons &subject, bool safety_offset_) { return _clipper_do(ClipperLib::ctUnion, subject, Polygons(), ClipperLib::pftEvenOdd, safety_offset_); } +ClipperLib::PolyTree union_pt(const ExPolygons &subject, bool safety_offset_) +{ + return _clipper_do(ClipperLib::ctUnion, subject, Polygons(), ClipperLib::pftEvenOdd, safety_offset_); +} + +ClipperLib::PolyTree union_pt(Polygons &&subject, bool safety_offset_) +{ + return _clipper_do(ClipperLib::ctUnion, std::move(subject), Polygons(), ClipperLib::pftEvenOdd, safety_offset_); +} + +ClipperLib::PolyTree union_pt(ExPolygons &&subject, bool safety_offset_) +{ + return _clipper_do(ClipperLib::ctUnion, std::move(subject), Polygons(), ClipperLib::pftEvenOdd, safety_offset_); +} + Polygons union_pt_chained(const Polygons &subject, bool safety_offset_) { @@ -664,28 +693,123 @@ union_pt_chained(const Polygons &subject, bool safety_offset_) return retval; } -void traverse_pt(ClipperLib::PolyNodes &nodes, Polygons* retval) +static ClipperLib::PolyNodes order_nodes(const ClipperLib::PolyNodes &nodes) +{ + // collect ordering points + Points ordering_points; + ordering_points.reserve(nodes.size()); + for (const ClipperLib::PolyNode *node : nodes) + ordering_points.emplace_back(Point(node->Contour.front().X, node->Contour.front().Y)); + + // perform the ordering + ClipperLib::PolyNodes ordered_nodes = chain_clipper_polynodes(ordering_points, nodes); + + return ordered_nodes; +} + +enum class e_ordering { + ORDER_POLYNODES, + DONT_ORDER_POLYNODES +}; + +template +void foreach_node(const ClipperLib::PolyNodes &nodes, + std::function fn); + +template<> void foreach_node( + const ClipperLib::PolyNodes & nodes, + std::function fn) +{ + for (auto &n : nodes) fn(n); +} + +template<> void foreach_node( + const ClipperLib::PolyNodes & nodes, + std::function fn) +{ + auto ordered_nodes = order_nodes(nodes); + for (auto &n : ordered_nodes) fn(n); +} + +template +void _traverse_pt(const ClipperLib::PolyNodes &nodes, Polygons *retval) { /* use a nearest neighbor search to order these children TODO: supply start_near to chained_path() too? */ - // collect ordering points - Points ordering_points; - ordering_points.reserve(nodes.size()); - for (ClipperLib::PolyNode *pn : nodes) - ordering_points.emplace_back(Point(pn->Contour.front().X, pn->Contour.front().Y)); - - // perform the ordering - ClipperLib::PolyNodes ordered_nodes = chain_clipper_polynodes(ordering_points, nodes); - // push results recursively - for (ClipperLib::PolyNode *pn : ordered_nodes) { + foreach_node(nodes, [&retval](const ClipperLib::PolyNode *node) { // traverse the next depth - traverse_pt(pn->Childs, retval); - retval->emplace_back(ClipperPath_to_Slic3rPolygon(pn->Contour)); - if (pn->IsHole()) - retval->back().reverse(); // ccw - } + _traverse_pt(node->Childs, retval); + retval->emplace_back(ClipperPath_to_Slic3rPolygon(node->Contour)); + if (node->IsHole()) retval->back().reverse(); // ccw + }); +} + +template +void _traverse_pt(const ClipperLib::PolyNode *tree, ExPolygons *retval) +{ + if (!retval || !tree) return; + + ExPolygons &retv = *retval; + + std::function hole_fn; + + auto contour_fn = [&retv, &hole_fn](const ClipperLib::PolyNode *pptr) { + ExPolygon poly; + poly.contour.points = ClipperPath_to_Slic3rPolygon(pptr->Contour); + auto fn = std::bind(hole_fn, std::placeholders::_1, poly); + foreach_node(pptr->Childs, fn); + retv.push_back(poly); + }; + + hole_fn = [&contour_fn](const ClipperLib::PolyNode *pptr, ExPolygon& poly) + { + poly.holes.emplace_back(); + poly.holes.back().points = ClipperPath_to_Slic3rPolygon(pptr->Contour); + foreach_node(pptr->Childs, contour_fn); + }; + + contour_fn(tree); +} + +template +void _traverse_pt(const ClipperLib::PolyNodes &nodes, ExPolygons *retval) +{ + // Here is the actual traverse + foreach_node(nodes, [&retval](const ClipperLib::PolyNode *node) { + _traverse_pt(node, retval); + }); +} + +void traverse_pt(const ClipperLib::PolyNode *tree, ExPolygons *retval) +{ + _traverse_pt(tree, retval); +} + +void traverse_pt_unordered(const ClipperLib::PolyNode *tree, ExPolygons *retval) +{ + _traverse_pt(tree, retval); +} + +void traverse_pt(const ClipperLib::PolyNodes &nodes, Polygons *retval) +{ + _traverse_pt(nodes, retval); +} + +void traverse_pt(const ClipperLib::PolyNodes &nodes, ExPolygons *retval) +{ + _traverse_pt(nodes, retval); +} + +void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, Polygons *retval) +{ + _traverse_pt(nodes, retval); +} + +void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, ExPolygons *retval) +{ + _traverse_pt(nodes, retval); } Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear) diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 0e58d7fac..d8f8a8f94 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -34,6 +34,7 @@ Slic3r::ExPolygons PolyTreeToExPolygons(ClipperLib::PolyTree& polytree); ClipperLib::Path Slic3rMultiPoint_to_ClipperPath(const Slic3r::MultiPoint &input); ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polygons &input); +ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const ExPolygons &input); ClipperLib::Paths Slic3rMultiPoints_to_ClipperPaths(const Polylines &input); Slic3r::Polygon ClipperPath_to_Slic3rPolygon(const ClipperLib::Path &input); Slic3r::Polyline ClipperPath_to_Slic3rPolyline(const ClipperLib::Path &input); @@ -215,8 +216,19 @@ inline Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject, bool safety_ ClipperLib::PolyTree union_pt(const Slic3r::Polygons &subject, bool safety_offset_ = false); +ClipperLib::PolyTree union_pt(const Slic3r::ExPolygons &subject, bool safety_offset_ = false); +ClipperLib::PolyTree union_pt(Slic3r::Polygons &&subject, bool safety_offset_ = false); +ClipperLib::PolyTree union_pt(Slic3r::ExPolygons &&subject, bool safety_offset_ = false); + Slic3r::Polygons union_pt_chained(const Slic3r::Polygons &subject, bool safety_offset_ = false); -void traverse_pt(ClipperLib::PolyNodes &nodes, Slic3r::Polygons* retval); + +void traverse_pt(const ClipperLib::PolyNodes &nodes, Slic3r::Polygons *retval); +void traverse_pt(const ClipperLib::PolyNodes &nodes, Slic3r::ExPolygons *retval); +void traverse_pt(const ClipperLib::PolyNode *tree, Slic3r::ExPolygons *retval); + +void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, Slic3r::Polygons *retval); +void traverse_pt_unordered(const ClipperLib::PolyNodes &nodes, Slic3r::ExPolygons *retval); +void traverse_pt_unordered(const ClipperLib::PolyNode *tree, Slic3r::ExPolygons *retval); /* OTHER */ Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject, bool preserve_collinear = false); diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 9aab3a0eb..63bd0c801 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -271,8 +271,6 @@ ConfigOptionDef* ConfigDef::add_nullable(const t_config_option_key &opt_key, Con return def; } -std::string ConfigOptionDef::nocli = "~~~noCLI"; - std::ostream& ConfigDef::print_cli_help(std::ostream& out, bool show_defaults, std::function filter) const { // prepare a function for wrapping text diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 334593ab5..463f6ef2d 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -1444,7 +1444,7 @@ public: std::vector cli_args(const std::string &key) const; // Assign this key to cli to disable CLI for this option. - static std::string nocli; + static const constexpr char *nocli = "~~~noCLI"; }; // Map from a config option name to its definition. diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index afbc0931e..a7ef2784c 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -24,6 +24,9 @@ public: ExPolygon& operator=(const ExPolygon &other) { contour = other.contour; holes = other.holes; return *this; } ExPolygon& operator=(ExPolygon &&other) { contour = std::move(other.contour); holes = std::move(other.holes); return *this; } + inline explicit ExPolygon(const Polygon &p): contour(p) {} + inline explicit ExPolygon(Polygon &&p): contour(std::move(p)) {} + Polygon contour; Polygons holes; diff --git a/src/libslic3r/ExPolygonCollection.cpp b/src/libslic3r/ExPolygonCollection.cpp index 6933544b6..c33df0f29 100644 --- a/src/libslic3r/ExPolygonCollection.cpp +++ b/src/libslic3r/ExPolygonCollection.cpp @@ -11,7 +11,7 @@ ExPolygonCollection::ExPolygonCollection(const ExPolygon &expolygon) ExPolygonCollection::operator Points() const { Points points; - Polygons pp = *this; + Polygons pp = (Polygons)*this; for (Polygons::const_iterator poly = pp.begin(); poly != pp.end(); ++poly) { for (Points::const_iterator point = poly->points.begin(); point != poly->points.end(); ++point) points.push_back(*point); diff --git a/src/libslic3r/ExPolygonCollection.hpp b/src/libslic3r/ExPolygonCollection.hpp index 4c181cd6a..35e1eef4e 100644 --- a/src/libslic3r/ExPolygonCollection.hpp +++ b/src/libslic3r/ExPolygonCollection.hpp @@ -13,15 +13,15 @@ typedef std::vector ExPolygonCollections; class ExPolygonCollection { - public: +public: ExPolygons expolygons; - ExPolygonCollection() {}; - ExPolygonCollection(const ExPolygon &expolygon); - ExPolygonCollection(const ExPolygons &expolygons) : expolygons(expolygons) {}; - operator Points() const; - operator Polygons() const; - operator ExPolygons&(); + ExPolygonCollection() {} + explicit ExPolygonCollection(const ExPolygon &expolygon); + explicit ExPolygonCollection(const ExPolygons &expolygons) : expolygons(expolygons) {} + explicit operator Points() const; + explicit operator Polygons() const; + explicit operator ExPolygons&(); void scale(double factor); void translate(double x, double y); void rotate(double angle, const Point ¢er); diff --git a/src/libslic3r/ExtrusionEntity.cpp b/src/libslic3r/ExtrusionEntity.cpp index 7f57b78af..c0d08c84b 100644 --- a/src/libslic3r/ExtrusionEntity.cpp +++ b/src/libslic3r/ExtrusionEntity.cpp @@ -14,12 +14,12 @@ namespace Slic3r { void ExtrusionPath::intersect_expolygons(const ExPolygonCollection &collection, ExtrusionEntityCollection* retval) const { - this->_inflate_collection(intersection_pl(this->polyline, collection), retval); + this->_inflate_collection(intersection_pl(this->polyline, (Polygons)collection), retval); } void ExtrusionPath::subtract_expolygons(const ExPolygonCollection &collection, ExtrusionEntityCollection* retval) const { - this->_inflate_collection(diff_pl(this->polyline, collection), retval); + this->_inflate_collection(diff_pl(this->polyline, (Polygons)collection), retval); } void ExtrusionPath::clip_end(double distance) diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp index f5ac72def..b22d85b65 100644 --- a/src/libslic3r/ExtrusionEntity.hpp +++ b/src/libslic3r/ExtrusionEntity.hpp @@ -14,7 +14,7 @@ class ExtrusionEntityCollection; class Extruder; // Each ExtrusionRole value identifies a distinct set of { extruder, speed } -enum ExtrusionRole { +enum ExtrusionRole : uint8_t { erNone, erPerimeter, erExternalPerimeter, @@ -117,25 +117,16 @@ public: float width; // Height of the extrusion, used for visualization purposes. float height; - // Feedrate of the extrusion, used for visualization purposes. - float feedrate; - // Id of the extruder, used for visualization purposes. - unsigned int extruder_id; - // Id of the color, used for visualization purposes in the color printing case. - unsigned int cp_color_id; - // Fan speed for the extrusion, used for visualization purposes. - float fan_speed; - ExtrusionPath(ExtrusionRole role) : mm3_per_mm(-1), width(-1), height(-1), feedrate(0.0f), extruder_id(0), cp_color_id(0), fan_speed(0.0f), m_role(role) {}; - ExtrusionPath(ExtrusionRole role, double mm3_per_mm, float width, float height) : mm3_per_mm(mm3_per_mm), width(width), height(height), feedrate(0.0f), extruder_id(0), cp_color_id(0), fan_speed(0.0f), m_role(role) {}; - ExtrusionPath(const ExtrusionPath& rhs) : polyline(rhs.polyline), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), feedrate(rhs.feedrate), extruder_id(rhs.extruder_id), cp_color_id(rhs.cp_color_id), fan_speed(rhs.fan_speed), 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), feedrate(rhs.feedrate), extruder_id(rhs.extruder_id), cp_color_id(rhs.cp_color_id), fan_speed(rhs.fan_speed), 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), feedrate(rhs.feedrate), extruder_id(rhs.extruder_id), cp_color_id(rhs.cp_color_id), fan_speed(rhs.fan_speed), 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), feedrate(rhs.feedrate), extruder_id(rhs.extruder_id), cp_color_id(rhs.cp_color_id), fan_speed(rhs.fan_speed), m_role(rhs.m_role) {} -// ExtrusionPath(ExtrusionRole role, const Flow &flow) : m_role(role), mm3_per_mm(flow.mm3_per_mm()), width(flow.width), height(flow.height), feedrate(0.0f), extruder_id(0) {}; + 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& 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->feedrate = rhs.feedrate, this->extruder_id = rhs.extruder_id, this->cp_color_id = rhs.cp_color_id, this->fan_speed = rhs.fan_speed, 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->feedrate = rhs.feedrate, this->extruder_id = rhs.extruder_id, this->cp_color_id = rhs.cp_color_id, this->fan_speed = rhs.fan_speed, this->polyline = std::move(rhs.polyline); return *this; } + 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; } ExtrusionEntity* clone() const override { return new ExtrusionPath(*this); } // Create a new object, initialize it with this object using the move semantics. diff --git a/src/libslic3r/Format/OBJ.cpp b/src/libslic3r/Format/OBJ.cpp index 85ca73551..aa389781a 100644 --- a/src/libslic3r/Format/OBJ.cpp +++ b/src/libslic3r/Format/OBJ.cpp @@ -15,39 +15,41 @@ namespace Slic3r { -bool load_obj(const char *path, Model *model, const char *object_name_in) +bool load_obj(const char *path, TriangleMesh *meshptr) { + if(meshptr == nullptr) return false; + // Parse the OBJ file. ObjParser::ObjData data; if (! ObjParser::objparse(path, data)) { -// die "Failed to parse $file\n" if !-e $path; + // die "Failed to parse $file\n" if !-e $path; return false; } - + // Count the faces and verify, that all faces are triangular. size_t num_faces = 0; - size_t num_quads = 0; + size_t num_quads = 0; for (size_t i = 0; i < data.vertices.size(); ) { size_t j = i; for (; j < data.vertices.size() && data.vertices[j].coordIdx != -1; ++ j) ; if (i == j) continue; - size_t face_vertices = j - i; - if (face_vertices != 3 && face_vertices != 4) { + size_t face_vertices = j - i; + if (face_vertices != 3 && face_vertices != 4) { // Non-triangular and non-quad faces are not supported as of now. return false; } - if (face_vertices == 4) - ++ num_quads; - ++ num_faces; + if (face_vertices == 4) + ++ num_quads; + ++ num_faces; i = j + 1; } - + // Convert ObjData into STL. - TriangleMesh mesh; + TriangleMesh &mesh = *meshptr; stl_file &stl = mesh.stl; stl.stats.type = inmemory; - stl.stats.number_of_facets = int(num_faces + num_quads); + stl.stats.number_of_facets = uint32_t(num_faces + num_quads); stl.stats.original_num_facets = int(num_faces + num_quads); // stl_allocate clears all the allocated data to zero, all normals are set to zeros as well. stl_allocate(&stl); @@ -68,14 +70,14 @@ bool load_obj(const char *path, Model *model, const char *object_name_in) ++ num_normals; } } - if (data.vertices[i].coordIdx != -1) { - // This is a quad. Produce the other triangle. - stl_facet &facet2 = stl.facet_start[i_face++]; + if (data.vertices[i].coordIdx != -1) { + // This is a quad. Produce the other triangle. + stl_facet &facet2 = stl.facet_start[i_face++]; facet2.vertex[0] = facet.vertex[0]; facet2.vertex[1] = facet.vertex[2]; - const ObjParser::ObjVertex &vertex = data.vertices[i++]; - memcpy(facet2.vertex[2].data(), &data.coordinates[vertex.coordIdx * 4], 3 * sizeof(float)); - if (vertex.normalIdx != -1) { + const ObjParser::ObjVertex &vertex = data.vertices[i++]; + memcpy(facet2.vertex[2].data(), &data.coordinates[vertex.coordIdx * 4], 3 * sizeof(float)); + if (vertex.normalIdx != -1) { normal(0) += data.normals[vertex.normalIdx*3]; normal(1) += data.normals[vertex.normalIdx*3+1]; normal(2) += data.normals[vertex.normalIdx*3+2]; @@ -96,25 +98,37 @@ bool load_obj(const char *path, Model *model, const char *object_name_in) if (len > EPSILON) facet.normal = normal / len; } - } + } stl_get_size(&stl); mesh.repair(); if (mesh.facets_count() == 0) { - // die "This STL file couldn't be read because it's empty.\n" + // die "This OBJ file couldn't be read because it's empty.\n" return false; } - - std::string object_name; - if (object_name_in == nullptr) { - const char *last_slash = strrchr(path, DIR_SEPARATOR); - object_name.assign((last_slash == nullptr) ? path : last_slash + 1); - } else - object_name.assign(object_name_in); - - model->add_object(object_name.c_str(), path, std::move(mesh)); + return true; } +bool load_obj(const char *path, Model *model, const char *object_name_in) +{ + TriangleMesh mesh; + + bool ret = load_obj(path, &mesh); + + if (ret) { + std::string object_name; + if (object_name_in == nullptr) { + const char *last_slash = strrchr(path, DIR_SEPARATOR); + object_name.assign((last_slash == nullptr) ? path : last_slash + 1); + } else + object_name.assign(object_name_in); + + model->add_object(object_name.c_str(), path, std::move(mesh)); + } + + return ret; +} + bool store_obj(const char *path, TriangleMesh *mesh) { //FIXME returning false even if write failed. diff --git a/src/libslic3r/Format/OBJ.hpp b/src/libslic3r/Format/OBJ.hpp index 36aa17951..3eb8b4139 100644 --- a/src/libslic3r/Format/OBJ.hpp +++ b/src/libslic3r/Format/OBJ.hpp @@ -5,8 +5,10 @@ namespace Slic3r { class TriangleMesh; class Model; +class ModelObject; // Load an OBJ file into a provided model. +extern bool load_obj(const char *path, TriangleMesh *mesh); extern bool load_obj(const char *path, Model *model, const char *object_name = nullptr); extern bool store_obj(const char *path, TriangleMesh *mesh); diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index efcf15ad5..65264c9cd 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -117,11 +117,11 @@ Polygons AvoidCrossingPerimeters::collect_contours_all_layers(const PrintObjectP const Layer* layer1 = object->layers()[i * 2]; const Layer* layer2 = object->layers()[i * 2 + 1]; Polygons polys; - polys.reserve(layer1->slices.expolygons.size() + layer2->slices.expolygons.size()); - for (const ExPolygon &expoly : layer1->slices.expolygons) + polys.reserve(layer1->slices.size() + layer2->slices.size()); + for (const ExPolygon &expoly : layer1->slices) //FIXME no holes? polys.emplace_back(expoly.contour); - for (const ExPolygon &expoly : layer2->slices.expolygons) + for (const ExPolygon &expoly : layer2->slices) //FIXME no holes? polys.emplace_back(expoly.contour); polygons_per_layer[i] = union_(polys); @@ -130,8 +130,8 @@ Polygons AvoidCrossingPerimeters::collect_contours_all_layers(const PrintObjectP if (object->layers().size() & 1) { const Layer *layer = object->layers().back(); Polygons polys; - polys.reserve(layer->slices.expolygons.size()); - for (const ExPolygon &expoly : layer->slices.expolygons) + polys.reserve(layer->slices.size()); + for (const ExPolygon &expoly : layer->slices) //FIXME no holes? polys.emplace_back(expoly.contour); polygons_per_layer.back() = union_(polys); @@ -1802,11 +1802,8 @@ void GCode::process_layer( // - for each island, we extrude perimeters first, unless user set the infill_first // option // (Still, we have to keep track of regions because we need to apply their config) - size_t n_slices = layer.slices.expolygons.size(); - std::vector layer_surface_bboxes; - layer_surface_bboxes.reserve(n_slices); - for (const ExPolygon &expoly : layer.slices.expolygons) - layer_surface_bboxes.push_back(get_extents(expoly.contour)); + size_t n_slices = layer.slices.size(); + const std::vector &layer_surface_bboxes = layer.slices_bboxes; // Traverse the slices in an increasing order of bounding box size, so that the islands inside another islands are tested first, // so we can just test a point inside ExPolygon::contour and we may skip testing the holes. std::vector slices_test_order; @@ -1822,7 +1819,7 @@ void GCode::process_layer( const BoundingBox &bbox = layer_surface_bboxes[i]; return point(0) >= bbox.min(0) && point(0) < bbox.max(0) && point(1) >= bbox.min(1) && point(1) < bbox.max(1) && - layer.slices.expolygons[i].contour.contains(point); + layer.slices[i].contour.contains(point); }; for (size_t region_id = 0; region_id < print.regions().size(); ++ region_id) { @@ -2418,7 +2415,7 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou static int iRun = 0; SVG svg(debug_out_path("GCode_extrude_loop-%d.svg", iRun ++)); if (m_layer->lower_layer != NULL) - svg.draw(m_layer->lower_layer->slices.expolygons); + svg.draw(m_layer->lower_layer->slices); for (size_t i = 0; i < loop.paths.size(); ++ i) svg.draw(loop.paths[i].as_polyline(), "red"); Polylines polylines; diff --git a/src/libslic3r/GCode/Analyzer.cpp b/src/libslic3r/GCode/Analyzer.cpp index fa4414da9..136959d71 100644 --- a/src/libslic3r/GCode/Analyzer.cpp +++ b/src/libslic3r/GCode/Analyzer.cpp @@ -866,7 +866,7 @@ void GCodeAnalyzer::_calc_gcode_preview_extrusion_layers(GCodePreviewData& previ } // if layer not found, create and return it - layers.emplace_back(z, ExtrusionPaths()); + layers.emplace_back(z, GCodePreviewData::Extrusion::Paths()); return layers.back(); } @@ -875,14 +875,18 @@ void GCodeAnalyzer::_calc_gcode_preview_extrusion_layers(GCodePreviewData& previ // if the polyline is valid, create the extrusion path from it and store it if (polyline.is_valid()) { - ExtrusionPath path(data.extrusion_role, data.mm3_per_mm, data.width, data.height); + auto& paths = get_layer_at_z(preview_data.extrusion.layers, z).paths; + paths.emplace_back(GCodePreviewData::Extrusion::Path()); + GCodePreviewData::Extrusion::Path &path = paths.back(); path.polyline = polyline; + path.extrusion_role = data.extrusion_role; + path.mm3_per_mm = data.mm3_per_mm; + path.width = data.width; + path.height = data.height; path.feedrate = data.feedrate; path.extruder_id = data.extruder_id; - path.fan_speed = data.fan_speed; path.cp_color_id = data.cp_color_id; - - get_layer_at_z(preview_data.extrusion.layers, z).paths.push_back(path); + path.fan_speed = data.fan_speed; } } }; diff --git a/src/libslic3r/GCode/PreviewData.cpp b/src/libslic3r/GCode/PreviewData.cpp index c6cfcc8af..53c13a2f2 100644 --- a/src/libslic3r/GCode/PreviewData.cpp +++ b/src/libslic3r/GCode/PreviewData.cpp @@ -23,7 +23,7 @@ std::vector GCodePreviewData::Color::as_bytes() const return ret; } -GCodePreviewData::Extrusion::Layer::Layer(float z, const ExtrusionPaths& paths) +GCodePreviewData::Extrusion::Layer::Layer(float z, const Paths& paths) : z(z) , paths(paths) { @@ -171,8 +171,8 @@ size_t GCodePreviewData::Extrusion::memory_used() const size_t out = sizeof(*this); out += SLIC3R_STDVEC_MEMSIZE(this->layers, Layer); for (const Layer &layer : this->layers) { - out += SLIC3R_STDVEC_MEMSIZE(layer.paths, ExtrusionPath); - for (const ExtrusionPath &path : layer.paths) + out += SLIC3R_STDVEC_MEMSIZE(layer.paths, Path); + for (const Path &path : layer.paths) out += SLIC3R_STDVEC_MEMSIZE(path.polyline.points, Point); } return out; diff --git a/src/libslic3r/GCode/PreviewData.hpp b/src/libslic3r/GCode/PreviewData.hpp index 7f5b691e1..70b6edffd 100644 --- a/src/libslic3r/GCode/PreviewData.hpp +++ b/src/libslic3r/GCode/PreviewData.hpp @@ -87,12 +87,34 @@ public: static const std::string Default_Extrusion_Role_Names[erCount]; static const EViewType Default_View_Type; + class Path + { + public: + Polyline polyline; + ExtrusionRole extrusion_role; + // Volumetric velocity. mm^3 of plastic per mm of linear head motion. Used by the G-code generator. + float mm3_per_mm; + // Width of the extrusion, used for visualization purposes. + float width; + // Height of the extrusion, used for visualization purposes. + float height; + // Feedrate of the extrusion, used for visualization purposes. + float feedrate; + // Id of the extruder, used for visualization purposes. + uint32_t extruder_id; + // Id of the color, used for visualization purposes in the color printing case. + uint32_t cp_color_id; + // Fan speed for the extrusion, used for visualization purposes. + float fan_speed; + }; + using Paths = std::vector; + struct Layer { float z; - ExtrusionPaths paths; + Paths paths; - Layer(float z, const ExtrusionPaths& paths); + Layer(float z, const Paths& paths); }; typedef std::vector LayersList; diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index a574209d8..394a6e502 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -14,6 +14,11 @@ using boost::polygon::voronoi_builder; using boost::polygon::voronoi_diagram; +namespace ClipperLib { +class PolyNode; +using PolyNodes = std::vector; +} + namespace Slic3r { namespace Geometry { // Generic result of an orientation predicate. diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 94f114a26..74deabf3e 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -47,8 +47,8 @@ void Layer::make_slices() slices = union_ex(slices_p); } - this->slices.expolygons.clear(); - this->slices.expolygons.reserve(slices.size()); + this->slices.clear(); + this->slices.reserve(slices.size()); // prepare ordering points Points ordering_points; @@ -61,7 +61,7 @@ void Layer::make_slices() // populate slices vector for (size_t i : order) - this->slices.expolygons.push_back(std::move(slices[i])); + this->slices.push_back(std::move(slices[i])); } // Merge typed slices into untyped slices. This method is used to revert the effects of detect_surfaces_type() called for posPrepareInfill. @@ -70,7 +70,7 @@ void Layer::merge_slices() if (m_regions.size() == 1) { // Optimization, also more robust. Don't merge classified pieces of layerm->slices, // but use the non-split islands of a layer. For a single region print, these shall be equal. - m_regions.front()->slices.set(this->slices.expolygons, stInternal); + m_regions.front()->slices.set(this->slices, stInternal); } else { for (LayerRegion *layerm : m_regions) // without safety offset, artifacts are generated (GH #2494) diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index 539ae3925..9a4297ce5 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -110,7 +110,8 @@ public: // also known as 'islands' (all regions and surface types are merged here) // The slices are chained by the shortest traverse distance and this traversal // order will be recovered by the G-code generator. - ExPolygonCollection slices; + ExPolygons slices; + std::vector slices_bboxes; size_t region_count() const { return m_regions.size(); } const LayerRegion* get_region(int idx) const { return m_regions.at(idx); } diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index d13549bf4..0ff59d35f 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -140,7 +140,7 @@ void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Poly // Remove voids from fill_boundaries, that are not supported by the layer below. if (lower_layer_covered == nullptr) { lower_layer_covered = &lower_layer_covered_tmp; - lower_layer_covered_tmp = to_polygons(lower_layer->slices.expolygons); + lower_layer_covered_tmp = to_polygons(lower_layer->slices); } if (! lower_layer_covered->empty()) voids = diff(voids, *lower_layer_covered); diff --git a/src/libslic3r/MTUtils.hpp b/src/libslic3r/MTUtils.hpp index 212752883..63ff6fb09 100644 --- a/src/libslic3r/MTUtils.hpp +++ b/src/libslic3r/MTUtils.hpp @@ -252,22 +252,15 @@ template struct remove_cvref template using remove_cvref_t = typename remove_cvref::type; -template class C, class T> -class Container : public C> -{ -public: - explicit Container(size_t count, T &&initval) - : C>(count, initval) - {} -}; - template using DefaultContainer = std::vector; /// Exactly like Matlab https://www.mathworks.com/help/matlab/ref/linspace.html -template class C = DefaultContainer> -inline C> linspace(const T &start, const T &stop, const I &n) +template class Container = DefaultContainer> +inline Container> linspace(const T &start, + const T &stop, + const I &n) { - Container vals(n, T()); + Container> vals(n, T()); T stride = (stop - start) / n; size_t i = 0; @@ -282,10 +275,13 @@ inline C> linspace(const T &start, const T &stop, const I &n) /// in the closest multiple of 'stride' less than or equal to 'end' and /// leaving 'stride' space between each value. /// Very similar to Matlab [start:stride:end] notation. -template class C = DefaultContainer> -inline C> grid(const T &start, const T &stop, const T &stride) +template class Container = DefaultContainer> +inline Container> grid(const T &start, + const T &stop, + const T &stride) { - Container vals(size_t(std::ceil((stop - start) / stride)), T()); + Container> + vals(size_t(std::ceil((stop - start) / stride)), T()); int i = 0; std::generate(vals.begin(), vals.end(), [&i, start, stride] { @@ -387,10 +383,12 @@ unscaled(const Eigen::Matrix &v) noexcept return v.template cast() * SCALING_FACTOR; } -template inline std::vector reserve_vector(size_t capacity) +template // Arbitrary allocator can be used +inline IntegerOnly> reserve_vector(I capacity) { - std::vector ret; - ret.reserve(capacity); + std::vector ret; + if (capacity > I(0)) ret.reserve(size_t(capacity)); + return ret; } diff --git a/src/libslic3r/MotionPlanner.cpp b/src/libslic3r/MotionPlanner.cpp index dd3443879..42bc6c2f1 100644 --- a/src/libslic3r/MotionPlanner.cpp +++ b/src/libslic3r/MotionPlanner.cpp @@ -136,11 +136,11 @@ Polyline MotionPlanner::shortest_path(const Point &from, const Point &to) if (! grown_env.contains(from)) { // delete second point while the line connecting first to third crosses the // boundaries as many times as the current first to second - while (polyline.points.size() > 2 && intersection_ln(Line(from, polyline.points[2]), grown_env).size() == 1) + while (polyline.points.size() > 2 && intersection_ln(Line(from, polyline.points[2]), (Polygons)grown_env).size() == 1) polyline.points.erase(polyline.points.begin() + 1); } if (! grown_env.contains(to)) - while (polyline.points.size() > 2 && intersection_ln(Line(*(polyline.points.end() - 3), to), grown_env).size() == 1) + while (polyline.points.size() > 2 && intersection_ln(Line(*(polyline.points.end() - 3), to), (Polygons)grown_env).size() == 1) polyline.points.erase(polyline.points.end() - 2); } diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 8cd71e697..c0d9e65a7 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -3,7 +3,6 @@ #include "libslic3r.h" #include -#include "ExPolygonCollection.hpp" #include "Flow.hpp" #include "Polygon.hpp" #include "PrintConfig.hpp" @@ -15,7 +14,7 @@ class PerimeterGenerator { public: // Inputs: const SurfaceCollection *slices; - const ExPolygonCollection *lower_slices; + const ExPolygons *lower_slices; double layer_height; int layer_id; Flow perimeter_flow; @@ -45,7 +44,7 @@ public: ExtrusionEntityCollection* gap_fill, // Infills without the gap fills SurfaceCollection* fill_surfaces) - : slices(slices), lower_slices(NULL), layer_height(layer_height), + : slices(slices), lower_slices(nullptr), layer_height(layer_height), layer_id(-1), perimeter_flow(flow), ext_perimeter_flow(flow), overhang_flow(flow), solid_infill_flow(flow), config(config), object_config(object_config), print_config(print_config), diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index f5601276f..245b79e80 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -328,17 +328,6 @@ unsigned int Print::num_object_instances() const return instances; } -void Print::_simplify_slices(double distance) -{ - for (PrintObject *object : m_objects) { - for (Layer *layer : object->m_layers) { - layer->slices.simplify(distance); - for (LayerRegion *layerm : layer->regions()) - layerm->slices.simplify(distance); - } - } -} - double Print::max_allowed_layer_height() const { double nozzle_diameter_max = 0.; @@ -1114,6 +1103,9 @@ std::string Print::validate() const if (m_objects.empty()) return L("All objects are outside of the print volume."); + if (extruders().empty()) + return L("The supplied settings will cause an empty print."); + if (m_config.complete_objects) { // Check horizontal clearance. { @@ -1271,10 +1263,7 @@ std::string Print::validate() const } { - // find the smallest nozzle diameter std::vector extruders = this->extruders(); - if (extruders.empty()) - return L("The supplied settings will cause an empty print."); // Find the smallest used nozzle diameter and the number of unique nozzle diameters. double min_nozzle_diameter = std::numeric_limits::max(); @@ -1593,7 +1582,7 @@ void Print::_make_skirt() for (const Layer *layer : object->m_layers) { if (layer->print_z > skirt_height_z) break; - for (const ExPolygon &expoly : layer->slices.expolygons) + for (const ExPolygon &expoly : layer->slices) // Collect the outer contour points only, ignore holes for the calculation of the convex hull. append(object_points, expoly.contour.points); } @@ -1704,7 +1693,7 @@ void Print::_make_brim() Polygons islands; for (PrintObject *object : m_objects) { Polygons object_islands; - for (ExPolygon &expoly : object->m_layers.front()->slices.expolygons) + for (ExPolygon &expoly : object->m_layers.front()->slices) object_islands.push_back(expoly.contour); if (! object->support_layers().empty()) object->support_layers().front()->support_fills.polygons_covered_by_spacing(object_islands, float(SCALED_EPSILON)); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 5cb13039c..ce616a150 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -180,7 +180,6 @@ private: void _slice(const std::vector &layer_height_profile); std::string _fix_slicing_errors(); void _simplify_slices(double distance); - void _make_perimeters(); bool has_support_material() const; void detect_surfaces_type(); void process_external_surfaces(); @@ -383,7 +382,6 @@ private: void _make_skirt(); void _make_brim(); void _make_wipe_tower(); - void _simplify_slices(double distance); // Declared here to have access to Model / ModelObject / ModelInstance static void model_volume_list_update_supports(ModelObject &model_object_dst, const ModelObject &model_object_src); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 6a57125d0..b3957218d 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2396,6 +2396,7 @@ void PrintConfigDef::init_sla_params() "the threshold in the middle. This behaviour eliminates " "antialiasing without losing holes in polygons."); def->min = 0; + def->max = 1; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(1.0)); @@ -2713,6 +2714,17 @@ void PrintConfigDef::init_sla_params() def->max = 30; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); + + def = this->add("pad_brim_size", coFloat); + def->label = L("Pad brim size"); + def->tooltip = L("How far should the pad extend around the contained geometry"); + def->category = L("Pad"); + // def->tooltip = L(""); + def->sidetext = L("mm"); + def->min = 0; + def->max = 30; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(1.6)); def = this->add("pad_max_merge_distance", coFloat); def->label = L("Max merge distance"); @@ -2753,6 +2765,13 @@ void PrintConfigDef::init_sla_params() def->tooltip = L("Create pad around object and ignore the support elevation"); def->mode = comSimple; def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("pad_around_object_everywhere", coBool); + def->label = L("Pad around object everywhere"); + def->category = L("Pad"); + def->tooltip = L("Force pad around object everywhere"); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); def = this->add("pad_object_gap", coFloat); def->label = L("Pad object gap"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index fc1916f1b..372e70e34 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1030,6 +1030,9 @@ public: // The height of the pad from the bottom to the top not considering the pit ConfigOptionFloat pad_wall_height /*= 5*/; + + // How far should the pad extend around the contained geometry + ConfigOptionFloat pad_brim_size; // The greatest distance where two individual pads are merged into one. The // distance is measured roughly from the centroids of the pads. @@ -1050,7 +1053,9 @@ public: // ///////////////////////////////////////////////////////////////////////// // Disable the elevation (ignore its value) and use the zero elevation mode - ConfigOptionBool pad_around_object; + ConfigOptionBool pad_around_object; + + ConfigOptionBool pad_around_object_everywhere; // This is the gap between the object bottom and the generated pad ConfigOptionFloat pad_object_gap; @@ -1090,10 +1095,12 @@ protected: OPT_PTR(pad_enable); OPT_PTR(pad_wall_thickness); OPT_PTR(pad_wall_height); + OPT_PTR(pad_brim_size); OPT_PTR(pad_max_merge_distance); // OPT_PTR(pad_edge_radius); OPT_PTR(pad_wall_slope); OPT_PTR(pad_around_object); + OPT_PTR(pad_around_object_everywhere); OPT_PTR(pad_object_gap); OPT_PTR(pad_object_connector_stride); OPT_PTR(pad_object_connector_width); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c4f77b6d7..d87e63c27 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -117,6 +117,19 @@ void PrintObject::slice() // Simplify slices if required. if (m_print->config().resolution) this->_simplify_slices(scale_(this->print()->config().resolution)); + // Update bounding boxes + tbb::parallel_for( + tbb::blocked_range(0, m_layers.size()), + [this](const tbb::blocked_range& range) { + for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { + m_print->throw_if_canceled(); + Layer &layer = *m_layers[layer_idx]; + layer.slices_bboxes.clear(); + layer.slices_bboxes.reserve(layer.slices.size()); + for (const ExPolygon &expoly : layer.slices) + layer.slices_bboxes.emplace_back(get_extents(expoly)); + } + }); if (m_layers.empty()) throw std::runtime_error("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n"); this->set_done(posSlice); @@ -865,7 +878,7 @@ void PrintObject::process_external_surfaces() // Shrink the holes, let the layer above expand slightly inside the unsupported areas. polygons_append(voids, offset(surface.expolygon, unsupported_width)); } - surfaces_covered[layer_idx] = diff(to_polygons(this->m_layers[layer_idx]->slices.expolygons), voids); + surfaces_covered[layer_idx] = diff(to_polygons(this->m_layers[layer_idx]->slices), voids); } } ); @@ -975,8 +988,8 @@ void PrintObject::discover_vertical_shells() polygons_append(cache.holes, offset(offset_ex(layer.slices, 0.3f * perimeter_min_spacing), - perimeter_offset - 0.3f * perimeter_min_spacing)); #ifdef SLIC3R_DEBUG_SLICE_PROCESSING { - Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.slices.expolygons)); - svg.draw(layer.slices.expolygons, "blue"); + Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.slices)); + svg.draw(layer.slices, "blue"); svg.draw(union_ex(cache.holes), "red"); svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05)); svg.Close(); @@ -1659,25 +1672,26 @@ void PrintObject::_slice(const std::vector &layer_height_profile) // Trim volumes in a single layer, one by the other, possibly apply upscaling. { Polygons processed; - for (SlicedVolume &sliced_volume : sliced_volumes) { - ExPolygons slices = std::move(sliced_volume.expolygons_by_layer[layer_id]); - if (upscale) - slices = offset_ex(std::move(slices), delta); - if (! processed.empty()) - // Trim by the slices of already processed regions. - slices = diff_ex(to_polygons(std::move(slices)), processed); - if (size_t(&sliced_volume - &sliced_volumes.front()) + 1 < sliced_volumes.size()) - // Collect the already processed regions to trim the to be processed regions. - polygons_append(processed, slices); - sliced_volume.expolygons_by_layer[layer_id] = std::move(slices); - } + for (SlicedVolume &sliced_volume : sliced_volumes) + if (! sliced_volume.expolygons_by_layer.empty()) { + ExPolygons slices = std::move(sliced_volume.expolygons_by_layer[layer_id]); + if (upscale) + slices = offset_ex(std::move(slices), delta); + if (! processed.empty()) + // Trim by the slices of already processed regions. + slices = diff_ex(to_polygons(std::move(slices)), processed); + if (size_t(&sliced_volume - &sliced_volumes.front()) + 1 < sliced_volumes.size()) + // Collect the already processed regions to trim the to be processed regions. + polygons_append(processed, slices); + sliced_volume.expolygons_by_layer[layer_id] = std::move(slices); + } } // Collect and union volumes of a single region. for (int region_id = 0; region_id < (int)this->region_volumes.size(); ++ region_id) { ExPolygons expolygons; size_t num_volumes = 0; for (SlicedVolume &sliced_volume : sliced_volumes) - if (sliced_volume.region_id == region_id && ! sliced_volume.expolygons_by_layer[layer_id].empty()) { + if (sliced_volume.region_id == region_id && ! sliced_volume.expolygons_by_layer.empty() && ! sliced_volume.expolygons_by_layer[layer_id].empty()) { ++ num_volumes; append(expolygons, std::move(sliced_volume.expolygons_by_layer[layer_id])); } @@ -2140,7 +2154,7 @@ std::string PrintObject::_fix_slicing_errors() BOOST_LOG_TRIVIAL(debug) << "Slicing objects - fixing slicing errors in parallel - end"; // remove empty layers from bottom - while (! m_layers.empty() && m_layers.front()->slices.expolygons.empty()) { + while (! m_layers.empty() && m_layers.front()->slices.empty()) { delete m_layers.front(); m_layers.erase(m_layers.begin()); m_layers.front()->lower_layer = nullptr; @@ -2167,115 +2181,17 @@ void PrintObject::_simplify_slices(double distance) Layer *layer = m_layers[layer_idx]; for (size_t region_idx = 0; region_idx < layer->m_regions.size(); ++ region_idx) layer->m_regions[region_idx]->slices.simplify(distance); - layer->slices.simplify(distance); + { + ExPolygons simplified; + for (const ExPolygon& expoly : layer->slices) + expoly.simplify(distance, &simplified); + layer->slices = std::move(simplified); + } } }); BOOST_LOG_TRIVIAL(debug) << "Slicing objects - siplifying slices in parallel - end"; } -void PrintObject::_make_perimeters() -{ - if (! this->set_started(posPerimeters)) - return; - - BOOST_LOG_TRIVIAL(info) << "Generating perimeters..." << log_memory_info(); - - // merge slices if they were split into types - if (this->typed_slices) { - for (Layer *layer : m_layers) - layer->merge_slices(); - this->typed_slices = false; - this->invalidate_step(posPrepareInfill); - } - - // compare each layer to the one below, and mark those slices needing - // one additional inner perimeter, like the top of domed objects- - - // this algorithm makes sure that at least one perimeter is overlapping - // but we don't generate any extra perimeter if fill density is zero, as they would be floating - // inside the object - infill_only_where_needed should be the method of choice for printing - // hollow objects - for (size_t region_id = 0; region_id < this->region_volumes.size(); ++ region_id) { - const PrintRegion ®ion = *m_print->regions()[region_id]; - if (! region.config().extra_perimeters || region.config().perimeters == 0 || region.config().fill_density == 0 || this->layer_count() < 2) - continue; - - BOOST_LOG_TRIVIAL(debug) << "Generating extra perimeters for region " << region_id << " in parallel - start"; - tbb::parallel_for( - tbb::blocked_range(0, m_layers.size() - 1), - [this, ®ion, region_id](const tbb::blocked_range& range) { - for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { - LayerRegion &layerm = *m_layers[layer_idx]->regions()[region_id]; - const LayerRegion &upper_layerm = *m_layers[layer_idx+1]->regions()[region_id]; - const Polygons upper_layerm_polygons = upper_layerm.slices; - // Filter upper layer polygons in intersection_ppl by their bounding boxes? - // my $upper_layerm_poly_bboxes= [ map $_->bounding_box, @{$upper_layerm_polygons} ]; - const double total_loop_length = total_length(upper_layerm_polygons); - const coord_t perimeter_spacing = layerm.flow(frPerimeter).scaled_spacing(); - const Flow ext_perimeter_flow = layerm.flow(frExternalPerimeter); - const coord_t ext_perimeter_width = ext_perimeter_flow.scaled_width(); - const coord_t ext_perimeter_spacing = ext_perimeter_flow.scaled_spacing(); - - for (Surface &slice : layerm.slices.surfaces) { - for (;;) { - // compute the total thickness of perimeters - const coord_t perimeters_thickness = ext_perimeter_width/2 + ext_perimeter_spacing/2 - + (region.config().perimeters-1 + slice.extra_perimeters) * perimeter_spacing; - // define a critical area where we don't want the upper slice to fall into - // (it should either lay over our perimeters or outside this area) - const coord_t critical_area_depth = coord_t(perimeter_spacing * 1.5); - const Polygons critical_area = diff( - offset(slice.expolygon, float(- perimeters_thickness)), - offset(slice.expolygon, float(- perimeters_thickness - critical_area_depth)) - ); - // check whether a portion of the upper slices falls inside the critical area - const Polylines intersection = intersection_pl(to_polylines(upper_layerm_polygons), critical_area); - // only add an additional loop if at least 30% of the slice loop would benefit from it - if (total_length(intersection) <= total_loop_length*0.3) - break; - /* - if (0) { - require "Slic3r/SVG.pm"; - Slic3r::SVG::output( - "extra.svg", - no_arrows => 1, - expolygons => union_ex($critical_area), - polylines => [ map $_->split_at_first_point, map $_->p, @{$upper_layerm->slices} ], - ); - } - */ - ++ slice.extra_perimeters; - } - #ifdef DEBUG - if (slice.extra_perimeters > 0) - printf(" adding %d more perimeter(s) at layer %zu\n", slice.extra_perimeters, layer_idx); - #endif - } - } - }); - BOOST_LOG_TRIVIAL(debug) << "Generating extra perimeters for region " << region_id << " in parallel - end"; - } - - BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - start"; - tbb::parallel_for( - tbb::blocked_range(0, m_layers.size()), - [this](const tbb::blocked_range& range) { - for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) - m_layers[layer_idx]->make_perimeters(); - } - ); - BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end"; - - /* - simplify slices (both layer and region slices), - we only need the max resolution for perimeters - ### This makes this method not-idempotent, so we keep it disabled for now. - ###$self->_simplify_slices(&Slic3r::SCALED_RESOLUTION); - */ - - this->set_done(posPerimeters); -} - // Only active if config->infill_only_where_needed. This step trims the sparse infill, // so it acts as an internal support. It maintains all other infill types intact. // Here the internal surfaces and perimeters have to be supported by the sparse infill. @@ -2301,7 +2217,7 @@ void PrintObject::clip_fill_surfaces() // Detect things that we need to support. // Cummulative slices. Polygons slices; - polygons_append(slices, layer->slices.expolygons); + polygons_append(slices, layer->slices); // Cummulative fill surfaces. Polygons fill_surfaces; // Solid surfaces to be supported. diff --git a/src/libslic3r/SLA/SLAAutoSupports.cpp b/src/libslic3r/SLA/SLAAutoSupports.cpp index 36378df39..65f590143 100644 --- a/src/libslic3r/SLA/SLAAutoSupports.cpp +++ b/src/libslic3r/SLA/SLAAutoSupports.cpp @@ -16,6 +16,7 @@ #include namespace Slic3r { +namespace sla { /*float SLAAutoSupports::approximate_geodesic_distance(const Vec3d& p1, const Vec3d& p2, Vec3d& n1, Vec3d& n2) { @@ -48,9 +49,16 @@ float SLAAutoSupports::distance_limit(float angle) const return 1./(2.4*get_required_density(angle)); }*/ -SLAAutoSupports::SLAAutoSupports(const TriangleMesh& mesh, const sla::EigenMesh3D& emesh, const std::vector& slices, const std::vector& heights, - const Config& config, std::function throw_on_cancel, std::function statusfn) -: m_config(config), m_emesh(emesh), m_throw_on_cancel(throw_on_cancel), m_statusfn(statusfn) +SLAAutoSupports::SLAAutoSupports(const sla::EigenMesh3D & emesh, + const std::vector &slices, + const std::vector & heights, + const Config & config, + std::function throw_on_cancel, + std::function statusfn) + : m_config(config) + , m_emesh(emesh) + , m_throw_on_cancel(throw_on_cancel) + , m_statusfn(statusfn) { process(slices, heights); project_onto_mesh(m_output); @@ -505,6 +513,21 @@ void SLAAutoSupports::uniformly_cover(const ExPolygons& islands, Structure& stru } } +void remove_bottom_points(std::vector &pts, double gnd_lvl, double tolerance) +{ + // get iterator to the reorganized vector end + auto endit = + std::remove_if(pts.begin(), pts.end(), + [tolerance, gnd_lvl](const sla::SupportPoint &sp) { + double diff = std::abs(gnd_lvl - + double(sp.pos(Z))); + return diff <= tolerance; + }); + + // erase all elements after the new end + pts.erase(endit, pts.end()); +} + #ifdef SLA_AUTOSUPPORTS_DEBUG void SLAAutoSupports::output_structures(const std::vector& structures) { @@ -533,4 +556,5 @@ void SLAAutoSupports::output_expolygons(const ExPolygons& expolys, const std::st } #endif +} // namespace sla } // namespace Slic3r diff --git a/src/libslic3r/SLA/SLAAutoSupports.hpp b/src/libslic3r/SLA/SLAAutoSupports.hpp index 38f21e6cf..d2f50f0a4 100644 --- a/src/libslic3r/SLA/SLAAutoSupports.hpp +++ b/src/libslic3r/SLA/SLAAutoSupports.hpp @@ -11,20 +11,22 @@ // #define SLA_AUTOSUPPORTS_DEBUG namespace Slic3r { +namespace sla { class SLAAutoSupports { public: struct Config { - float density_relative; - float minimal_distance; - float head_diameter; + float density_relative {1.f}; + float minimal_distance {1.f}; + float head_diameter {0.4f}; /////////////// inline float support_force() const { return 7.7f / density_relative; } // a force one point can support (arbitrary force unit) inline float tear_pressure() const { return 1.f; } // pressure that the display exerts (the force unit per mm2) }; - SLAAutoSupports(const TriangleMesh& mesh, const sla::EigenMesh3D& emesh, const std::vector& slices, + SLAAutoSupports(const sla::EigenMesh3D& emesh, const std::vector& slices, const std::vector& heights, const Config& config, std::function throw_on_cancel, std::function statusfn); + const std::vector& output() { return m_output; } struct MyLayer; @@ -199,7 +201,9 @@ private: std::function m_statusfn; }; +void remove_bottom_points(std::vector &pts, double gnd_lvl, double tolerance); +} // namespace sla } // namespace Slic3r diff --git a/src/libslic3r/SLA/SLABasePool.cpp b/src/libslic3r/SLA/SLABasePool.cpp deleted file mode 100644 index 53dfef404..000000000 --- a/src/libslic3r/SLA/SLABasePool.cpp +++ /dev/null @@ -1,922 +0,0 @@ -#include "SLABasePool.hpp" -#include "SLABoilerPlate.hpp" - -#include "boost/log/trivial.hpp" -#include "SLABoostAdapter.hpp" -#include "ClipperUtils.hpp" -#include "Tesselate.hpp" -#include "MTUtils.hpp" - -// For debugging: -// #include -// #include -// #include "SVG.hpp" - -namespace Slic3r { namespace sla { - -/// This function will return a triangulation of a sheet connecting an upper -/// and a lower plate given as input polygons. It will not triangulate the -/// plates themselves only the sheet. The caller has to specify the lower and -/// upper z levels in world coordinates as well as the offset difference -/// between the sheets. If the lower_z_mm is higher than upper_z_mm or the -/// offset difference is negative, the resulting triangle orientation will be -/// reversed. -/// -/// IMPORTANT: This is not a universal triangulation algorithm. It assumes -/// that the lower and upper polygons are offsetted versions of the same -/// original polygon. In general, it assumes that one of the polygons is -/// completely inside the other. The offset difference is the reference -/// distance from the inner polygon's perimeter to the outer polygon's -/// perimeter. The real distance will be variable as the clipper offset has -/// different strategies (rounding, etc...). This algorithm should have -/// O(2n + 3m) complexity where n is the number of upper vertices and m is the -/// number of lower vertices. -Contour3D walls(const Polygon& lower, const Polygon& upper, - double lower_z_mm, double upper_z_mm, - double offset_difference_mm, ThrowOnCancel thr) -{ - Contour3D ret; - - if(upper.points.size() < 3 || lower.size() < 3) return ret; - - // The concept of the algorithm is relatively simple. It will try to find - // the closest vertices from the upper and the lower polygon and use those - // as starting points. Then it will create the triangles sequentially using - // an edge from the upper polygon and a vertex from the lower or vice versa, - // depending on the resulting triangle's quality. - // The quality is measured by a scalar value. So far it looks like it is - // enough to derive it from the slope of the triangle's two edges connecting - // the upper and the lower part. A reference slope is calculated from the - // height and the offset difference. - - // Offset in the index array for the ceiling - const auto offs = upper.points.size(); - - // Shorthand for the vertex arrays - auto& upoints = upper.points, &lpoints = lower.points; - auto& rpts = ret.points; auto& ind = ret.indices; - - // If the Z levels are flipped, or the offset difference is negative, we - // will interpret that as the triangles normals should be inverted. - bool inverted = upper_z_mm < lower_z_mm || offset_difference_mm < 0; - - // Copy the points into the mesh, convert them from 2D to 3D - rpts.reserve(upoints.size() + lpoints.size()); - ind.reserve(2 * upoints.size() + 2 * lpoints.size()); - for (auto &p : upoints) - rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), upper_z_mm); - for (auto &p : lpoints) - rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), lower_z_mm); - - // Create pointing indices into vertex arrays. u-upper, l-lower - size_t uidx = 0, lidx = offs, unextidx = 1, lnextidx = offs + 1; - - // Simple squared distance calculation. - auto distfn = [](const Vec3d& p1, const Vec3d& p2) { - auto p = p1 - p2; return p.transpose() * p; - }; - - // We need to find the closest point on lower polygon to the first point on - // the upper polygon. These will be our starting points. - double distmin = std::numeric_limits::max(); - for(size_t l = lidx; l < rpts.size(); ++l) { - thr(); - double d = distfn(rpts[l], rpts[uidx]); - if(d < distmin) { lidx = l; distmin = d; } - } - - // Set up lnextidx to be ahead of lidx in cyclic mode - lnextidx = lidx + 1; - if(lnextidx == rpts.size()) lnextidx = offs; - - // This will be the flip switch to toggle between upper and lower triangle - // creation mode - enum class Proceed { - UPPER, // A segment from the upper polygon and one vertex from the lower - LOWER // A segment from the lower polygon and one vertex from the upper - } proceed = Proceed::UPPER; - - // Flags to help evaluating loop termination. - bool ustarted = false, lstarted = false; - - // The variables for the fitness values, one for the actual and one for the - // previous. - double current_fit = 0, prev_fit = 0; - - // Every triangle of the wall has two edges connecting the upper plate with - // the lower plate. From the length of these two edges and the zdiff we - // can calculate the momentary squared offset distance at a particular - // position on the wall. The average of the differences from the reference - // (squared) offset distance will give us the driving fitness value. - const double offsdiff2 = std::pow(offset_difference_mm, 2); - const double zdiff2 = std::pow(upper_z_mm - lower_z_mm, 2); - - // Mark the current vertex iterator positions. If the iterators return to - // the same position, the loop can be terminated. - size_t uendidx = uidx, lendidx = lidx; - - do { thr(); // check throw if canceled - - prev_fit = current_fit; - - switch(proceed) { // proceed depending on the current state - case Proceed::UPPER: - if(!ustarted || uidx != uendidx) { // there are vertices remaining - // Get the 3D vertices in order - const Vec3d& p_up1 = rpts[uidx]; - const Vec3d& p_low = rpts[lidx]; - const Vec3d& p_up2 = rpts[unextidx]; - - // Calculate fitness: the average of the two connecting edges - double a = offsdiff2 - (distfn(p_up1, p_low) - zdiff2); - double b = offsdiff2 - (distfn(p_up2, p_low) - zdiff2); - current_fit = (std::abs(a) + std::abs(b)) / 2; - - if(current_fit > prev_fit) { // fit is worse than previously - proceed = Proceed::LOWER; - } else { // good to go, create the triangle - inverted - ? ind.emplace_back(int(unextidx), int(lidx), int(uidx)) - : ind.emplace_back(int(uidx), int(lidx), int(unextidx)); - - // Increment the iterators, rotate if necessary - ++uidx; ++unextidx; - if(unextidx == offs) unextidx = 0; - if(uidx == offs) uidx = 0; - - ustarted = true; // mark the movement of the iterators - // so that the comparison to uendidx can be made correctly - } - } else proceed = Proceed::LOWER; - - break; - case Proceed::LOWER: - // Mode with lower segment, upper vertex. Same structure: - if(!lstarted || lidx != lendidx) { - const Vec3d& p_low1 = rpts[lidx]; - const Vec3d& p_low2 = rpts[lnextidx]; - const Vec3d& p_up = rpts[uidx]; - - double a = offsdiff2 - (distfn(p_up, p_low1) - zdiff2); - double b = offsdiff2 - (distfn(p_up, p_low2) - zdiff2); - current_fit = (std::abs(a) + std::abs(b)) / 2; - - if(current_fit > prev_fit) { - proceed = Proceed::UPPER; - } else { - inverted - ? ind.emplace_back(int(uidx), int(lnextidx), int(lidx)) - : ind.emplace_back(int(lidx), int(lnextidx), int(uidx)); - - ++lidx; ++lnextidx; - if(lnextidx == rpts.size()) lnextidx = offs; - if(lidx == rpts.size()) lidx = offs; - - lstarted = true; - } - } else proceed = Proceed::UPPER; - - break; - } // end of switch - } while(!ustarted || !lstarted || uidx != uendidx || lidx != lendidx); - - return ret; -} - -/// Offsetting with clipper and smoothing the edges into a curvature. -void offset(ExPolygon& sh, coord_t distance, bool edgerounding = true) { - using ClipperLib::ClipperOffset; - using ClipperLib::jtRound; - using ClipperLib::jtMiter; - using ClipperLib::etClosedPolygon; - using ClipperLib::Paths; - using ClipperLib::Path; - - auto&& ctour = Slic3rMultiPoint_to_ClipperPath(sh.contour); - auto&& holes = Slic3rMultiPoints_to_ClipperPaths(sh.holes); - - // If the input is not at least a triangle, we can not do this algorithm - if(ctour.size() < 3 || - std::any_of(holes.begin(), holes.end(), - [](const Path& p) { return p.size() < 3; }) - ) { - BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!"; - return; - } - - auto jointype = edgerounding? jtRound : jtMiter; - - ClipperOffset offs; - offs.ArcTolerance = scaled(0.01); - Paths result; - offs.AddPath(ctour, jointype, etClosedPolygon); - offs.AddPaths(holes, jointype, etClosedPolygon); - offs.Execute(result, static_cast(distance)); - - // Offsetting reverts the orientation and also removes the last vertex - // so boost will not have a closed polygon. - - bool found_the_contour = false; - sh.holes.clear(); - for(auto& r : result) { - if(ClipperLib::Orientation(r)) { - // We don't like if the offsetting generates more than one contour - // but throwing would be an overkill. Instead, we should warn the - // caller about the inability to create correct geometries - if(!found_the_contour) { - auto rr = ClipperPath_to_Slic3rPolygon(r); - sh.contour.points.swap(rr.points); - found_the_contour = true; - } else { - BOOST_LOG_TRIVIAL(warning) - << "Warning: offsetting result is invalid!"; - } - } else { - // TODO If there are multiple contours we can't be sure which hole - // belongs to the first contour. (But in this case the situation is - // bad enough to let it go...) - sh.holes.emplace_back(ClipperPath_to_Slic3rPolygon(r)); - } - } -} - -void offset(Polygon &sh, coord_t distance, bool edgerounding = true) -{ - using ClipperLib::ClipperOffset; - using ClipperLib::jtRound; - using ClipperLib::jtMiter; - using ClipperLib::etClosedPolygon; - using ClipperLib::Paths; - using ClipperLib::Path; - - auto &&ctour = Slic3rMultiPoint_to_ClipperPath(sh); - - // If the input is not at least a triangle, we can not do this algorithm - if (ctour.size() < 3) { - BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!"; - return; - } - - ClipperOffset offs; - offs.ArcTolerance = 0.01 * scaled(1.); - Paths result; - offs.AddPath(ctour, edgerounding ? jtRound : jtMiter, etClosedPolygon); - offs.Execute(result, static_cast(distance)); - - // Offsetting reverts the orientation and also removes the last vertex - // so boost will not have a closed polygon. - - bool found_the_contour = false; - for (auto &r : result) { - if (ClipperLib::Orientation(r)) { - // We don't like if the offsetting generates more than one contour - // but throwing would be an overkill. Instead, we should warn the - // caller about the inability to create correct geometries - if (!found_the_contour) { - auto rr = ClipperPath_to_Slic3rPolygon(r); - sh.points.swap(rr.points); - found_the_contour = true; - } else { - BOOST_LOG_TRIVIAL(warning) - << "Warning: offsetting result is invalid!"; - } - } - } -} - -/// Unification of polygons (with clipper) preserving holes as well. -ExPolygons unify(const ExPolygons& shapes) { - using ClipperLib::ptSubject; - - ExPolygons retv; - - bool closed = true; - bool valid = true; - - ClipperLib::Clipper clipper; - - for(auto& path : shapes) { - auto clipperpath = Slic3rMultiPoint_to_ClipperPath(path.contour); - - if(!clipperpath.empty()) - valid &= clipper.AddPath(clipperpath, ptSubject, closed); - - auto clipperholes = Slic3rMultiPoints_to_ClipperPaths(path.holes); - - for(auto& hole : clipperholes) { - if(!hole.empty()) - valid &= clipper.AddPath(hole, ptSubject, closed); - } - } - - if(!valid) BOOST_LOG_TRIVIAL(warning) << "Unification of invalid shapes!"; - - ClipperLib::PolyTree result; - clipper.Execute(ClipperLib::ctUnion, result, ClipperLib::pftNonZero); - - retv.reserve(static_cast(result.Total())); - - // Now we will recursively traverse the polygon tree and serialize it - // into an ExPolygon with holes. The polygon tree has the clipper-ish - // PolyTree structure which alternates its nodes as contours and holes - - // A "declaration" of function for traversing leafs which are holes - std::function processHole; - - // Process polygon which calls processHoles which than calls processPoly - // again until no leafs are left. - auto processPoly = [&retv, &processHole](ClipperLib::PolyNode *pptr) { - ExPolygon poly; - poly.contour.points = ClipperPath_to_Slic3rPolygon(pptr->Contour); - for(auto h : pptr->Childs) { processHole(h, poly); } - retv.push_back(poly); - }; - - // Body of the processHole function - processHole = [&processPoly](ClipperLib::PolyNode *pptr, ExPolygon& poly) - { - poly.holes.emplace_back(); - poly.holes.back().points = ClipperPath_to_Slic3rPolygon(pptr->Contour); - for(auto c : pptr->Childs) processPoly(c); - }; - - // Wrapper for traversing. - auto traverse = [&processPoly] (ClipperLib::PolyNode *node) - { - for(auto ch : node->Childs) { - processPoly(ch); - } - }; - - // Here is the actual traverse - traverse(&result); - - return retv; -} - -Polygons unify(const Polygons& shapes) { - using ClipperLib::ptSubject; - - bool closed = true; - bool valid = true; - - ClipperLib::Clipper clipper; - - for(auto& path : shapes) { - auto clipperpath = Slic3rMultiPoint_to_ClipperPath(path); - - if(!clipperpath.empty()) - valid &= clipper.AddPath(clipperpath, ptSubject, closed); - } - - if(!valid) BOOST_LOG_TRIVIAL(warning) << "Unification of invalid shapes!"; - - ClipperLib::Paths result; - clipper.Execute(ClipperLib::ctUnion, result, ClipperLib::pftNonZero); - - Polygons ret; - for (ClipperLib::Path &p : result) { - Polygon pp = ClipperPath_to_Slic3rPolygon(p); - if (!pp.is_clockwise()) ret.emplace_back(std::move(pp)); - } - - return ret; -} - -// Function to cut tiny connector cavities for a given polygon. The input poly -// will be offsetted by "padding" and small rectangle shaped cavities will be -// inserted along the perimeter in every "stride" distance. The stick rectangles -// will have a with about "stick_width". The input dimensions are in world -// measure, not the scaled clipper units. -void breakstick_holes(ExPolygon& poly, - double padding, - double stride, - double stick_width, - double penetration) -{ - // SVG svg("bridgestick_plate.svg"); - // svg.draw(poly); - - auto transf = [stick_width, penetration, padding, stride](Points &pts) { - // The connector stick will be a small rectangle with dimensions - // stick_width x (penetration + padding) to have some penetration - // into the input polygon. - - Points out; - out.reserve(2 * pts.size()); // output polygon points - - // stick bottom and right edge dimensions - double sbottom = scaled(stick_width); - double sright = scaled(penetration + padding); - - // scaled stride distance - double sstride = scaled(stride); - double t = 0; - - // process pairs of vertices as an edge, start with the last and - // first point - for (size_t i = pts.size() - 1, j = 0; j < pts.size(); i = j, ++j) { - // Get vertices and the direction vectors - const Point &a = pts[i], &b = pts[j]; - Vec2d dir = b.cast() - a.cast(); - double nrm = dir.norm(); - dir /= nrm; - Vec2d dirp(-dir(Y), dir(X)); - - // Insert start point - out.emplace_back(a); - - // dodge the start point, do not make sticks on the joins - while (t < sbottom) t += sbottom; - double tend = nrm - sbottom; - - while (t < tend) { // insert the stick on the polygon perimeter - - // calculate the stick rectangle vertices and insert them - // into the output. - Point p1 = a + (t * dir).cast(); - Point p2 = p1 + (sright * dirp).cast(); - Point p3 = p2 + (sbottom * dir).cast(); - Point p4 = p3 + (sright * -dirp).cast(); - out.insert(out.end(), {p1, p2, p3, p4}); - - // continue along the perimeter - t += sstride; - } - - t = t - nrm; - - // Insert edge endpoint - out.emplace_back(b); - } - - // move the new points - out.shrink_to_fit(); - pts.swap(out); - }; - - if(stride > 0.0 && stick_width > 0.0 && padding > 0.0) { - transf(poly.contour.points); - for (auto &h : poly.holes) transf(h.points); - } - - // svg.draw(poly); - // svg.Close(); -} - -/// This method will create a rounded edge around a flat polygon in 3d space. -/// 'base_plate' parameter is the target plate. -/// 'radius' is the radius of the edges. -/// 'degrees' is tells how much of a circle should be created as the rounding. -/// It should be in degrees, not radians. -/// 'ceilheight_mm' is the Z coordinate of the flat polygon in 3D space. -/// 'dir' Is the direction of the round edges: inward or outward -/// 'thr' Throws if a cancel signal was received -/// 'last_offset' An auxiliary output variable to save the last offsetted -/// version of 'base_plate' -/// 'last_height' An auxiliary output to save the last z coordinate of the -/// offsetted base_plate. In other words, where the rounded edges end. -Contour3D round_edges(const ExPolygon& base_plate, - double radius_mm, - double degrees, - double ceilheight_mm, - bool dir, - ThrowOnCancel thr, - ExPolygon& last_offset, double& last_height) -{ - auto ob = base_plate; - auto ob_prev = ob; - double wh = ceilheight_mm, wh_prev = wh; - Contour3D curvedwalls; - - int steps = 30; - double stepx = radius_mm / steps; - coord_t s = dir? 1 : -1; - degrees = std::fmod(degrees, 180); - - // we use sin for x distance because we interpret the angle starting from - // PI/2 - int tos = degrees < 90? - int(radius_mm*std::cos(degrees * PI / 180 - PI/2) / stepx) : steps; - - for(int i = 1; i <= tos; ++i) { - thr(); - - ob = base_plate; - - double r2 = radius_mm * radius_mm; - double xx = i*stepx; - double x2 = xx*xx; - double stepy = std::sqrt(r2 - x2); - - offset(ob, s * scaled(xx)); - wh = ceilheight_mm - radius_mm + stepy; - - Contour3D pwalls; - double prev_x = xx - (i - 1) * stepx; - pwalls = walls(ob.contour, ob_prev.contour, wh, wh_prev, s*prev_x, thr); - - curvedwalls.merge(pwalls); - ob_prev = ob; - wh_prev = wh; - } - - if(degrees > 90) { - double tox = radius_mm - radius_mm*std::cos(degrees * PI / 180 - PI/2); - int tos = int(tox / stepx); - - for(int i = 1; i <= tos; ++i) { - thr(); - ob = base_plate; - - double r2 = radius_mm * radius_mm; - double xx = radius_mm - i*stepx; - double x2 = xx*xx; - double stepy = std::sqrt(r2 - x2); - offset(ob, s * scaled(xx)); - wh = ceilheight_mm - radius_mm - stepy; - - Contour3D pwalls; - double prev_x = xx - radius_mm + (i - 1)*stepx; - pwalls = - walls(ob_prev.contour, ob.contour, wh_prev, wh, s*prev_x, thr); - - curvedwalls.merge(pwalls); - ob_prev = ob; - wh_prev = wh; - } - } - - last_offset = std::move(ob); - last_height = wh; - - return curvedwalls; -} - -inline Point centroid(Points& pp) { - Point c; - switch(pp.size()) { - case 0: break; - case 1: c = pp.front(); break; - case 2: c = (pp[0] + pp[1]) / 2; break; - default: { - auto MAX = std::numeric_limits::max(); - auto MIN = std::numeric_limits::min(); - Point min = {MAX, MAX}, max = {MIN, MIN}; - - for(auto& p : pp) { - if(p(0) < min(0)) min(0) = p(0); - if(p(1) < min(1)) min(1) = p(1); - if(p(0) > max(0)) max(0) = p(0); - if(p(1) > max(1)) max(1) = p(1); - } - c(0) = min(0) + (max(0) - min(0)) / 2; - c(1) = min(1) + (max(1) - min(1)) / 2; - - // TODO: fails for non convex cluster -// c = std::accumulate(pp.begin(), pp.end(), Point{0, 0}); -// x(c) /= coord_t(pp.size()); y(c) /= coord_t(pp.size()); - break; - } - } - - return c; -} - -inline Point centroid(const Polygon& poly) { - return poly.centroid(); -} - -/// A fake concave hull that is constructed by connecting separate shapes -/// with explicit bridges. Bridges are generated from each shape's centroid -/// to the center of the "scene" which is the centroid calculated from the shape -/// centroids (a star is created...) -Polygons concave_hull(const Polygons& polys, double maxd_mm, ThrowOnCancel thr) -{ - namespace bgi = boost::geometry::index; - using SpatElement = std::pair; - using SpatIndex = bgi::rtree< SpatElement, bgi::rstar<16, 4> >; - - if(polys.empty()) return Polygons(); - - const double max_dist = scaled(maxd_mm); - - Polygons punion = unify(polys); // could be redundant - - if(punion.size() == 1) return punion; - - // We get the centroids of all the islands in the 2D slice - Points centroids; centroids.reserve(punion.size()); - std::transform(punion.begin(), punion.end(), std::back_inserter(centroids), - [](const Polygon& poly) { return centroid(poly); }); - - SpatIndex ctrindex; - unsigned idx = 0; - for(const Point &ct : centroids) ctrindex.insert(std::make_pair(ct, idx++)); - - // Centroid of the centroids of islands. This is where the additional - // connector sticks are routed. - Point cc = centroid(centroids); - - punion.reserve(punion.size() + centroids.size()); - - idx = 0; - std::transform(centroids.begin(), centroids.end(), - std::back_inserter(punion), - [¢roids, &ctrindex, cc, max_dist, &idx, thr] - (const Point& c) - { - thr(); - double dx = x(c) - x(cc), dy = y(c) - y(cc); - double l = std::sqrt(dx * dx + dy * dy); - double nx = dx / l, ny = dy / l; - - Point& ct = centroids[idx]; - - std::vector result; - ctrindex.query(bgi::nearest(ct, 2), std::back_inserter(result)); - - double dist = max_dist; - for (const SpatElement &el : result) - if (el.second != idx) { - dist = Line(el.first, ct).length(); - break; - } - - idx++; - - if (dist >= max_dist) return Polygon(); - - Polygon r; - auto& ctour = r.points; - - ctour.reserve(3); - ctour.emplace_back(cc); - - Point d(scaled(nx), scaled(ny)); - ctour.emplace_back(c + Point( -y(d), x(d) )); - ctour.emplace_back(c + Point( y(d), -x(d) )); - offset(r, scaled(1.)); - - return r; - }); - - // This is unavoidable... - punion = unify(punion); - - return punion; -} - -void base_plate(const TriangleMesh & mesh, - ExPolygons & output, - const std::vector &heights, - ThrowOnCancel thrfn) -{ - if (mesh.empty()) return; - // m.require_shared_vertices(); // TriangleMeshSlicer needs this - TriangleMeshSlicer slicer(&mesh); - - std::vector out; out.reserve(heights.size()); - slicer.slice(heights, 0.f, &out, thrfn); - - size_t count = 0; for(auto& o : out) count += o.size(); - - // Now we have to unify all slice layers which can be an expensive operation - // so we will try to simplify the polygons - ExPolygons tmp; tmp.reserve(count); - for(ExPolygons& o : out) - for(ExPolygon& e : o) { - auto&& exss = e.simplify(scaled(0.1)); - for(ExPolygon& ep : exss) tmp.emplace_back(std::move(ep)); - } - - ExPolygons utmp = unify(tmp); - - for(auto& o : utmp) { - auto&& smp = o.simplify(scaled(0.1)); - output.insert(output.end(), smp.begin(), smp.end()); - } -} - -void base_plate(const TriangleMesh &mesh, - ExPolygons & output, - float h, - float layerh, - ThrowOnCancel thrfn) -{ - auto bb = mesh.bounding_box(); - float gnd = float(bb.min(Z)); - std::vector heights = {float(bb.min(Z))}; - - for(float hi = gnd + layerh; hi <= gnd + h; hi += layerh) - heights.emplace_back(hi); - - base_plate(mesh, output, heights, thrfn); -} - -Contour3D create_base_pool(const Polygons &ground_layer, - const ExPolygons &obj_self_pad = {}, - const PoolConfig& cfg = PoolConfig()) -{ - // for debugging: - // Benchmark bench; - // bench.start(); - - double mergedist = 2*(1.8*cfg.min_wall_thickness_mm + 4*cfg.edge_radius_mm)+ - cfg.max_merge_distance_mm; - - // Here we get the base polygon from which the pad has to be generated. - // We create an artificial concave hull from this polygon and that will - // serve as the bottom plate of the pad. We will offset this concave hull - // and then offset back the result with clipper with rounding edges ON. This - // trick will create a nice rounded pad shape. - Polygons concavehs = concave_hull(ground_layer, mergedist, cfg.throw_on_cancel); - - const double thickness = cfg.min_wall_thickness_mm; - const double wingheight = cfg.min_wall_height_mm; - const double fullheight = wingheight + thickness; - const double slope = cfg.wall_slope; - const double wingdist = wingheight / std::tan(slope); - const double bottom_offs = (thickness + wingheight) / std::tan(slope); - - // scaled values - const coord_t s_thickness = scaled(thickness); - const coord_t s_eradius = scaled(cfg.edge_radius_mm); - const coord_t s_safety_dist = 2*s_eradius + coord_t(0.8*s_thickness); - const coord_t s_wingdist = scaled(wingdist); - const coord_t s_bottom_offs = scaled(bottom_offs); - - auto& thrcl = cfg.throw_on_cancel; - - Contour3D pool; - - for(Polygon& concaveh : concavehs) { - if(concaveh.points.empty()) return pool; - - // Here lies the trick that does the smoothing only with clipper offset - // calls. The offset is configured to round edges. Inner edges will - // be rounded because we offset twice: ones to get the outer (top) plate - // and again to get the inner (bottom) plate - auto outer_base = concaveh; - offset(outer_base, s_safety_dist + s_wingdist + s_thickness); - - ExPolygon bottom_poly; bottom_poly.contour = outer_base; - offset(bottom_poly, -s_bottom_offs); - - // Punching a hole in the top plate for the cavity - ExPolygon top_poly; - ExPolygon middle_base; - ExPolygon inner_base; - top_poly.contour = outer_base; - - if(wingheight > 0) { - inner_base.contour = outer_base; - offset(inner_base, -(s_thickness + s_wingdist + s_eradius)); - - middle_base.contour = outer_base; - offset(middle_base, -s_thickness); - top_poly.holes.emplace_back(middle_base.contour); - auto& tph = top_poly.holes.back().points; - std::reverse(tph.begin(), tph.end()); - } - - ExPolygon ob; ob.contour = outer_base; double wh = 0; - - // now we will calculate the angle or portion of the circle from - // pi/2 that will connect perfectly with the bottom plate. - // this is a tangent point calculation problem and the equation can - // be found for example here: - // http://www.ambrsoft.com/TrigoCalc/Circles2/CirclePoint/CirclePointDistance.htm - // the y coordinate would be: - // y = cy + (r^2*py - r*px*sqrt(px^2 + py^2 - r^2) / (px^2 + py^2) - // where px and py are the coordinates of the point outside the circle - // cx and cy are the circle center, r is the radius - // We place the circle center to (0, 0) in the calculation the make - // things easier. - // to get the angle we use arcsin function and subtract 90 degrees then - // flip the sign to get the right input to the round_edge function. - double r = cfg.edge_radius_mm; - double cy = 0; - double cx = 0; - double px = thickness + wingdist; - double py = r - fullheight; - - double pxcx = px - cx; - double pycy = py - cy; - double b_2 = pxcx*pxcx + pycy*pycy; - double r_2 = r*r; - double D = std::sqrt(b_2 - r_2); - double vy = (r_2*pycy - r*pxcx*D) / b_2; - double phi = -(std::asin(vy/r) * 180 / PI - 90); - - - // Generate the smoothed edge geometry - if(s_eradius > 0) pool.merge(round_edges(ob, - r, - phi, - 0, // z position of the input plane - true, - thrcl, - ob, wh)); - - // Now that we have the rounded edge connecting the top plate with - // the outer side walls, we can generate and merge the sidewall geometry - pool.merge(walls(ob.contour, bottom_poly.contour, wh, -fullheight, - bottom_offs, thrcl)); - - if(wingheight > 0) { - // Generate the smoothed edge geometry - wh = 0; - ob = middle_base; - if(s_eradius) pool.merge(round_edges(middle_base, - r, - phi - 90, // from tangent lines - 0, // z position of the input plane - false, - thrcl, - ob, wh)); - - // Next is the cavity walls connecting to the top plate's - // artificially created hole. - pool.merge(walls(inner_base.contour, ob.contour, -wingheight, - wh, -wingdist, thrcl)); - } - - if (cfg.embed_object) { - ExPolygons bttms = diff_ex(to_polygons(bottom_poly), - to_polygons(obj_self_pad)); - - assert(!bttms.empty()); - - std::sort(bttms.begin(), bttms.end(), - [](const ExPolygon& e1, const ExPolygon& e2) { - return e1.contour.area() > e2.contour.area(); - }); - - if(wingheight > 0) inner_base.holes = bttms.front().holes; - else top_poly.holes = bttms.front().holes; - - auto straight_walls = - [&pool](const Polygon &cntr, coord_t z_low, coord_t z_high) { - - auto lines = cntr.lines(); - - for (auto &l : lines) { - auto s = coord_t(pool.points.size()); - auto& pts = pool.points; - pts.emplace_back(unscale(l.a.x(), l.a.y(), z_low)); - pts.emplace_back(unscale(l.b.x(), l.b.y(), z_low)); - pts.emplace_back(unscale(l.a.x(), l.a.y(), z_high)); - pts.emplace_back(unscale(l.b.x(), l.b.y(), z_high)); - - pool.indices.emplace_back(s, s + 1, s + 3); - pool.indices.emplace_back(s, s + 3, s + 2); - } - }; - - coord_t z_lo = -scaled(fullheight), z_hi = -scaled(wingheight); - for (ExPolygon &ep : bttms) { - pool.merge(triangulate_expolygon_3d(ep, -fullheight, true)); - for (auto &h : ep.holes) straight_walls(h, z_lo, z_hi); - } - - // Skip the outer contour, triangulate the holes - for (auto it = std::next(bttms.begin()); it != bttms.end(); ++it) { - pool.merge(triangulate_expolygon_3d(*it, -wingheight)); - straight_walls(it->contour, z_lo, z_hi); - } - - } else { - // Now we need to triangulate the top and bottom plates as well as - // the cavity bottom plate which is the same as the bottom plate - // but it is elevated by the thickness. - - pool.merge(triangulate_expolygon_3d(bottom_poly, -fullheight, true)); - } - - pool.merge(triangulate_expolygon_3d(top_poly)); - - if(wingheight > 0) - pool.merge(triangulate_expolygon_3d(inner_base, -wingheight)); - - } - - return pool; -} - -void create_base_pool(const Polygons &ground_layer, TriangleMesh& out, - const ExPolygons &holes, const PoolConfig& cfg) -{ - - - // For debugging: - // bench.stop(); - // std::cout << "Pad creation time: " << bench.getElapsedSec() << std::endl; - // std::fstream fout("pad_debug.obj", std::fstream::out); - // if(fout.good()) pool.to_obj(fout); - - out.merge(mesh(create_base_pool(ground_layer, holes, cfg))); -} - -} -} diff --git a/src/libslic3r/SLA/SLABasePool.hpp b/src/libslic3r/SLA/SLABasePool.hpp deleted file mode 100644 index eec426bbf..000000000 --- a/src/libslic3r/SLA/SLABasePool.hpp +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef SLABASEPOOL_HPP -#define SLABASEPOOL_HPP - -#include -#include -#include - -namespace Slic3r { - -class ExPolygon; -class Polygon; -using ExPolygons = std::vector; -using Polygons = std::vector; - -class TriangleMesh; - -namespace sla { - -using ThrowOnCancel = std::function; - -/// Calculate the polygon representing the silhouette from the specified height -void base_plate(const TriangleMesh& mesh, // input mesh - ExPolygons& output, // Output will be merged with - float samplingheight = 0.1f, // The height range to sample - float layerheight = 0.05f, // The sampling height - ThrowOnCancel thrfn = [](){}); // Will be called frequently - -void base_plate(const TriangleMesh& mesh, // input mesh - ExPolygons& output, // Output will be merged with - const std::vector&, // Exact Z levels to sample - ThrowOnCancel thrfn = [](){}); // Will be called frequently - -// Function to cut tiny connector cavities for a given polygon. The input poly -// will be offsetted by "padding" and small rectangle shaped cavities will be -// inserted along the perimeter in every "stride" distance. The stick rectangles -// will have a with about "stick_width". The input dimensions are in world -// measure, not the scaled clipper units. -void breakstick_holes(ExPolygon &poly, - double padding, - double stride, - double stick_width, - double penetration = 0.0); - -Polygons concave_hull(const Polygons& polys, double max_dist_mm = 50, - ThrowOnCancel throw_on_cancel = [](){}); - -struct PoolConfig { - double min_wall_thickness_mm = 2; - double min_wall_height_mm = 5; - double max_merge_distance_mm = 50; - double edge_radius_mm = 1; - double wall_slope = std::atan(1.0); // Universal constant for Pi/4 - struct EmbedObject { - double object_gap_mm = 0.5; - double stick_stride_mm = 10; - double stick_width_mm = 0.3; - double stick_penetration_mm = 0.1; - bool enabled = false; - operator bool() const { return enabled; } - } embed_object; - - ThrowOnCancel throw_on_cancel = [](){}; - - inline PoolConfig() {} - inline PoolConfig(double wt, double wh, double md, double er, double slope): - min_wall_thickness_mm(wt), - min_wall_height_mm(wh), - max_merge_distance_mm(md), - edge_radius_mm(er), - wall_slope(slope) {} -}; - -/// Calculate the pool for the mesh for SLA printing -void create_base_pool(const Polygons& base_plate, - TriangleMesh& output_mesh, - const ExPolygons& holes, - const PoolConfig& = PoolConfig()); - -/// Returns the elevation needed for compensating the pad. -inline double get_pad_elevation(const PoolConfig& cfg) { - return cfg.min_wall_thickness_mm; -} - -inline double get_pad_fullheight(const PoolConfig& cfg) { - return cfg.min_wall_height_mm + cfg.min_wall_thickness_mm; -} - -} - -} - -#endif // SLABASEPOOL_HPP diff --git a/src/libslic3r/SLA/SLABoilerPlate.hpp b/src/libslic3r/SLA/SLABoilerPlate.hpp index 86e90f3b7..d7ce26bb2 100644 --- a/src/libslic3r/SLA/SLABoilerPlate.hpp +++ b/src/libslic3r/SLA/SLABoilerPlate.hpp @@ -8,35 +8,19 @@ #include #include +#include "SLACommon.hpp" +#include "SLASpatIndex.hpp" + namespace Slic3r { namespace sla { -/// Get x and y coordinates (because we are eigenizing...) -inline coord_t x(const Point& p) { return p(0); } -inline coord_t y(const Point& p) { return p(1); } -inline coord_t& x(Point& p) { return p(0); } -inline coord_t& y(Point& p) { return p(1); } - -inline coordf_t x(const Vec3d& p) { return p(0); } -inline coordf_t y(const Vec3d& p) { return p(1); } -inline coordf_t z(const Vec3d& p) { return p(2); } -inline coordf_t& x(Vec3d& p) { return p(0); } -inline coordf_t& y(Vec3d& p) { return p(1); } -inline coordf_t& z(Vec3d& p) { return p(2); } - -inline coord_t& x(Vec3crd& p) { return p(0); } -inline coord_t& y(Vec3crd& p) { return p(1); } -inline coord_t& z(Vec3crd& p) { return p(2); } -inline coord_t x(const Vec3crd& p) { return p(0); } -inline coord_t y(const Vec3crd& p) { return p(1); } -inline coord_t z(const Vec3crd& p) { return p(2); } - /// Intermediate struct for a 3D mesh struct Contour3D { Pointf3s points; std::vector indices; - void merge(const Contour3D& ctr) { + Contour3D& merge(const Contour3D& ctr) + { auto s3 = coord_t(points.size()); auto s = indices.size(); @@ -44,21 +28,27 @@ struct Contour3D { indices.insert(indices.end(), ctr.indices.begin(), ctr.indices.end()); for(size_t n = s; n < indices.size(); n++) { - auto& idx = indices[n]; x(idx) += s3; y(idx) += s3; z(idx) += s3; + auto& idx = indices[n]; idx.x() += s3; idx.y() += s3; idx.z() += s3; } + + return *this; } - void merge(const Pointf3s& triangles) { + Contour3D& merge(const Pointf3s& triangles) + { const size_t offs = points.size(); points.insert(points.end(), triangles.begin(), triangles.end()); indices.reserve(indices.size() + points.size() / 3); - - for(int i = (int)offs; i < (int)points.size(); i += 3) + + for(int i = int(offs); i < int(points.size()); i += 3) indices.emplace_back(i, i + 1, i + 2); + + return *this; } // Write the index triangle structure to OBJ file for debugging purposes. - void to_obj(std::ostream& stream) { + void to_obj(std::ostream& stream) + { for(auto& p : points) { stream << "v " << p.transpose() << "\n"; } @@ -72,6 +62,31 @@ struct Contour3D { using ClusterEl = std::vector; using ClusteredPoints = std::vector; +// Clustering a set of points by the given distance. +ClusteredPoints cluster(const std::vector& indices, + std::function pointfn, + double dist, + unsigned max_points); + +ClusteredPoints cluster(const PointSet& points, + double dist, + unsigned max_points); + +ClusteredPoints cluster( + const std::vector& indices, + std::function pointfn, + std::function predicate, + unsigned max_points); + + +// Calculate the normals for the selected points (from 'points' set) on the +// mesh. This will call squared distance for each point. +PointSet normals(const PointSet& points, + const EigenMesh3D& mesh, + double eps = 0.05, // min distance from edges + std::function throw_on_cancel = [](){}, + const std::vector& selected_points = {}); + /// Mesh from an existing contour. inline TriangleMesh mesh(const Contour3D& ctour) { return {ctour.points, ctour.indices}; diff --git a/src/libslic3r/SLA/SLACommon.hpp b/src/libslic3r/SLA/SLACommon.hpp index d8e258035..97b459676 100644 --- a/src/libslic3r/SLA/SLACommon.hpp +++ b/src/libslic3r/SLA/SLACommon.hpp @@ -1,8 +1,9 @@ #ifndef SLACOMMON_HPP #define SLACOMMON_HPP -#include #include +#include +#include // #define SLIC3R_SLA_NEEDS_WINDTREE @@ -69,6 +70,8 @@ struct SupportPoint } }; +using SupportPoints = std::vector; + /// An index-triangle structure for libIGL functions. Also serves as an /// alternative (raw) input format for the SLASupportTree class EigenMesh3D { @@ -175,6 +178,8 @@ public: } }; +using PointSet = Eigen::MatrixXd; + } // namespace sla } // namespace Slic3r diff --git a/src/libslic3r/SLA/SLAConcurrency.hpp b/src/libslic3r/SLA/SLAConcurrency.hpp new file mode 100644 index 000000000..4beb2aead --- /dev/null +++ b/src/libslic3r/SLA/SLAConcurrency.hpp @@ -0,0 +1,56 @@ +#ifndef SLACONCURRENCY_H +#define SLACONCURRENCY_H + +#include +#include +#include + +namespace Slic3r { +namespace sla { + +// Set this to true to enable full parallelism in this module. +// Only the well tested parts will be concurrent if this is set to false. +const constexpr bool USE_FULL_CONCURRENCY = true; + +template struct _ccr {}; + +template<> struct _ccr +{ + using SpinningMutex = tbb::spin_mutex; + using BlockingMutex = tbb::mutex; + + template + static inline void enumerate(It from, It to, Fn fn) + { + auto iN = to - from; + size_t N = iN < 0 ? 0 : size_t(iN); + + tbb::parallel_for(size_t(0), N, [from, fn](size_t n) { + fn(*(from + decltype(iN)(n)), n); + }); + } +}; + +template<> struct _ccr +{ +private: + struct _Mtx { inline void lock() {} inline void unlock() {} }; + +public: + using SpinningMutex = _Mtx; + using BlockingMutex = _Mtx; + + template + static inline void enumerate(It from, It to, Fn fn) + { + for (auto it = from; it != to; ++it) fn(*it, size_t(it - from)); + } +}; + +using ccr = _ccr; +using ccr_seq = _ccr; +using ccr_par = _ccr; + +}} // namespace Slic3r::sla + +#endif // SLACONCURRENCY_H diff --git a/src/libslic3r/SLA/SLAPad.cpp b/src/libslic3r/SLA/SLAPad.cpp new file mode 100644 index 000000000..71f8b1c7f --- /dev/null +++ b/src/libslic3r/SLA/SLAPad.cpp @@ -0,0 +1,870 @@ +#include "SLAPad.hpp" +#include "SLABoilerPlate.hpp" +#include "SLASpatIndex.hpp" + +#include "boost/log/trivial.hpp" +#include "SLABoostAdapter.hpp" +#include "ClipperUtils.hpp" +#include "Tesselate.hpp" +#include "MTUtils.hpp" + +// For debugging: +// #include +// #include +#include "SVG.hpp" + +#include "I18N.hpp" +#include + +//! macro used to mark string used at localization, +//! return same string +#define L(s) Slic3r::I18N::translate(s) + +namespace Slic3r { namespace sla { + +namespace { + +/// This function will return a triangulation of a sheet connecting an upper +/// and a lower plate given as input polygons. It will not triangulate the +/// plates themselves only the sheet. The caller has to specify the lower and +/// upper z levels in world coordinates as well as the offset difference +/// between the sheets. If the lower_z_mm is higher than upper_z_mm or the +/// offset difference is negative, the resulting triangle orientation will be +/// reversed. +/// +/// IMPORTANT: This is not a universal triangulation algorithm. It assumes +/// that the lower and upper polygons are offsetted versions of the same +/// original polygon. In general, it assumes that one of the polygons is +/// completely inside the other. The offset difference is the reference +/// distance from the inner polygon's perimeter to the outer polygon's +/// perimeter. The real distance will be variable as the clipper offset has +/// different strategies (rounding, etc...). This algorithm should have +/// O(2n + 3m) complexity where n is the number of upper vertices and m is the +/// number of lower vertices. +Contour3D walls( + const Polygon &lower, + const Polygon &upper, + double lower_z_mm, + double upper_z_mm, + double offset_difference_mm, + ThrowOnCancel thr = [] {}) +{ + Contour3D ret; + + if(upper.points.size() < 3 || lower.size() < 3) return ret; + + // The concept of the algorithm is relatively simple. It will try to find + // the closest vertices from the upper and the lower polygon and use those + // as starting points. Then it will create the triangles sequentially using + // an edge from the upper polygon and a vertex from the lower or vice versa, + // depending on the resulting triangle's quality. + // The quality is measured by a scalar value. So far it looks like it is + // enough to derive it from the slope of the triangle's two edges connecting + // the upper and the lower part. A reference slope is calculated from the + // height and the offset difference. + + // Offset in the index array for the ceiling + const auto offs = upper.points.size(); + + // Shorthand for the vertex arrays + auto& upts = upper.points, &lpts = lower.points; + auto& rpts = ret.points; auto& ind = ret.indices; + + // If the Z levels are flipped, or the offset difference is negative, we + // will interpret that as the triangles normals should be inverted. + bool inverted = upper_z_mm < lower_z_mm || offset_difference_mm < 0; + + // Copy the points into the mesh, convert them from 2D to 3D + rpts.reserve(upts.size() + lpts.size()); + ind.reserve(2 * upts.size() + 2 * lpts.size()); + for (auto &p : upts) + rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), upper_z_mm); + for (auto &p : lpts) + rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), lower_z_mm); + + // Create pointing indices into vertex arrays. u-upper, l-lower + size_t uidx = 0, lidx = offs, unextidx = 1, lnextidx = offs + 1; + + // Simple squared distance calculation. + auto distfn = [](const Vec3d& p1, const Vec3d& p2) { + auto p = p1 - p2; return p.transpose() * p; + }; + + // We need to find the closest point on lower polygon to the first point on + // the upper polygon. These will be our starting points. + double distmin = std::numeric_limits::max(); + for(size_t l = lidx; l < rpts.size(); ++l) { + thr(); + double d = distfn(rpts[l], rpts[uidx]); + if(d < distmin) { lidx = l; distmin = d; } + } + + // Set up lnextidx to be ahead of lidx in cyclic mode + lnextidx = lidx + 1; + if(lnextidx == rpts.size()) lnextidx = offs; + + // This will be the flip switch to toggle between upper and lower triangle + // creation mode + enum class Proceed { + UPPER, // A segment from the upper polygon and one vertex from the lower + LOWER // A segment from the lower polygon and one vertex from the upper + } proceed = Proceed::UPPER; + + // Flags to help evaluating loop termination. + bool ustarted = false, lstarted = false; + + // The variables for the fitness values, one for the actual and one for the + // previous. + double current_fit = 0, prev_fit = 0; + + // Every triangle of the wall has two edges connecting the upper plate with + // the lower plate. From the length of these two edges and the zdiff we + // can calculate the momentary squared offset distance at a particular + // position on the wall. The average of the differences from the reference + // (squared) offset distance will give us the driving fitness value. + const double offsdiff2 = std::pow(offset_difference_mm, 2); + const double zdiff2 = std::pow(upper_z_mm - lower_z_mm, 2); + + // Mark the current vertex iterator positions. If the iterators return to + // the same position, the loop can be terminated. + size_t uendidx = uidx, lendidx = lidx; + + do { thr(); // check throw if canceled + + prev_fit = current_fit; + + switch(proceed) { // proceed depending on the current state + case Proceed::UPPER: + if(!ustarted || uidx != uendidx) { // there are vertices remaining + // Get the 3D vertices in order + const Vec3d& p_up1 = rpts[uidx]; + const Vec3d& p_low = rpts[lidx]; + const Vec3d& p_up2 = rpts[unextidx]; + + // Calculate fitness: the average of the two connecting edges + double a = offsdiff2 - (distfn(p_up1, p_low) - zdiff2); + double b = offsdiff2 - (distfn(p_up2, p_low) - zdiff2); + current_fit = (std::abs(a) + std::abs(b)) / 2; + + if(current_fit > prev_fit) { // fit is worse than previously + proceed = Proceed::LOWER; + } else { // good to go, create the triangle + inverted + ? ind.emplace_back(int(unextidx), int(lidx), int(uidx)) + : ind.emplace_back(int(uidx), int(lidx), int(unextidx)); + + // Increment the iterators, rotate if necessary + ++uidx; ++unextidx; + if(unextidx == offs) unextidx = 0; + if(uidx == offs) uidx = 0; + + ustarted = true; // mark the movement of the iterators + // so that the comparison to uendidx can be made correctly + } + } else proceed = Proceed::LOWER; + + break; + case Proceed::LOWER: + // Mode with lower segment, upper vertex. Same structure: + if(!lstarted || lidx != lendidx) { + const Vec3d& p_low1 = rpts[lidx]; + const Vec3d& p_low2 = rpts[lnextidx]; + const Vec3d& p_up = rpts[uidx]; + + double a = offsdiff2 - (distfn(p_up, p_low1) - zdiff2); + double b = offsdiff2 - (distfn(p_up, p_low2) - zdiff2); + current_fit = (std::abs(a) + std::abs(b)) / 2; + + if(current_fit > prev_fit) { + proceed = Proceed::UPPER; + } else { + inverted + ? ind.emplace_back(int(uidx), int(lnextidx), int(lidx)) + : ind.emplace_back(int(lidx), int(lnextidx), int(uidx)); + + ++lidx; ++lnextidx; + if(lnextidx == rpts.size()) lnextidx = offs; + if(lidx == rpts.size()) lidx = offs; + + lstarted = true; + } + } else proceed = Proceed::UPPER; + + break; + } // end of switch + } while(!ustarted || !lstarted || uidx != uendidx || lidx != lendidx); + + return ret; +} + +// Same as walls() but with identical higher and lower polygons. +Contour3D inline straight_walls(const Polygon &plate, + double lo_z, + double hi_z, + ThrowOnCancel thr) +{ + return walls(plate, plate, lo_z, hi_z, .0 /*offset_diff*/, thr); +} + +// As it shows, the current offset_ex in ClipperUtils hangs if used in jtRound +// mode +ClipperLib::Paths fast_offset(const ClipperLib::Paths &paths, + coord_t delta, + ClipperLib::JoinType jointype) +{ + using ClipperLib::ClipperOffset; + using ClipperLib::etClosedPolygon; + using ClipperLib::Paths; + using ClipperLib::Path; + + ClipperOffset offs; + offs.ArcTolerance = scaled(0.01); + + for (auto &p : paths) + // If the input is not at least a triangle, we can not do this algorithm + if(p.size() < 3) { + BOOST_LOG_TRIVIAL(error) << "Invalid geometry for offsetting!"; + return {}; + } + + offs.AddPaths(paths, jointype, etClosedPolygon); + + Paths result; + offs.Execute(result, static_cast(delta)); + + return result; +} + + +// Function to cut tiny connector cavities for a given polygon. The input poly +// will be offsetted by "padding" and small rectangle shaped cavities will be +// inserted along the perimeter in every "stride" distance. The stick rectangles +// will have a with about "stick_width". The input dimensions are in world +// measure, not the scaled clipper units. +void breakstick_holes(Points& pts, + double padding, + double stride, + double stick_width, + double penetration) +{ + if(stride <= EPSILON || stick_width <= EPSILON || padding <= EPSILON) + return; + + // SVG svg("bridgestick_plate.svg"); + // svg.draw(poly); + + // The connector stick will be a small rectangle with dimensions + // stick_width x (penetration + padding) to have some penetration + // into the input polygon. + + Points out; + out.reserve(2 * pts.size()); // output polygon points + + // stick bottom and right edge dimensions + double sbottom = scaled(stick_width); + double sright = scaled(penetration + padding); + + // scaled stride distance + double sstride = scaled(stride); + double t = 0; + + // process pairs of vertices as an edge, start with the last and + // first point + for (size_t i = pts.size() - 1, j = 0; j < pts.size(); i = j, ++j) { + // Get vertices and the direction vectors + const Point &a = pts[i], &b = pts[j]; + Vec2d dir = b.cast() - a.cast(); + double nrm = dir.norm(); + dir /= nrm; + Vec2d dirp(-dir(Y), dir(X)); + + // Insert start point + out.emplace_back(a); + + // dodge the start point, do not make sticks on the joins + while (t < sbottom) t += sbottom; + double tend = nrm - sbottom; + + while (t < tend) { // insert the stick on the polygon perimeter + + // calculate the stick rectangle vertices and insert them + // into the output. + Point p1 = a + (t * dir).cast(); + Point p2 = p1 + (sright * dirp).cast(); + Point p3 = p2 + (sbottom * dir).cast(); + Point p4 = p3 + (sright * -dirp).cast(); + out.insert(out.end(), {p1, p2, p3, p4}); + + // continue along the perimeter + t += sstride; + } + + t = t - nrm; + + // Insert edge endpoint + out.emplace_back(b); + } + + // move the new points + out.shrink_to_fit(); + pts.swap(out); +} + +template +ExPolygons breakstick_holes(const ExPolygons &input, Args...args) +{ + ExPolygons ret = input; + for (ExPolygon &p : ret) { + breakstick_holes(p.contour.points, args...); + for (auto &h : p.holes) breakstick_holes(h.points, args...); + } + + return ret; +} + +/// A fake concave hull that is constructed by connecting separate shapes +/// with explicit bridges. Bridges are generated from each shape's centroid +/// to the center of the "scene" which is the centroid calculated from the shape +/// centroids (a star is created...) +class ConcaveHull { + Polygons m_polys; + + Point centroid(const Points& pp) const + { + Point c; + switch(pp.size()) { + case 0: break; + case 1: c = pp.front(); break; + case 2: c = (pp[0] + pp[1]) / 2; break; + default: { + auto MAX = std::numeric_limits::max(); + auto MIN = std::numeric_limits::min(); + Point min = {MAX, MAX}, max = {MIN, MIN}; + + for(auto& p : pp) { + if(p(0) < min(0)) min(0) = p(0); + if(p(1) < min(1)) min(1) = p(1); + if(p(0) > max(0)) max(0) = p(0); + if(p(1) > max(1)) max(1) = p(1); + } + c(0) = min(0) + (max(0) - min(0)) / 2; + c(1) = min(1) + (max(1) - min(1)) / 2; + break; + } + } + + return c; + } + + inline Point centroid(const Polygon &poly) const { return poly.centroid(); } + + Points calculate_centroids() const + { + // We get the centroids of all the islands in the 2D slice + Points centroids = reserve_vector(m_polys.size()); + std::transform(m_polys.begin(), m_polys.end(), + std::back_inserter(centroids), + [this](const Polygon &poly) { return centroid(poly); }); + + return centroids; + } + + void merge_polygons() { m_polys = union_(m_polys); } + + void add_connector_rectangles(const Points ¢roids, + coord_t max_dist, + ThrowOnCancel thr) + { + namespace bgi = boost::geometry::index; + using PointIndexElement = std::pair; + using PointIndex = bgi::rtree>; + + // Centroid of the centroids of islands. This is where the additional + // connector sticks are routed. + Point cc = centroid(centroids); + + PointIndex ctrindex; + unsigned idx = 0; + for(const Point &ct : centroids) + ctrindex.insert(std::make_pair(ct, idx++)); + + m_polys.reserve(m_polys.size() + centroids.size()); + + idx = 0; + for (const Point &c : centroids) { + thr(); + + double dx = c.x() - cc.x(), dy = c.y() - cc.y(); + double l = std::sqrt(dx * dx + dy * dy); + double nx = dx / l, ny = dy / l; + + const Point &ct = centroids[idx]; + + std::vector result; + ctrindex.query(bgi::nearest(ct, 2), std::back_inserter(result)); + + double dist = max_dist; + for (const PointIndexElement &el : result) + if (el.second != idx) { + dist = Line(el.first, ct).length(); + break; + } + + idx++; + + if (dist >= max_dist) return; + + Polygon r; + r.points.reserve(3); + r.points.emplace_back(cc); + + Point d(scaled(nx), scaled(ny)); + r.points.emplace_back(c + Point(-d.y(), d.x())); + r.points.emplace_back(c + Point(d.y(), -d.x())); + offset(r, scaled(1.)); + + m_polys.emplace_back(r); + } + } + +public: + + ConcaveHull(const ExPolygons& polys, double merge_dist, ThrowOnCancel thr) + : ConcaveHull{to_polygons(polys), merge_dist, thr} {} + + ConcaveHull(const Polygons& polys, double mergedist, ThrowOnCancel thr) + { + if(polys.empty()) return; + + m_polys = polys; + merge_polygons(); + + if(m_polys.size() == 1) return; + + Points centroids = calculate_centroids(); + + add_connector_rectangles(centroids, scaled(mergedist), thr); + + merge_polygons(); + } + + // const Polygons & polygons() const { return m_polys; } + + ExPolygons to_expolygons() const + { + auto ret = reserve_vector(m_polys.size()); + for (const Polygon &p : m_polys) ret.emplace_back(ExPolygon(p)); + return ret; + } + + void offset_waffle_style(coord_t delta) { + ClipperLib::Paths paths = Slic3rMultiPoints_to_ClipperPaths(m_polys); + paths = fast_offset(paths, 2 * delta, ClipperLib::jtRound); + paths = fast_offset(paths, -delta, ClipperLib::jtRound); + m_polys = ClipperPaths_to_Slic3rPolygons(paths); + } + + static inline coord_t get_waffle_offset(const PadConfig &c) + { + return scaled(c.brim_size_mm + c.wing_distance()); + } + + static inline double get_merge_distance(const PadConfig &c) + { + return 2. * (1.8 * c.wall_thickness_mm) + c.max_merge_dist_mm; + } +}; + +// Part of the pad configuration that is used for 3D geometry generation +struct PadConfig3D { + double thickness, height, wing_height, slope; + + explicit PadConfig3D(const PadConfig &cfg2d) + : thickness{cfg2d.wall_thickness_mm} + , height{cfg2d.full_height()} + , wing_height{cfg2d.wall_height_mm} + , slope{cfg2d.wall_slope} + {} + + inline double bottom_offset() const + { + return (thickness + wing_height) / std::tan(slope); + } +}; + +// Outer part of the skeleton is used to generate the waffled edges of the pad. +// Inner parts will not be waffled or offsetted. Inner parts are only used if +// pad is generated around the object and correspond to holes and inner polygons +// in the model blueprint. +struct PadSkeleton { ExPolygons inner, outer; }; + +PadSkeleton divide_blueprint(const ExPolygons &bp) +{ + ClipperLib::PolyTree ptree = union_pt(bp); + + PadSkeleton ret; + ret.inner.reserve(size_t(ptree.Total())); + ret.outer.reserve(size_t(ptree.Total())); + + for (ClipperLib::PolyTree::PolyNode *node : ptree.Childs) { + ExPolygon poly(ClipperPath_to_Slic3rPolygon(node->Contour)); + for (ClipperLib::PolyTree::PolyNode *child : node->Childs) { + if (child->IsHole()) { + poly.holes.emplace_back( + ClipperPath_to_Slic3rPolygon(child->Contour)); + + traverse_pt_unordered(child->Childs, &ret.inner); + } + else traverse_pt_unordered(child, &ret.inner); + } + + ret.outer.emplace_back(poly); + } + + return ret; +} + +// A helper class for storing polygons and maintaining a spatial index of their +// bounding boxes. +class Intersector { + BoxIndex m_index; + ExPolygons m_polys; + +public: + + // Add a new polygon to the index + void add(const ExPolygon &ep) + { + m_polys.emplace_back(ep); + m_index.insert(BoundingBox{ep}, unsigned(m_index.size())); + } + + // Check an arbitrary polygon for intersection with the indexed polygons + bool intersects(const ExPolygon &poly) + { + // Create a suitable query bounding box. + auto bb = poly.contour.bounding_box(); + + std::vector qres = m_index.query(bb, BoxIndex::qtIntersects); + + // Now check intersections on the actual polygons (not just the boxes) + bool is_overlap = false; + auto qit = qres.begin(); + while (!is_overlap && qit != qres.end()) + is_overlap = is_overlap || poly.overlaps(m_polys[(qit++)->second]); + + return is_overlap; + } +}; + +// This dummy intersector to implement the "force pad everywhere" feature +struct DummyIntersector +{ + inline void add(const ExPolygon &) {} + inline bool intersects(const ExPolygon &) { return true; } +}; + +template +class _AroundPadSkeleton : public PadSkeleton +{ + // A spatial index used to be able to efficiently find intersections of + // support polygons with the model polygons. + _Intersector m_intersector; + +public: + _AroundPadSkeleton(const ExPolygons &support_blueprint, + const ExPolygons &model_blueprint, + const PadConfig & cfg, + ThrowOnCancel thr) + { + // We need to merge the support and the model contours in a special + // way in which the model contours have to be substracted from the + // support contours. The pad has to have a hole in which the model can + // fit perfectly (thus the substraction -- diff_ex). Also, the pad has + // to be eliminated from areas where there is no need for a pad, due + // to missing supports. + + add_supports_to_index(support_blueprint); + + auto model_bp_offs = + offset_ex(model_blueprint, + scaled(cfg.embed_object.object_gap_mm), + ClipperLib::jtMiter, 1); + + ConcaveHull fullcvh = + wafflized_concave_hull(support_blueprint, model_bp_offs, cfg, thr); + + auto model_bp_sticks = + breakstick_holes(model_bp_offs, cfg.embed_object.object_gap_mm, + cfg.embed_object.stick_stride_mm, + cfg.embed_object.stick_width_mm, + cfg.embed_object.stick_penetration_mm); + + ExPolygons fullpad = diff_ex(fullcvh.to_expolygons(), model_bp_sticks); + + remove_redundant_parts(fullpad); + + PadSkeleton divided = divide_blueprint(fullpad); + outer = std::move(divided.outer); + inner = std::move(divided.inner); + } + +private: + + // Add the support blueprint to the search index to be queried later + void add_supports_to_index(const ExPolygons &supp_bp) + { + for (auto &ep : supp_bp) m_intersector.add(ep); + } + + // Create the wafflized pad around all object in the scene. This pad doesnt + // have any holes yet. + ConcaveHull wafflized_concave_hull(const ExPolygons &supp_bp, + const ExPolygons &model_bp, + const PadConfig &cfg, + ThrowOnCancel thr) + { + auto allin = reserve_vector(supp_bp.size() + model_bp.size()); + + for (auto &ep : supp_bp) allin.emplace_back(ep.contour); + for (auto &ep : model_bp) allin.emplace_back(ep.contour); + + ConcaveHull ret{allin, ConcaveHull::get_merge_distance(cfg), thr}; + ret.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg)); + + return ret; + } + + // To remove parts of the pad skeleton which do not host any supports + void remove_redundant_parts(ExPolygons &parts) + { + auto endit = std::remove_if(parts.begin(), parts.end(), + [this](const ExPolygon &p) { + return !m_intersector.intersects(p); + }); + + parts.erase(endit, parts.end()); + } +}; + +using AroundPadSkeleton = _AroundPadSkeleton; +using BrimPadSkeleton = _AroundPadSkeleton; + +class BelowPadSkeleton : public PadSkeleton +{ +public: + BelowPadSkeleton(const ExPolygons &support_blueprint, + const ExPolygons &model_blueprint, + const PadConfig & cfg, + ThrowOnCancel thr) + { + outer.reserve(support_blueprint.size() + model_blueprint.size()); + + for (auto &ep : support_blueprint) outer.emplace_back(ep.contour); + for (auto &ep : model_blueprint) outer.emplace_back(ep.contour); + + ConcaveHull ochull{outer, ConcaveHull::get_merge_distance(cfg), thr}; + + ochull.offset_waffle_style(ConcaveHull::get_waffle_offset(cfg)); + outer = ochull.to_expolygons(); + } +}; + +// Offset the contour only, leave the holes untouched +template +ExPolygon offset_contour_only(const ExPolygon &poly, coord_t delta, Args...args) +{ + ExPolygons tmp = offset_ex(poly.contour, float(delta), args...); + + if (tmp.empty()) return {}; + + Polygons holes = poly.holes; + for (auto &h : holes) h.reverse(); + + tmp = diff_ex(to_polygons(tmp), holes); + + if (tmp.empty()) return {}; + + return tmp.front(); +} + +bool add_cavity(Contour3D &pad, ExPolygon &top_poly, const PadConfig3D &cfg, + ThrowOnCancel thr) +{ + auto logerr = []{BOOST_LOG_TRIVIAL(error)<<"Could not create pad cavity";}; + + double wing_distance = cfg.wing_height / std::tan(cfg.slope); + coord_t delta_inner = -scaled(cfg.thickness + wing_distance); + coord_t delta_middle = -scaled(cfg.thickness); + ExPolygon inner_base = offset_contour_only(top_poly, delta_inner); + ExPolygon middle_base = offset_contour_only(top_poly, delta_middle); + + if (inner_base.empty() || middle_base.empty()) { logerr(); return false; } + + ExPolygons pdiff = diff_ex(top_poly, middle_base.contour); + + if (pdiff.size() != 1) { logerr(); return false; } + + top_poly = pdiff.front(); + + double z_min = -cfg.wing_height, z_max = 0; + double offset_difference = -wing_distance; + pad.merge(walls(inner_base.contour, middle_base.contour, z_min, z_max, + offset_difference, thr)); + + pad.merge(triangulate_expolygon_3d(inner_base, z_min, NORMALS_UP)); + + return true; +} + +Contour3D create_outer_pad_geometry(const ExPolygons & skeleton, + const PadConfig3D &cfg, + ThrowOnCancel thr) +{ + Contour3D ret; + + for (const ExPolygon &pad_part : skeleton) { + ExPolygon top_poly{pad_part}; + ExPolygon bottom_poly = + offset_contour_only(pad_part, -scaled(cfg.bottom_offset())); + + if (bottom_poly.empty()) continue; + + double z_min = -cfg.height, z_max = 0; + ret.merge(walls(top_poly.contour, bottom_poly.contour, z_max, z_min, + cfg.bottom_offset(), thr)); + + if (cfg.wing_height > 0. && add_cavity(ret, top_poly, cfg, thr)) + z_max = -cfg.wing_height; + + for (auto &h : bottom_poly.holes) + ret.merge(straight_walls(h, z_max, z_min, thr)); + + ret.merge(triangulate_expolygon_3d(bottom_poly, z_min, NORMALS_DOWN)); + ret.merge(triangulate_expolygon_3d(top_poly, NORMALS_UP)); + } + + return ret; +} + +Contour3D create_inner_pad_geometry(const ExPolygons & skeleton, + const PadConfig3D &cfg, + ThrowOnCancel thr) +{ + Contour3D ret; + + double z_max = 0., z_min = -cfg.height; + for (const ExPolygon &pad_part : skeleton) { + ret.merge(straight_walls(pad_part.contour, z_max, z_min,thr)); + + for (auto &h : pad_part.holes) + ret.merge(straight_walls(h, z_max, z_min, thr)); + + ret.merge(triangulate_expolygon_3d(pad_part, z_min, NORMALS_DOWN)); + ret.merge(triangulate_expolygon_3d(pad_part, z_max, NORMALS_UP)); + } + + return ret; +} + +Contour3D create_pad_geometry(const PadSkeleton &skelet, + const PadConfig & cfg, + ThrowOnCancel thr) +{ +#ifndef NDEBUG + SVG svg("pad_skeleton.svg"); + svg.draw(skelet.outer, "green"); + svg.draw(skelet.inner, "blue"); + svg.Close(); +#endif + + PadConfig3D cfg3d(cfg); + return create_outer_pad_geometry(skelet.outer, cfg3d, thr) + .merge(create_inner_pad_geometry(skelet.inner, cfg3d, thr)); +} + +Contour3D create_pad_geometry(const ExPolygons &supp_bp, + const ExPolygons &model_bp, + const PadConfig & cfg, + ThrowOnCancel thr) +{ + PadSkeleton skelet; + + if (cfg.embed_object.enabled) { + if (cfg.embed_object.everywhere) + skelet = BrimPadSkeleton(supp_bp, model_bp, cfg, thr); + else + skelet = AroundPadSkeleton(supp_bp, model_bp, cfg, thr); + } else + skelet = BelowPadSkeleton(supp_bp, model_bp, cfg, thr); + + return create_pad_geometry(skelet, cfg, thr); +} + +} // namespace + +void pad_blueprint(const TriangleMesh & mesh, + ExPolygons & output, + const std::vector &heights, + ThrowOnCancel thrfn) +{ + if (mesh.empty()) return; + TriangleMeshSlicer slicer(&mesh); + + auto out = reserve_vector(heights.size()); + slicer.slice(heights, 0.f, &out, thrfn); + + size_t count = 0; + for(auto& o : out) count += o.size(); + + // Unification is expensive, a simplify also speeds up the pad generation + auto tmp = reserve_vector(count); + for(ExPolygons& o : out) + for(ExPolygon& e : o) { + auto&& exss = e.simplify(scaled(0.1)); + for(ExPolygon& ep : exss) tmp.emplace_back(std::move(ep)); + } + + ExPolygons utmp = union_ex(tmp); + + for(auto& o : utmp) { + auto&& smp = o.simplify(scaled(0.1)); + output.insert(output.end(), smp.begin(), smp.end()); + } +} + +void pad_blueprint(const TriangleMesh &mesh, + ExPolygons & output, + float h, + float layerh, + ThrowOnCancel thrfn) +{ + float gnd = float(mesh.bounding_box().min(Z)); + + std::vector slicegrid = grid(gnd, gnd + h, layerh); + pad_blueprint(mesh, output, slicegrid, thrfn); +} + +void create_pad(const ExPolygons &sup_blueprint, + const ExPolygons &model_blueprint, + TriangleMesh & out, + const PadConfig & cfg, + ThrowOnCancel thr) +{ + Contour3D t = create_pad_geometry(sup_blueprint, model_blueprint, cfg, thr); + out.merge(mesh(std::move(t))); +} + +std::string PadConfig::validate() const +{ + static const double constexpr MIN_BRIM_SIZE_MM = .1; + + if (brim_size_mm < MIN_BRIM_SIZE_MM || + bottom_offset() > brim_size_mm + wing_distance() || + ConcaveHull::get_waffle_offset(*this) <= MIN_BRIM_SIZE_MM) + return L("Pad brim size is too small for the current configuration."); + + return ""; +} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SLAPad.hpp b/src/libslic3r/SLA/SLAPad.hpp new file mode 100644 index 000000000..4abcdd281 --- /dev/null +++ b/src/libslic3r/SLA/SLAPad.hpp @@ -0,0 +1,94 @@ +#ifndef SLABASEPOOL_HPP +#define SLABASEPOOL_HPP + +#include +#include +#include +#include + +namespace Slic3r { + +class ExPolygon; +class Polygon; +using ExPolygons = std::vector; +using Polygons = std::vector; + +class TriangleMesh; + +namespace sla { + +using ThrowOnCancel = std::function; + +/// Calculate the polygon representing the silhouette. +void pad_blueprint( + const TriangleMesh &mesh, // input mesh + ExPolygons & output, // Output will be merged with + const std::vector &, // Exact Z levels to sample + ThrowOnCancel thrfn = [] {}); // Function that throws if cancel was requested + +void pad_blueprint( + const TriangleMesh &mesh, + ExPolygons & output, + float samplingheight = 0.1f, // The height range to sample + float layerheight = 0.05f, // The sampling height + ThrowOnCancel thrfn = [] {}); + +struct PadConfig { + double wall_thickness_mm = 1.; + double wall_height_mm = 1.; + double max_merge_dist_mm = 50; + double wall_slope = std::atan(1.0); // Universal constant for Pi/4 + double brim_size_mm = 1.6; + + struct EmbedObject { + double object_gap_mm = 1.; + double stick_stride_mm = 10.; + double stick_width_mm = 0.5; + double stick_penetration_mm = 0.1; + bool enabled = false; + bool everywhere = false; + operator bool() const { return enabled; } + } embed_object; + + inline PadConfig() = default; + inline PadConfig(double thickness, + double height, + double mergedist, + double slope) + : wall_thickness_mm(thickness) + , wall_height_mm(height) + , max_merge_dist_mm(mergedist) + , wall_slope(slope) + {} + + inline double bottom_offset() const + { + return (wall_thickness_mm + wall_height_mm) / std::tan(wall_slope); + } + + inline double wing_distance() const + { + return wall_height_mm / std::tan(wall_slope); + } + + inline double full_height() const + { + return wall_height_mm + wall_thickness_mm; + } + + /// Returns the elevation needed for compensating the pad. + inline double required_elevation() const { return wall_thickness_mm; } + + std::string validate() const; +}; + +void create_pad(const ExPolygons &support_contours, + const ExPolygons &model_contours, + TriangleMesh & output_mesh, + const PadConfig & = PadConfig(), + ThrowOnCancel throw_on_cancel = []{}); + +} // namespace sla +} // namespace Slic3r + +#endif // SLABASEPOOL_HPP diff --git a/src/libslic3r/SLA/SLARaster.cpp b/src/libslic3r/SLA/SLARaster.cpp index 32a88b1b5..091cadd23 100644 --- a/src/libslic3r/SLA/SLARaster.cpp +++ b/src/libslic3r/SLA/SLARaster.cpp @@ -5,6 +5,7 @@ #include "SLARaster.hpp" #include "libslic3r/ExPolygon.hpp" +#include "libslic3r/MTUtils.hpp" #include // For rasterizing @@ -32,25 +33,30 @@ inline const ClipperLib::Paths& holes(const ClipperLib::Polygon& p) { return p.H namespace sla { +const Raster::TMirroring Raster::NoMirror = {false, false}; +const Raster::TMirroring Raster::MirrorX = {true, false}; +const Raster::TMirroring Raster::MirrorY = {false, true}; +const Raster::TMirroring Raster::MirrorXY = {true, true}; + + +using TPixelRenderer = agg::pixfmt_gray8; // agg::pixfmt_rgb24; +using TRawRenderer = agg::renderer_base; +using TPixel = TPixelRenderer::color_type; +using TRawBuffer = agg::rendering_buffer; +using TBuffer = std::vector; + +using TRendererAA = agg::renderer_scanline_aa_solid; + class Raster::Impl { public: - using TPixelRenderer = agg::pixfmt_gray8; // agg::pixfmt_rgb24; - using TRawRenderer = agg::renderer_base; - using TPixel = TPixelRenderer::color_type; - using TRawBuffer = agg::rendering_buffer; - - using TBuffer = std::vector; - - using TRendererAA = agg::renderer_scanline_aa_solid; static const TPixel ColorWhite; static const TPixel ColorBlack; - using Format = Raster::Format; + using Format = Raster::RawData; private: Raster::Resolution m_resolution; -// Raster::PixelDim m_pxdim; Raster::PixelDim m_pxdim_scaled; // used for scaled coordinate polygons TBuffer m_buf; TRawBuffer m_rbuf; @@ -59,74 +65,49 @@ private: TRendererAA m_renderer; std::function m_gammafn; - std::array m_mirror; - Format m_fmt = Format::PNG; + Trafo m_trafo; inline void flipy(agg::path_storage& path) const { - path.flip_y(0, m_resolution.height_px); + path.flip_y(0, double(m_resolution.height_px)); } inline void flipx(agg::path_storage& path) const { - path.flip_x(0, m_resolution.width_px); + path.flip_x(0, double(m_resolution.width_px)); } public: - - inline Impl(const Raster::Resolution& res, const Raster::PixelDim &pd, - const std::array& mirror, double gamma = 1.0): - m_resolution(res), -// m_pxdim(pd), - m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm), - m_buf(res.pixels()), - m_rbuf(reinterpret_cast(m_buf.data()), - res.width_px, res.height_px, - int(res.width_px*TPixelRenderer::num_components)), - m_pixfmt(m_rbuf), - m_raw_renderer(m_pixfmt), - m_renderer(m_raw_renderer), - m_mirror(mirror) + inline Impl(const Raster::Resolution & res, + const Raster::PixelDim & pd, + const Trafo &trafo) + : m_resolution(res) + , m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm) + , m_buf(res.pixels()) + , m_rbuf(reinterpret_cast(m_buf.data()), + unsigned(res.width_px), + unsigned(res.height_px), + int(res.width_px * TPixelRenderer::num_components)) + , m_pixfmt(m_rbuf) + , m_raw_renderer(m_pixfmt) + , m_renderer(m_raw_renderer) + , m_trafo(trafo) { m_renderer.color(ColorWhite); - if(gamma > 0) m_gammafn = agg::gamma_power(gamma); + if (trafo.gamma > 0) m_gammafn = agg::gamma_power(trafo.gamma); else m_gammafn = agg::gamma_threshold(0.5); clear(); } - - inline Impl(const Raster::Resolution& res, - const Raster::PixelDim &pd, - Format fmt, - double gamma = 1.0): - Impl(res, pd, {false, false}, gamma) - { - switch (fmt) { - case Format::PNG: m_mirror = {false, true}; break; - case Format::RAW: m_mirror = {false, false}; break; - } - m_fmt = fmt; - } template void draw(const P &poly) { agg::rasterizer_scanline_aa<> ras; agg::scanline_p8 scanlines; ras.gamma(m_gammafn); - - auto&& path = to_path(contour(poly)); - if(m_mirror[X]) flipx(path); - if(m_mirror[Y]) flipy(path); - - ras.add_path(path); - - for(auto& h : holes(poly)) { - auto&& holepath = to_path(h); - if(m_mirror[X]) flipx(holepath); - if(m_mirror[Y]) flipy(holepath); - ras.add_path(holepath); - } - + ras.add_path(to_path(contour(poly))); + for(auto& h : holes(poly)) ras.add_path(to_path(h)); + agg::render_scanlines(ras, scanlines, m_renderer); } @@ -135,11 +116,16 @@ public: } inline TBuffer& buffer() { return m_buf; } + inline const TBuffer& buffer() const { return m_buf; } - inline Format format() const { return m_fmt; } inline const Raster::Resolution resolution() { return m_resolution; } - + inline const Raster::PixelDim pixdim() + { + return {SCALING_FACTOR / m_pxdim_scaled.w_mm, + SCALING_FACTOR / m_pxdim_scaled.h_mm}; + } + private: inline double getPx(const Point& p) { return p(0) * m_pxdim_scaled.w_mm; @@ -162,49 +148,67 @@ private: return p.Y * m_pxdim_scaled.h_mm; } - template agg::path_storage to_path(const PointVec& poly) + template agg::path_storage _to_path(const PointVec& v) { agg::path_storage path; - auto it = poly.begin(); + auto it = v.begin(); path.move_to(getPx(*it), getPy(*it)); + while(++it != v.end()) path.line_to(getPx(*it), getPy(*it)); + path.line_to(getPx(v.front()), getPy(v.front())); + + return path; + } + + template agg::path_storage _to_path_flpxy(const PointVec& v) + { + agg::path_storage path; + + auto it = v.begin(); + path.move_to(getPy(*it), getPx(*it)); + while(++it != v.end()) path.line_to(getPy(*it), getPx(*it)); + path.line_to(getPy(v.front()), getPx(v.front())); + + return path; + } + + template agg::path_storage to_path(const PointVec &v) + { + auto path = m_trafo.flipXY ? _to_path_flpxy(v) : _to_path(v); + + path.translate_all_paths(m_trafo.origin_x * m_pxdim_scaled.w_mm, + m_trafo.origin_y * m_pxdim_scaled.h_mm); + + if(m_trafo.mirror_x) flipx(path); + if(m_trafo.mirror_y) flipy(path); - while(++it != poly.end()) - path.line_to(getPx(*it), getPy(*it)); - - path.line_to(getPx(poly.front()), getPy(poly.front())); return path; } }; -const Raster::Impl::TPixel Raster::Impl::ColorWhite = Raster::Impl::TPixel(255); -const Raster::Impl::TPixel Raster::Impl::ColorBlack = Raster::Impl::TPixel(0); +const TPixel Raster::Impl::ColorWhite = TPixel(255); +const TPixel Raster::Impl::ColorBlack = TPixel(0); + +Raster::Raster() { reset(); } + +Raster::Raster(const Raster::Resolution &r, + const Raster::PixelDim & pd, + const Raster::Trafo & tr) +{ + reset(r, pd, tr); +} -template<> Raster::Raster() { reset(); }; Raster::~Raster() = default; -// Raster::Raster(Raster &&m) = default; -// Raster& Raster::operator=(Raster&&) = default; - -// FIXME: remove after migrating to higher version of windows compiler -Raster::Raster(Raster &&m): m_impl(std::move(m.m_impl)) {} -Raster& Raster::operator=(Raster &&m) { - m_impl = std::move(m.m_impl); return *this; -} +Raster::Raster(Raster &&m) = default; +Raster &Raster::operator=(Raster &&) = default; void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd, - Format fmt, double gamma) + const Trafo &trafo) { m_impl.reset(); - m_impl.reset(new Impl(r, pd, fmt, gamma)); -} - -void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd, - const std::array& mirror, double gamma) -{ - m_impl.reset(); - m_impl.reset(new Impl(r, pd, mirror, gamma)); + m_impl.reset(new Impl(r, pd, trafo)); } void Raster::reset() @@ -214,9 +218,16 @@ void Raster::reset() Raster::Resolution Raster::resolution() const { - if(m_impl) return m_impl->resolution(); + if (m_impl) return m_impl->resolution(); + + return Resolution{0, 0}; +} - return Resolution(0, 0); +Raster::PixelDim Raster::pixel_dimensions() const +{ + if (m_impl) return m_impl->pixdim(); + + return PixelDim{0., 0.}; } void Raster::clear() @@ -227,103 +238,83 @@ void Raster::clear() void Raster::draw(const ExPolygon &expoly) { + assert(m_impl); m_impl->draw(expoly); } void Raster::draw(const ClipperLib::Polygon &poly) { + assert(m_impl); m_impl->draw(poly); } -void Raster::save(std::ostream& stream, Format fmt) +uint8_t Raster::read_pixel(size_t x, size_t y) const { - assert(m_impl); - if(!stream.good()) return; - - switch(fmt) { - case Format::PNG: { - auto& b = m_impl->buffer(); - size_t out_len = 0; - void * rawdata = tdefl_write_image_to_png_file_in_memory( - b.data(), - int(resolution().width_px), - int(resolution().height_px), 1, &out_len); - - if(rawdata == nullptr) break; - - stream.write(static_cast(rawdata), - std::streamsize(out_len)); - - MZ_FREE(rawdata); - - break; - } - case Format::RAW: { - stream << "P5 " - << m_impl->resolution().width_px << " " - << m_impl->resolution().height_px << " " - << "255 "; - - auto sz = m_impl->buffer().size()*sizeof(Impl::TBuffer::value_type); - stream.write(reinterpret_cast(m_impl->buffer().data()), - std::streamsize(sz)); - } - } + assert (m_impl); + TPixel::value_type px; + m_impl->buffer()[y * resolution().width_px + x].get(px); + return px; } -void Raster::save(std::ostream &stream) +PNGImage & PNGImage::serialize(const Raster &raster) { - save(stream, m_impl->format()); + size_t s = 0; + m_buffer.clear(); + + void *rawdata = tdefl_write_image_to_png_file_in_memory( + get_internals(raster).buffer().data(), + int(raster.resolution().width_px), + int(raster.resolution().height_px), 1, &s); + + // On error, data() will return an empty vector. No other info can be + // retrieved from miniz anyway... + if (rawdata == nullptr) return *this; + + auto ptr = static_cast(rawdata); + + m_buffer.reserve(s); + std::copy(ptr, ptr + s, std::back_inserter(m_buffer)); + + MZ_FREE(rawdata); + return *this; } -RawBytes Raster::save(Format fmt) +std::ostream &operator<<(std::ostream &stream, const Raster::RawData &bytes) { - assert(m_impl); - - std::vector data; size_t s = 0; - - switch(fmt) { - case Format::PNG: { - void *rawdata = tdefl_write_image_to_png_file_in_memory( - m_impl->buffer().data(), - int(resolution().width_px), - int(resolution().height_px), 1, &s); - - if(rawdata == nullptr) break; - auto ptr = static_cast(rawdata); - - data.reserve(s); std::copy(ptr, ptr + s, std::back_inserter(data)); - - MZ_FREE(rawdata); - break; - } - case Format::RAW: { - auto header = std::string("P5 ") + - std::to_string(m_impl->resolution().width_px) + " " + - std::to_string(m_impl->resolution().height_px) + " " + "255 "; - - auto sz = m_impl->buffer().size()*sizeof(Impl::TBuffer::value_type); - s = sz + header.size(); - - data.reserve(s); - - auto buff = reinterpret_cast(m_impl->buffer().data()); - std::copy(header.begin(), header.end(), std::back_inserter(data)); - std::copy(buff, buff+sz, std::back_inserter(data)); - - break; - } - } - - return {std::move(data)}; + stream.write(reinterpret_cast(bytes.data()), + std::streamsize(bytes.size())); + + return stream; } -RawBytes Raster::save() +Raster::RawData::~RawData() = default; + +PPMImage & PPMImage::serialize(const Raster &raster) { - return save(m_impl->format()); + auto header = std::string("P5 ") + + std::to_string(raster.resolution().width_px) + " " + + std::to_string(raster.resolution().height_px) + " " + "255 "; + + const auto &impl = get_internals(raster); + auto sz = impl.buffer().size() * sizeof(TBuffer::value_type); + size_t s = sz + header.size(); + + m_buffer.clear(); + m_buffer.reserve(s); + + auto buff = reinterpret_cast(impl.buffer().data()); + std::copy(header.begin(), header.end(), std::back_inserter(m_buffer)); + std::copy(buff, buff+sz, std::back_inserter(m_buffer)); + + return *this; } +const Raster::Impl &Raster::RawData::get_internals(const Raster &raster) +{ + return *raster.m_impl; } -} + +} // namespace sla +} // namespace Slic3r #endif // SLARASTER_CPP diff --git a/src/libslic3r/SLA/SLARaster.hpp b/src/libslic3r/SLA/SLARaster.hpp index 8b27fd153..b3d73536b 100644 --- a/src/libslic3r/SLA/SLARaster.hpp +++ b/src/libslic3r/SLA/SLARaster.hpp @@ -8,45 +8,13 @@ #include #include +#include + namespace ClipperLib { struct Polygon; } -namespace Slic3r { - -class ExPolygon; - +namespace Slic3r { namespace sla { -// Raw byte buffer paired with its size. Suitable for compressed PNG data. -class RawBytes { - - std::vector m_buffer; -public: - - RawBytes() = default; - RawBytes(std::vector&& data): m_buffer(std::move(data)) {} - - size_t size() const { return m_buffer.size(); } - const uint8_t * data() { return m_buffer.data(); } - - RawBytes(const RawBytes&) = delete; - RawBytes& operator=(const RawBytes&) = delete; - - // ///////////////////////////////////////////////////////////////////////// - // FIXME: the following is needed for MSVC2013 compatibility - // ///////////////////////////////////////////////////////////////////////// - - // RawBytes(RawBytes&&) = default; - // RawBytes& operator=(RawBytes&&) = default; - - RawBytes(RawBytes&& mv) : m_buffer(std::move(mv.m_buffer)) {} - RawBytes& operator=(RawBytes&& mv) { - m_buffer = std::move(mv.m_buffer); - return *this; - } - - // ///////////////////////////////////////////////////////////////////////// -}; - /** * @brief Raster captures an anti-aliased monochrome canvas where vectorial * polygons can be rasterized. Fill color is always white and the background is @@ -60,10 +28,28 @@ class Raster { std::unique_ptr m_impl; public: - /// Supported compression types - enum class Format { - RAW, //!> Uncompressed pixel data - PNG //!> PNG compression + // Raw byte buffer paired with its size. Suitable for compressed image data. + class RawData + { + protected: + std::vector m_buffer; + const Impl& get_internals(const Raster& raster); + public: + RawData() = default; + RawData(std::vector&& data): m_buffer(std::move(data)) {} + virtual ~RawData(); + + RawData(const RawData &) = delete; + RawData &operator=(const RawData &) = delete; + + RawData(RawData &&) = default; + RawData &operator=(RawData &&) = default; + + size_t size() const { return m_buffer.size(); } + const uint8_t * data() const { return m_buffer.data(); } + + virtual RawData& serialize(const Raster &/*raster*/) { return *this; } + virtual std::string get_file_extension() const = 0; }; /// Type that represents a resolution in pixels. @@ -86,11 +72,36 @@ public: w_mm(px_width_mm), h_mm(px_height_mm) {} }; - /// Constructor taking the resolution and the pixel dimension. - template Raster(Args...args) { - reset(std::forward(args)...); - } - + enum Orientation { roLandscape, roPortrait }; + + using TMirroring = std::array; + static const TMirroring NoMirror; + static const TMirroring MirrorX; + static const TMirroring MirrorY; + static const TMirroring MirrorXY; + + struct Trafo { + bool mirror_x = false, mirror_y = false, flipXY = false; + coord_t origin_x = 0, origin_y = 0; + + // If gamma is zero, thresholding will be performed which disables AA. + double gamma = 1.; + + // Portrait orientation will make sure the drawed polygons are rotated + // by 90 degrees. + Trafo(Orientation o = roLandscape, const TMirroring &mirror = NoMirror) + // XY flipping implicitly does an X mirror + : mirror_x(o == roPortrait ? !mirror[0] : mirror[0]) + , mirror_y(!mirror[1]) // Makes raster origin to be top left corner + , flipXY(o == roPortrait) + {} + }; + + Raster(); + Raster(const Resolution &r, + const PixelDim & pd, + const Trafo & tr = {}); + Raster(const Raster& cpy) = delete; Raster& operator=(const Raster& cpy) = delete; Raster(Raster&& m); @@ -98,18 +109,10 @@ public: ~Raster(); /// Reallocated everything for the given resolution and pixel dimension. - /// The third parameter is either the X, Y mirroring or a supported format - /// for which the correct mirroring will be configured. - void reset(const Resolution&, - const PixelDim&, - const std::array& mirror, - double gamma = 1.0); - - void reset(const Resolution& r, - const PixelDim& pd, - Format o, - double gamma = 1.0); - + void reset(const Resolution& r, + const PixelDim& pd, + const Trafo &tr = {}); + /** * Release the allocated resources. Drawing in this state ends in * unspecified behavior. @@ -118,6 +121,7 @@ public: /// Get the resolution of the raster. Resolution resolution() const; + PixelDim pixel_dimensions() const; /// Clear the raster with black color. void clear(); @@ -126,24 +130,28 @@ public: void draw(const ExPolygon& poly); void draw(const ClipperLib::Polygon& poly); - // Saving the raster: - // It is possible to override the format given in the constructor but - // be aware that the mirroring will not be modified. - - /// Save the raster on the specified stream. - void save(std::ostream& stream, Format); - void save(std::ostream& stream); + uint8_t read_pixel(size_t w, size_t h) const; + + inline bool empty() const { return ! bool(m_impl); } - /// Save into a continuous byte stream which is returned. - RawBytes save(Format fmt); - RawBytes save(); }; -// This prevents the duplicate default constructor warning on MSVC2013 -template<> Raster::Raster(); +class PNGImage: public Raster::RawData { +public: + PNGImage& serialize(const Raster &raster) override; + std::string get_file_extension() const override { return "png"; } +}; +class PPMImage: public Raster::RawData { +public: + PPMImage& serialize(const Raster &raster) override; + std::string get_file_extension() const override { return "ppm"; } +}; + +std::ostream& operator<<(std::ostream &stream, const Raster::RawData &bytes); } // sla } // Slic3r + #endif // SLARASTER_HPP diff --git a/src/libslic3r/SLA/SLARasterWriter.cpp b/src/libslic3r/SLA/SLARasterWriter.cpp index 3e6f015d4..f80ce01ab 100644 --- a/src/libslic3r/SLA/SLARasterWriter.cpp +++ b/src/libslic3r/SLA/SLARasterWriter.cpp @@ -10,7 +10,7 @@ namespace Slic3r { namespace sla { -std::string SLARasterWriter::createIniContent(const std::string& projectname) const +std::string RasterWriter::createIniContent(const std::string& projectname) const { std::string out("action = print\njobDir = "); out += projectname + "\n"; @@ -21,39 +21,14 @@ std::string SLARasterWriter::createIniContent(const std::string& projectname) co return out; } -void SLARasterWriter::flpXY(ClipperLib::Polygon &poly) -{ - for(auto& p : poly.Contour) std::swap(p.X, p.Y); - std::reverse(poly.Contour.begin(), poly.Contour.end()); - - for(auto& h : poly.Holes) { - for(auto& p : h) std::swap(p.X, p.Y); - std::reverse(h.begin(), h.end()); - } -} +RasterWriter::RasterWriter(const Raster::Resolution &res, + const Raster::PixelDim & pixdim, + const Raster::Trafo & trafo, + double gamma) + : m_res(res), m_pxdim(pixdim), m_trafo(trafo), m_gamma(gamma) +{} -void SLARasterWriter::flpXY(ExPolygon &poly) -{ - for(auto& p : poly.contour.points) p = Point(p.y(), p.x()); - std::reverse(poly.contour.points.begin(), poly.contour.points.end()); - - for(auto& h : poly.holes) { - for(auto& p : h.points) p = Point(p.y(), p.x()); - std::reverse(h.points.begin(), h.points.end()); - } -} - -SLARasterWriter::SLARasterWriter(const Raster::Resolution &res, - const Raster::PixelDim &pixdim, - const std::array &mirror, - double gamma) - : m_res(res), m_pxdim(pixdim), m_mirror(mirror), m_gamma(gamma) -{ - // PNG raster will implicitly do an Y mirror - m_mirror[1] = !m_mirror[1]; -} - -void SLARasterWriter::save(const std::string &fpath, const std::string &prjname) +void RasterWriter::save(const std::string &fpath, const std::string &prjname) { try { Zipper zipper(fpath); // zipper with no compression @@ -103,7 +78,7 @@ std::string get_cfg_value(const DynamicPrintConfig &cfg, const std::string &key) } // namespace -void SLARasterWriter::set_config(const DynamicPrintConfig &cfg) +void RasterWriter::set_config(const DynamicPrintConfig &cfg) { m_config["layerHeight"] = get_cfg_value(cfg, "layer_height"); m_config["expTime"] = get_cfg_value(cfg, "exposure_time"); @@ -114,11 +89,11 @@ void SLARasterWriter::set_config(const DynamicPrintConfig &cfg) m_config["printerProfile"] = get_cfg_value(cfg, "printer_settings_id"); m_config["printProfile"] = get_cfg_value(cfg, "sla_print_settings_id"); - m_config["fileCreationTimestamp"] = Utils::current_utc_time2str(); + m_config["fileCreationTimestamp"] = Utils::utc_timestamp(); m_config["prusaSlicerVersion"] = SLIC3R_BUILD_ID; } -void SLARasterWriter::set_statistics(const PrintStatistics &stats) +void RasterWriter::set_statistics(const PrintStatistics &stats) { m_config["usedMaterial"] = std::to_string(stats.used_material); m_config["numFade"] = std::to_string(stats.num_fade); diff --git a/src/libslic3r/SLA/SLARasterWriter.hpp b/src/libslic3r/SLA/SLARasterWriter.hpp index b9202c464..c231655d2 100644 --- a/src/libslic3r/SLA/SLARasterWriter.hpp +++ b/src/libslic3r/SLA/SLARasterWriter.hpp @@ -15,20 +15,17 @@ namespace Slic3r { namespace sla { -// Implementation for PNG raster output +// API to write the zipped sla output layers and metadata. +// Implementation uses PNG raster output. // Be aware that if a large number of layers are allocated, it can very well // exhaust the available memory especially on 32 bit platform. // This class is designed to be used in parallel mode. Layers have an ID and // each layer can be written and compressed independently (in parallel). // At the end when all layers where written, the save method can be used to // write out the result into a zipped archive. -class SLARasterWriter +class RasterWriter { public: - enum Orientation { - roLandscape, - roPortrait - }; // Used for addressing parameters of set_statistics() struct PrintStatistics @@ -45,7 +42,7 @@ private: // A struct to bind the raster image data and its compressed bytes together. struct Layer { Raster raster; - RawBytes rawbytes; + PNGImage rawbytes; Layer() = default; @@ -61,70 +58,55 @@ private: // parallel. Later we can write every layer to the disk sequentially. std::vector m_layers_rst; Raster::Resolution m_res; - Raster::PixelDim m_pxdim; - std::array m_mirror; - double m_gamma; - + Raster::PixelDim m_pxdim; + Raster::Trafo m_trafo; + double m_gamma; + std::map m_config; std::string createIniContent(const std::string& projectname) const; - - static void flpXY(ClipperLib::Polygon& poly); - static void flpXY(ExPolygon& poly); public: - SLARasterWriter(const Raster::Resolution &res, - const Raster::PixelDim &pixdim, - const std::array &mirror, - double gamma = 1.); + + // SLARasterWriter is using Raster in custom mirroring mode + RasterWriter(const Raster::Resolution &res, + const Raster::PixelDim & pixdim, + const Raster::Trafo & trafo, + double gamma = 1.); - SLARasterWriter(const SLARasterWriter& ) = delete; - SLARasterWriter& operator=(const SLARasterWriter&) = delete; - SLARasterWriter(SLARasterWriter&& m) = default; - SLARasterWriter& operator=(SLARasterWriter&&) = default; + RasterWriter(const RasterWriter& ) = delete; + RasterWriter& operator=(const RasterWriter&) = delete; + RasterWriter(RasterWriter&& m) = default; + RasterWriter& operator=(RasterWriter&&) = default; inline void layers(unsigned cnt) { if(cnt > 0) m_layers_rst.resize(cnt); } inline unsigned layers() const { return unsigned(m_layers_rst.size()); } - template void draw_polygon(const Poly& p, unsigned lyr, - Orientation o = roPortrait) + template void draw_polygon(const Poly& p, unsigned lyr) { assert(lyr < m_layers_rst.size()); - - switch (o) { - case roPortrait: { - Poly poly(p); - flpXY(poly); - m_layers_rst[lyr].raster.draw(poly); - break; - } - case roLandscape: - m_layers_rst[lyr].raster.draw(p); - break; - } + m_layers_rst[lyr].raster.draw(p); } inline void begin_layer(unsigned lyr) { if(m_layers_rst.size() <= lyr) m_layers_rst.resize(lyr+1); - m_layers_rst[lyr].raster.reset(m_res, m_pxdim, m_mirror, m_gamma); + m_layers_rst[lyr].raster.reset(m_res, m_pxdim, m_trafo); } inline void begin_layer() { m_layers_rst.emplace_back(); - m_layers_rst.front().raster.reset(m_res, m_pxdim, m_mirror, m_gamma); + m_layers_rst.front().raster.reset(m_res, m_pxdim, m_trafo); } inline void finish_layer(unsigned lyr_id) { assert(lyr_id < m_layers_rst.size()); - m_layers_rst[lyr_id].rawbytes = - m_layers_rst[lyr_id].raster.save(Raster::Format::PNG); + m_layers_rst[lyr_id].rawbytes.serialize(m_layers_rst[lyr_id].raster); m_layers_rst[lyr_id].raster.reset(); } inline void finish_layer() { if(!m_layers_rst.empty()) { - m_layers_rst.back().rawbytes = - m_layers_rst.back().raster.save(Raster::Format::PNG); + m_layers_rst.back().rawbytes.serialize(m_layers_rst.back().raster); m_layers_rst.back().raster.reset(); } } diff --git a/src/libslic3r/SLA/SLASpatIndex.hpp b/src/libslic3r/SLA/SLASpatIndex.hpp index 90dcdc362..20b6fcd58 100644 --- a/src/libslic3r/SLA/SLASpatIndex.hpp +++ b/src/libslic3r/SLA/SLASpatIndex.hpp @@ -39,14 +39,19 @@ public: insert(std::make_pair(v, unsigned(idx))); } - std::vector query(std::function); - std::vector nearest(const Vec3d&, unsigned k); + std::vector query(std::function) const; + std::vector nearest(const Vec3d&, unsigned k) const; + std::vector query(const Vec3d &v, unsigned k) const // wrapper + { + return nearest(v, k); + } // For testing size_t size() const; bool empty() const { return size() == 0; } void foreach(std::function fn); + void foreach(std::function fn) const; }; using BoxIndexEl = std::pair; diff --git a/src/libslic3r/SLA/SLASupportTree.cpp b/src/libslic3r/SLA/SLASupportTree.cpp index 99f7bc8b3..fea8bf731 100644 --- a/src/libslic3r/SLA/SLASupportTree.cpp +++ b/src/libslic3r/SLA/SLASupportTree.cpp @@ -7,7 +7,7 @@ #include "SLASupportTree.hpp" #include "SLABoilerPlate.hpp" #include "SLASpatIndex.hpp" -#include "SLABasePool.hpp" +#include "SLASupportTreeBuilder.hpp" #include #include @@ -17,51 +17,14 @@ #include #include #include +#include +#include #include //! macro used to mark string used at localization, //! return same string #define L(s) Slic3r::I18N::translate(s) -/** - * Terminology: - * - * Support point: - * The point on the model surface that needs support. - * - * Pillar: - * A thick column that spans from a support point to the ground and has - * a thick cone shaped base where it touches the ground. - * - * Ground facing support point: - * A support point that can be directly connected with the ground with a pillar - * that does not collide or cut through the model. - * - * Non ground facing support point: - * A support point that cannot be directly connected with the ground (only with - * the model surface). - * - * Head: - * The pinhead that connects to the model surface with the sharp end end - * to a pillar or bridge stick with the dull end. - * - * Headless support point: - * A support point on the model surface for which there is not enough place for - * the head. It is either in a hole or there is some barrier that would collide - * with the head geometry. The headless support point can be ground facing and - * non ground facing as well. - * - * Bridge: - * A stick that connects two pillars or a head with a pillar. - * - * Junction: - * A small ball in the intersection of two or more sticks (pillar, bridge, ...) - * - * CompactBridge: - * A bridge that connects a headless support point with the model surface or a - * nearby pillar. - */ - namespace Slic3r { namespace sla { @@ -80,2527 +43,16 @@ const unsigned SupportConfig::optimizer_max_iterations = 1000; const unsigned SupportConfig::pillar_cascade_neighbors = 3; const unsigned SupportConfig::max_bridges_on_pillar = 3; -using Coordf = double; -using Portion = std::tuple; - -// Set this to true to enable full parallelism in this module. -// Only the well tested parts will be concurrent if this is set to false. -const constexpr bool USE_FULL_CONCURRENCY = false; - -template struct _ccr {}; - -template<> struct _ccr -{ - using Mutex = SpinMutex; - - template - static inline void enumerate(It from, It to, Fn fn) - { - using TN = size_t; - auto iN = to - from; - TN N = iN < 0 ? 0 : TN(iN); - - tbb::parallel_for(TN(0), N, [from, fn](TN n) { fn(*(from + n), n); }); - } -}; - -template<> struct _ccr -{ - struct Mutex { inline void lock() {} inline void unlock() {} }; - - template - static inline void enumerate(It from, It to, Fn fn) - { - for (auto it = from; it != to; ++it) fn(*it, it - from); - } -}; - -using ccr = _ccr; -using ccr_seq = _ccr; -using ccr_par = _ccr; - -inline Portion make_portion(double a, double b) { - return std::make_tuple(a, b); +void SupportTree::retrieve_full_mesh(TriangleMesh &outmesh) const { + outmesh.merge(retrieve_mesh(MeshType::Support)); + outmesh.merge(retrieve_mesh(MeshType::Pad)); } -template double distance(const Vec& p) { - return std::sqrt(p.transpose() * p); -} - -template double distance(const Vec& pp1, const Vec& pp2) { - auto p = pp2 - pp1; - return distance(p); -} - -Contour3D sphere(double rho, Portion portion = make_portion(0.0, 2.0*PI), - double fa=(2*PI/360)) { - - Contour3D ret; - - // prohibit close to zero radius - if(rho <= 1e-6 && rho >= -1e-6) return ret; - - auto& vertices = ret.points; - auto& facets = ret.indices; - - // Algorithm: - // Add points one-by-one to the sphere grid and form facets using relative - // coordinates. Sphere is composed effectively of a mesh of stacked circles. - - // adjust via rounding to get an even multiple for any provided angle. - double angle = (2*PI / floor(2*PI / fa)); - - // Ring to be scaled to generate the steps of the sphere - std::vector ring; - - for (double i = 0; i < 2*PI; i+=angle) ring.emplace_back(i); - - const auto sbegin = size_t(2*std::get<0>(portion)/angle); - const auto send = size_t(2*std::get<1>(portion)/angle); - - const size_t steps = ring.size(); - const double increment = 1.0 / double(steps); - - // special case: first ring connects to 0,0,0 - // insert and form facets. - if(sbegin == 0) - vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*sbegin*2.0*rho)); - - auto id = coord_t(vertices.size()); - for (size_t i = 0; i < ring.size(); i++) { - // Fixed scaling - const double z = -rho + increment*rho*2.0 * (sbegin + 1.0); - // radius of the circle for this step. - const double r = std::sqrt(std::abs(rho*rho - z*z)); - Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); - vertices.emplace_back(Vec3d(b(0), b(1), z)); - - if(sbegin == 0) - facets.emplace_back((i == 0) ? Vec3crd(coord_t(ring.size()), 0, 1) : - Vec3crd(id - 1, 0, id)); - ++ id; - } - - // General case: insert and form facets for each step, - // joining it to the ring below it. - for (size_t s = sbegin + 2; s < send - 1; s++) { - const double z = -rho + increment*double(s*2.0*rho); - const double r = std::sqrt(std::abs(rho*rho - z*z)); - - for (size_t i = 0; i < ring.size(); i++) { - Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); - vertices.emplace_back(Vec3d(b(0), b(1), z)); - auto id_ringsize = coord_t(id - int(ring.size())); - if (i == 0) { - // wrap around - facets.emplace_back(Vec3crd(id - 1, id, - id + coord_t(ring.size() - 1))); - facets.emplace_back(Vec3crd(id - 1, id_ringsize, id)); - } else { - facets.emplace_back(Vec3crd(id_ringsize - 1, id_ringsize, id)); - facets.emplace_back(Vec3crd(id - 1, id_ringsize - 1, id)); - } - id++; - } - } - - // special case: last ring connects to 0,0,rho*2.0 - // only form facets. - if(send >= size_t(2*PI / angle)) { - vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*send*2.0*rho)); - for (size_t i = 0; i < ring.size(); i++) { - auto id_ringsize = coord_t(id - int(ring.size())); - if (i == 0) { - // third vertex is on the other side of the ring. - facets.emplace_back(Vec3crd(id - 1, id_ringsize, id)); - } else { - auto ci = coord_t(id_ringsize + coord_t(i)); - facets.emplace_back(Vec3crd(ci - 1, ci, id)); - } - } - } - id++; - - return ret; -} - -// Down facing cylinder in Z direction with arguments: -// r: radius -// h: Height -// ssteps: how many edges will create the base circle -// sp: starting point -Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d sp = {0,0,0}) -{ - Contour3D ret; - - auto steps = int(ssteps); - auto& points = ret.points; - auto& indices = ret.indices; - points.reserve(2*ssteps); - double a = 2*PI/steps; - - Vec3d jp = sp; - Vec3d endp = {sp(X), sp(Y), sp(Z) + h}; - - // Upper circle points - for(int i = 0; i < steps; ++i) { - double phi = i*a; - double ex = endp(X) + r*std::cos(phi); - double ey = endp(Y) + r*std::sin(phi); - points.emplace_back(ex, ey, endp(Z)); - } - - // Lower circle points - for(int i = 0; i < steps; ++i) { - double phi = i*a; - double x = jp(X) + r*std::cos(phi); - double y = jp(Y) + r*std::sin(phi); - points.emplace_back(x, y, jp(Z)); - } - - // Now create long triangles connecting upper and lower circles - indices.reserve(2*ssteps); - auto offs = steps; - for(int i = 0; i < steps - 1; ++i) { - indices.emplace_back(i, i + offs, offs + i + 1); - indices.emplace_back(i, offs + i + 1, i + 1); - } - - // Last triangle connecting the first and last vertices - auto last = steps - 1; - indices.emplace_back(0, last, offs); - indices.emplace_back(last, offs + last, offs); - - // According to the slicing algorithms, we need to aid them with generating - // a watertight body. So we create a triangle fan for the upper and lower - // ending of the cylinder to close the geometry. - points.emplace_back(jp); int ci = int(points.size() - 1); - for(int i = 0; i < steps - 1; ++i) - indices.emplace_back(i + offs + 1, i + offs, ci); - - indices.emplace_back(offs, steps + offs - 1, ci); - - points.emplace_back(endp); ci = int(points.size() - 1); - for(int i = 0; i < steps - 1; ++i) - indices.emplace_back(ci, i, i + 1); - - indices.emplace_back(steps - 1, 0, ci); - - return ret; -} - -struct Head { - Contour3D mesh; - - size_t steps = 45; - Vec3d dir = {0, 0, -1}; - Vec3d tr = {0, 0, 0}; - - double r_back_mm = 1; - double r_pin_mm = 0.5; - double width_mm = 2; - double penetration_mm = 0.5; - - // For identification purposes. This will be used as the index into the - // container holding the head structures. See SLASupportTree::Impl - long id = -1; - - // If there is a pillar connecting to this head, then the id will be set. - long pillar_id = -1; - - inline void invalidate() { id = -1; } - inline bool is_valid() const { return id >= 0; } - - Head(double r_big_mm, - double r_small_mm, - double length_mm, - double penetration, - Vec3d direction = {0, 0, -1}, // direction (normal to the dull end ) - Vec3d offset = {0, 0, 0}, // displacement - const size_t circlesteps = 45): - steps(circlesteps), dir(direction), tr(offset), - r_back_mm(r_big_mm), r_pin_mm(r_small_mm), width_mm(length_mm), - penetration_mm(penetration) - { - - // We create two spheres which will be connected with a robe that fits - // both circles perfectly. - - // Set up the model detail level - const double detail = 2*PI/steps; - - // We don't generate whole circles. Instead, we generate only the - // portions which are visible (not covered by the robe) To know the - // exact portion of the bottom and top circles we need to use some - // rules of tangent circles from which we can derive (using simple - // triangles the following relations: - - // The height of the whole mesh - const double h = r_big_mm + r_small_mm + width_mm; - double phi = PI/2 - std::acos( (r_big_mm - r_small_mm) / h ); - - // To generate a whole circle we would pass a portion of (0, Pi) - // To generate only a half horizontal circle we can pass (0, Pi/2) - // The calculated phi is an offset to the half circles needed to smooth - // the transition from the circle to the robe geometry - - auto&& s1 = sphere(r_big_mm, make_portion(0, PI/2 + phi), detail); - auto&& s2 = sphere(r_small_mm, make_portion(PI/2 + phi, PI), detail); - - for(auto& p : s2.points) z(p) += h; - - mesh.merge(s1); - mesh.merge(s2); - - for(size_t idx1 = s1.points.size() - steps, idx2 = s1.points.size(); - idx1 < s1.points.size() - 1; - idx1++, idx2++) - { - coord_t i1s1 = coord_t(idx1), i1s2 = coord_t(idx2); - coord_t i2s1 = i1s1 + 1, i2s2 = i1s2 + 1; - - mesh.indices.emplace_back(i1s1, i2s1, i2s2); - mesh.indices.emplace_back(i1s1, i2s2, i1s2); - } - - auto i1s1 = coord_t(s1.points.size()) - coord_t(steps); - auto i2s1 = coord_t(s1.points.size()) - 1; - auto i1s2 = coord_t(s1.points.size()); - auto i2s2 = coord_t(s1.points.size()) + coord_t(steps) - 1; - - mesh.indices.emplace_back(i2s2, i2s1, i1s1); - mesh.indices.emplace_back(i1s2, i2s2, i1s1); - - // To simplify further processing, we translate the mesh so that the - // last vertex of the pointing sphere (the pinpoint) will be at (0,0,0) - for(auto& p : mesh.points) z(p) -= (h + r_small_mm - penetration_mm); - } - - void transform() - { - using Quaternion = Eigen::Quaternion; - - // We rotate the head to the specified direction The head's pointing - // side is facing upwards so this means that it would hold a support - // point with a normal pointing straight down. This is the reason of - // the -1 z coordinate - auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, -1}, dir); - - for(auto& p : mesh.points) p = quatern * p + tr; - } - - double fullwidth() const { - return 2 * r_pin_mm + width_mm + 2*r_back_mm - penetration_mm; - } - - static double fullwidth(const SupportConfig& cfg) { - return 2 * cfg.head_front_radius_mm + cfg.head_width_mm + - 2 * cfg.head_back_radius_mm - cfg.head_penetration_mm; - } - - Vec3d junction_point() const { - return tr + ( 2 * r_pin_mm + width_mm + r_back_mm - penetration_mm)*dir; - } - - double request_pillar_radius(double radius) const { - const double rmax = r_back_mm; - return radius > 0 && radius < rmax ? radius : rmax; - } -}; - -struct Junction { - Contour3D mesh; - double r = 1; - size_t steps = 45; - Vec3d pos; - - long id = -1; - - Junction(const Vec3d& tr, double r_mm, size_t stepnum = 45): - r(r_mm), steps(stepnum), pos(tr) - { - mesh = sphere(r_mm, make_portion(0, PI), 2*PI/steps); - for(auto& p : mesh.points) p += tr; - } -}; - -struct Pillar { - Contour3D mesh; - Contour3D base; - double r = 1; - size_t steps = 0; - Vec3d endpt; - double height = 0; - - long id = -1; - - // If the pillar connects to a head, this is the id of that head - bool starts_from_head = true; // Could start from a junction as well - long start_junction_id = -1; - - // How many bridges are connected to this pillar - unsigned bridges = 0; - - // How many pillars are cascaded with this one - unsigned links = 0; - - Pillar(const Vec3d& jp, const Vec3d& endp, - double radius = 1, size_t st = 45): - r(radius), steps(st), endpt(endp), starts_from_head(false) - { - assert(steps > 0); - - height = jp(Z) - endp(Z); - if(height > EPSILON) { // Endpoint is below the starting point - - // We just create a bridge geometry with the pillar parameters and - // move the data. - Contour3D body = cylinder(radius, height, st, endp); - mesh.points.swap(body.points); - mesh.indices.swap(body.indices); - } - } - - Pillar(const Junction& junc, const Vec3d& endp): - Pillar(junc.pos, endp, junc.r, junc.steps){} - - Pillar(const Head& head, const Vec3d& endp, double radius = 1): - Pillar(head.junction_point(), endp, head.request_pillar_radius(radius), - head.steps) - { - } - - inline Vec3d startpoint() const { - return {endpt(X), endpt(Y), endpt(Z) + height}; - } - - inline const Vec3d& endpoint() const { return endpt; } - - Pillar& add_base(double baseheight = 3, double radius = 2) { - if(baseheight <= 0) return *this; - if(baseheight > height) baseheight = height; - - assert(steps >= 0); - auto last = int(steps - 1); - - if(radius < r ) radius = r; - - double a = 2*PI/steps; - double z = endpt(Z) + baseheight; - - for(size_t i = 0; i < steps; ++i) { - double phi = i*a; - double x = endpt(X) + r*std::cos(phi); - double y = endpt(Y) + r*std::sin(phi); - base.points.emplace_back(x, y, z); - } - - for(size_t i = 0; i < steps; ++i) { - double phi = i*a; - double x = endpt(X) + radius*std::cos(phi); - double y = endpt(Y) + radius*std::sin(phi); - base.points.emplace_back(x, y, z - baseheight); - } - - auto ep = endpt; ep(Z) += baseheight; - base.points.emplace_back(endpt); - base.points.emplace_back(ep); - - auto& indices = base.indices; - auto hcenter = int(base.points.size() - 1); - auto lcenter = int(base.points.size() - 2); - auto offs = int(steps); - for(int i = 0; i < last; ++i) { - indices.emplace_back(i, i + offs, offs + i + 1); - indices.emplace_back(i, offs + i + 1, i + 1); - indices.emplace_back(i, i + 1, hcenter); - indices.emplace_back(lcenter, offs + i + 1, offs + i); - } - - indices.emplace_back(0, last, offs); - indices.emplace_back(last, offs + last, offs); - indices.emplace_back(hcenter, last, 0); - indices.emplace_back(offs, offs + last, lcenter); - return *this; - } - - bool has_base() const { return !base.points.empty(); } -}; - -// A Bridge between two pillars (with junction endpoints) -struct Bridge { - Contour3D mesh; - double r = 0.8; - - long id = -1; - long start_jid = -1; - long end_jid = -1; - - // We should reduce the radius a tiny bit to help the convex hull algorithm - Bridge(const Vec3d& j1, const Vec3d& j2, - double r_mm = 0.8, size_t steps = 45): - r(r_mm) - { - using Quaternion = Eigen::Quaternion; - Vec3d dir = (j2 - j1).normalized(); - double d = distance(j2, j1); - - mesh = cylinder(r, d, steps); - - auto quater = Quaternion::FromTwoVectors(Vec3d{0,0,1}, dir); - for(auto& p : mesh.points) p = quater * p + j1; - } - - Bridge(const Junction& j1, const Junction& j2, double r_mm = 0.8): - Bridge(j1.pos, j2.pos, r_mm, j1.steps) {} - -}; - -// A bridge that spans from model surface to model surface with small connecting -// edges on the endpoints. Used for headless support points. -struct CompactBridge { - Contour3D mesh; - long id = -1; - - CompactBridge(const Vec3d& sp, - const Vec3d& ep, - const Vec3d& n, - double r, - bool endball = true, - size_t steps = 45) - { - Vec3d startp = sp + r * n; - Vec3d dir = (ep - startp).normalized(); - Vec3d endp = ep - r * dir; - - Bridge br(startp, endp, r, steps); - mesh.merge(br.mesh); - - // now add the pins - double fa = 2*PI/steps; - auto upperball = sphere(r, Portion{PI / 2 - fa, PI}, fa); - for(auto& p : upperball.points) p += startp; - - if(endball) { - auto lowerball = sphere(r, Portion{0, PI/2 + 2*fa}, fa); - for(auto& p : lowerball.points) p += endp; - mesh.merge(lowerball); - } - - mesh.merge(upperball); - } -}; - -// A wrapper struct around the base pool (pad) -struct Pad { - TriangleMesh tmesh; - PoolConfig cfg; - double zlevel = 0; - - Pad() = default; - - Pad(const TriangleMesh& support_mesh, - const ExPolygons& modelbase, - double ground_level, - const PoolConfig& pcfg) : - cfg(pcfg), - zlevel(ground_level + - sla::get_pad_fullheight(pcfg) - - sla::get_pad_elevation(pcfg)) - { - Polygons basep; - auto &thr = cfg.throw_on_cancel; - - thr(); - - // Get a sample for the pad from the support mesh - { - ExPolygons platetmp; - - float zstart = float(zlevel); - float zend = zstart + float(get_pad_fullheight(pcfg) + EPSILON); - - base_plate(support_mesh, platetmp, grid(zstart, zend, 0.1f), thr); - - // We don't need no... holes control... - for (const ExPolygon &bp : platetmp) - basep.emplace_back(std::move(bp.contour)); - } - - if(pcfg.embed_object) { - - // If the zero elevation mode is ON, we need to process the model - // base silhouette. Create the offsetted version and punch the - // breaksticks across its perimeter. - - ExPolygons modelbase_offs = modelbase; - - if (pcfg.embed_object.object_gap_mm > 0.0) - modelbase_offs - = offset_ex(modelbase_offs, - float(scaled(pcfg.embed_object.object_gap_mm))); - - // Create a spatial index of the support silhouette polygons. - // This will be used to check for intersections with the model - // silhouette polygons. If there is no intersection, then a certain - // part of the pad is redundant as it does not host any supports. - BoxIndex bindex; - { - unsigned idx = 0; - for(auto &bp : basep) { - auto bb = bp.bounding_box(); - bb.offset(float(scaled(pcfg.min_wall_thickness_mm))); - bindex.insert(bb, idx++); - } - } - - ExPolygons concaveh = offset_ex( - concave_hull(basep, pcfg.max_merge_distance_mm, thr), - scaled(pcfg.min_wall_thickness_mm)); - - // Punching the breaksticks across the offsetted polygon perimeters - auto pad_stickholes = reserve_vector(modelbase.size()); - for(auto& poly : modelbase_offs) { - - bool overlap = false; - for (const ExPolygon &p : concaveh) - overlap = overlap || poly.overlaps(p); - - auto bb = poly.contour.bounding_box(); - bb.offset(scaled(pcfg.min_wall_thickness_mm)); - - std::vector qres = - bindex.query(bb, BoxIndex::qtIntersects); - - if (!qres.empty() || overlap) { - - // The model silhouette polygon 'poly' HAS an intersection - // with the support silhouettes. Include this polygon - // in the pad holes with the breaksticks and merge the - // original (offsetted) version with the rest of the pad - // base plate. - - basep.emplace_back(poly.contour); - - // The holes of 'poly' will become positive parts of the - // pad, so they has to be checked for intersections as well - // and erased if there is no intersection with the supports - auto it = poly.holes.begin(); - while(it != poly.holes.end()) { - if (bindex.query(it->bounding_box(), - BoxIndex::qtIntersects).empty()) - it = poly.holes.erase(it); - else - ++it; - } - - // Punch the breaksticks - sla::breakstick_holes( - poly, - pcfg.embed_object.object_gap_mm, // padding - pcfg.embed_object.stick_stride_mm, - pcfg.embed_object.stick_width_mm, - pcfg.embed_object.stick_penetration_mm); - - pad_stickholes.emplace_back(poly); - } - } - - create_base_pool(basep, tmesh, pad_stickholes, cfg); - } else { - for (const ExPolygon &bp : modelbase) basep.emplace_back(bp.contour); - create_base_pool(basep, tmesh, {}, cfg); - } - - tmesh.translate(0, 0, float(zlevel)); - if (!tmesh.empty()) tmesh.require_shared_vertices(); - } - - bool empty() const { return tmesh.facets_count() == 0; } -}; - -// The minimum distance for two support points to remain valid. -static const double /*constexpr*/ D_SP = 0.1; - -enum { // For indexing Eigen vectors as v(X), v(Y), v(Z) instead of numbers - X, Y, Z -}; - -// Calculate the normals for the selected points (from 'points' set) on the -// mesh. This will call squared distance for each point. -PointSet normals(const PointSet& points, - const EigenMesh3D& mesh, - double eps = 0.05, // min distance from edges - std::function throw_on_cancel = [](){}, - const std::vector& selected_points = {}); - -inline Vec2d to_vec2(const Vec3d& v3) { - return {v3(X), v3(Y)}; -} - -bool operator==(const PointIndexEl& e1, const PointIndexEl& e2) { - return e1.second == e2.second; -} - -// Clustering a set of points by the given distance. -ClusteredPoints cluster(const std::vector& indices, - std::function pointfn, - double dist, - unsigned max_points); - -ClusteredPoints cluster(const PointSet& points, - double dist, - unsigned max_points); - -ClusteredPoints cluster( - const std::vector& indices, - std::function pointfn, - std::function predicate, - unsigned max_points); - -// This class will hold the support tree meshes with some additional bookkeeping -// as well. Various parts of the support geometry are stored separately and are -// merged when the caller queries the merged mesh. The merged result is cached -// for fast subsequent delivery of the merged mesh which can be quite complex. -// An object of this class will be used as the result type during the support -// generation algorithm. Parts will be added with the appropriate methods such -// as add_head or add_pillar which forwards the constructor arguments and fills -// the IDs of these substructures. The IDs are basically indices into the arrays -// of the appropriate type (heads, pillars, etc...). One can later query e.g. a -// pillar for a specific head... -// -// The support pad is considered an auxiliary geometry and is not part of the -// merged mesh. It can be retrieved using a dedicated method (pad()) -class SLASupportTree::Impl { - // For heads it is beneficial to use the same IDs as for the support points. - std::vector m_heads; - std::vector m_head_indices; - - std::vector m_pillars; - std::vector m_junctions; - std::vector m_bridges; - std::vector m_compact_bridges; - Controller m_ctl; - - Pad m_pad; - - using Mutex = ccr::Mutex; - - mutable Mutex m_mutex; - mutable TriangleMesh meshcache; mutable bool meshcache_valid = false; - mutable double model_height = 0; // the full height of the model - -public: - double ground_level = 0; - - Impl() = default; - inline Impl(const Controller& ctl): m_ctl(ctl) {} - - const Controller& ctl() const { return m_ctl; } - - template Head& add_head(unsigned id, Args&&... args) - { - std::lock_guard lk(m_mutex); - m_heads.emplace_back(std::forward(args)...); - m_heads.back().id = id; - - if (id >= m_head_indices.size()) m_head_indices.resize(id + 1); - m_head_indices[id] = m_heads.size() - 1; - - meshcache_valid = false; - return m_heads.back(); - } - - template Pillar& add_pillar(unsigned headid, Args&&... args) - { - std::lock_guard lk(m_mutex); - - assert(headid < m_head_indices.size()); - Head &head = m_heads[m_head_indices[headid]]; - - m_pillars.emplace_back(head, std::forward(args)...); - Pillar& pillar = m_pillars.back(); - pillar.id = long(m_pillars.size() - 1); - head.pillar_id = pillar.id; - pillar.start_junction_id = head.id; - pillar.starts_from_head = true; - - meshcache_valid = false; - return m_pillars.back(); - } - - void increment_bridges(const Pillar& pillar) - { - std::lock_guard lk(m_mutex); - assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()); - - if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()) - m_pillars[size_t(pillar.id)].bridges++; - } - - void increment_links(const Pillar& pillar) - { - std::lock_guard lk(m_mutex); - assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()); - - if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()) - m_pillars[size_t(pillar.id)].links++; - } - - template Pillar& add_pillar(Args&&...args) - { - std::lock_guard lk(m_mutex); - m_pillars.emplace_back(std::forward(args)...); - Pillar& pillar = m_pillars.back(); - pillar.id = long(m_pillars.size() - 1); - pillar.starts_from_head = false; - meshcache_valid = false; - return m_pillars.back(); - } - - const Head& pillar_head(long pillar_id) const - { - std::lock_guard lk(m_mutex); - assert(pillar_id >= 0 && pillar_id < long(m_pillars.size())); - - const Pillar& p = m_pillars[size_t(pillar_id)]; - assert(p.starts_from_head && p.start_junction_id >= 0); - assert(size_t(p.start_junction_id) < m_head_indices.size()); - - return m_heads[m_head_indices[p.start_junction_id]]; - } - - const Pillar& head_pillar(unsigned headid) const - { - std::lock_guard lk(m_mutex); - assert(headid < m_head_indices.size()); - - const Head& h = m_heads[m_head_indices[headid]]; - assert(h.pillar_id >= 0 && h.pillar_id < long(m_pillars.size())); - - return m_pillars[size_t(h.pillar_id)]; - } - - template const Junction& add_junction(Args&&... args) - { - std::lock_guard lk(m_mutex); - m_junctions.emplace_back(std::forward(args)...); - m_junctions.back().id = long(m_junctions.size() - 1); - meshcache_valid = false; - return m_junctions.back(); - } - - template const Bridge& add_bridge(Args&&... args) - { - std::lock_guard lk(m_mutex); - m_bridges.emplace_back(std::forward(args)...); - m_bridges.back().id = long(m_bridges.size() - 1); - meshcache_valid = false; - return m_bridges.back(); - } - - template const CompactBridge& add_compact_bridge(Args&&...args) - { - std::lock_guard lk(m_mutex); - m_compact_bridges.emplace_back(std::forward(args)...); - m_compact_bridges.back().id = long(m_compact_bridges.size() - 1); - meshcache_valid = false; - return m_compact_bridges.back(); - } - - Head &head(unsigned id) - { - std::lock_guard lk(m_mutex); - assert(id < m_head_indices.size()); - - meshcache_valid = false; - return m_heads[m_head_indices[id]]; - } - - inline size_t pillarcount() const { - std::lock_guard lk(m_mutex); - return m_pillars.size(); - } - - template inline IntegerOnly pillar(T id) const - { - std::lock_guard lk(m_mutex); - assert(id >= 0 && size_t(id) < m_pillars.size() && - size_t(id) < std::numeric_limits::max()); - - return m_pillars[size_t(id)]; - } - - const Pad &create_pad(const TriangleMesh &object_supports, - const ExPolygons & modelbase, - const PoolConfig & cfg) - { - m_pad = Pad(object_supports, modelbase, ground_level, cfg); - return m_pad; - } - - void remove_pad() { m_pad = Pad(); } - - const Pad& pad() const { return m_pad; } - - // WITHOUT THE PAD!!! - const TriangleMesh &merged_mesh() const - { - if (meshcache_valid) return meshcache; - - Contour3D merged; - - for (auto &head : m_heads) { - if (m_ctl.stopcondition()) break; - if (head.is_valid()) merged.merge(head.mesh); - } - - for (auto &stick : m_pillars) { - if (m_ctl.stopcondition()) break; - merged.merge(stick.mesh); - merged.merge(stick.base); - } - - for (auto &j : m_junctions) { - if (m_ctl.stopcondition()) break; - merged.merge(j.mesh); - } - - for (auto &cb : m_compact_bridges) { - if (m_ctl.stopcondition()) break; - merged.merge(cb.mesh); - } - - for (auto &bs : m_bridges) { - if (m_ctl.stopcondition()) break; - merged.merge(bs.mesh); - } - - if (m_ctl.stopcondition()) { - // In case of failure we have to return an empty mesh - meshcache = TriangleMesh(); - return meshcache; - } - - meshcache = mesh(merged); - - // The mesh will be passed by const-pointer to TriangleMeshSlicer, - // which will need this. - if (!meshcache.empty()) meshcache.require_shared_vertices(); - - BoundingBoxf3 &&bb = meshcache.bounding_box(); - model_height = bb.max(Z) - bb.min(Z); - - meshcache_valid = true; - return meshcache; - } - - // WITH THE PAD - double full_height() const - { - if (merged_mesh().empty() && !pad().empty()) - return get_pad_fullheight(pad().cfg); - - double h = mesh_height(); - if (!pad().empty()) h += sla::get_pad_elevation(pad().cfg); - return h; - } - - // WITHOUT THE PAD!!! - double mesh_height() const - { - if (!meshcache_valid) merged_mesh(); - return model_height; - } - - // Intended to be called after the generation is fully complete - void merge_and_cleanup() - { - merged_mesh(); // in case the mesh is not generated, it should be... - - // Doing clear() does not garantee to release the memory. - m_heads = {}; - m_head_indices = {}; - m_pillars = {}; - m_junctions = {}; - m_bridges = {}; - m_compact_bridges = {}; - } -}; - -// This function returns the position of the centroid in the input 'clust' -// vector of point indices. -template -long cluster_centroid(const ClusterEl& clust, - std::function pointfn, - DistFn df) -{ - switch(clust.size()) { - case 0: /* empty cluster */ return -1; - case 1: /* only one element */ return 0; - case 2: /* if two elements, there is no center */ return 0; - default: ; - } - - // The function works by calculating for each point the average distance - // from all the other points in the cluster. We create a selector bitmask of - // the same size as the cluster. The bitmask will have two true bits and - // false bits for the rest of items and we will loop through all the - // permutations of the bitmask (combinations of two points). Get the - // distance for the two points and add the distance to the averages. - // The point with the smallest average than wins. - - // The complexity should be O(n^2) but we will mostly apply this function - // for small clusters only (cca 3 elements) - - std::vector sel(clust.size(), false); // create full zero bitmask - std::fill(sel.end() - 2, sel.end(), true); // insert the two ones - std::vector avgs(clust.size(), 0.0); // store the average distances - - do { - std::array idx; - for(size_t i = 0, j = 0; i < clust.size(); i++) if(sel[i]) idx[j++] = i; - - double d = df(pointfn(clust[idx[0]]), - pointfn(clust[idx[1]])); - - // add the distance to the sums for both associated points - for(auto i : idx) avgs[i] += d; - - // now continue with the next permutation of the bitmask with two 1s - } while(std::next_permutation(sel.begin(), sel.end())); - - // Divide by point size in the cluster to get the average (may be redundant) - for(auto& a : avgs) a /= clust.size(); - - // get the lowest average distance and return the index - auto minit = std::min_element(avgs.begin(), avgs.end()); - return long(minit - avgs.begin()); -} - -inline Vec3d dirv(const Vec3d& startp, const Vec3d& endp) { - return (endp - startp).normalized(); -} - -class SLASupportTree::Algorithm { - const SupportConfig& m_cfg; - const EigenMesh3D& m_mesh; - const std::vector& m_support_pts; - - using PtIndices = std::vector; - - PtIndices m_iheads; // support points with pinhead - PtIndices m_iheadless; // headless support points - - // supp. pts. connecting to model: point index and the ray hit data - std::vector> m_iheads_onmodel; - - // normals for support points from model faces. - PointSet m_support_nmls; - - // Clusters of points which can reach the ground directly and can be - // bridged to one central pillar - std::vector m_pillar_clusters; - - // This algorithm uses the Impl class as its output stream. It will be - // filled gradually with support elements (heads, pillars, bridges, ...) - using Result = SLASupportTree::Impl; - - Result& m_result; - - // support points in Eigen/IGL format - PointSet m_points; - - // throw if canceled: It will be called many times so a shorthand will - // come in handy. - ThrowOnCancel m_thr; - - // A spatial index to easily find strong pillars to connect to. - PointIndex m_pillar_index; - - inline double ray_mesh_intersect(const Vec3d& s, - const Vec3d& dir) - { - return m_mesh.query_ray_hit(s, dir).distance(); - } - - // This function will test if a future pinhead would not collide with the - // model geometry. It does not take a 'Head' object because those are - // created after this test. Parameters: s: The touching point on the model - // surface. dir: This is the direction of the head from the pin to the back - // r_pin, r_back: the radiuses of the pin and the back sphere width: This - // is the full width from the pin center to the back center m: The object - // mesh. - // The return value is the hit result from the ray casting. If the starting - // point was inside the model, an "invalid" hit_result will be returned - // with a zero distance value instead of a NAN. This way the result can - // be used safely for comparison with other distances. - EigenMesh3D::hit_result pinhead_mesh_intersect( - const Vec3d& s, - const Vec3d& dir, - double r_pin, - double r_back, - double width) - { - static const size_t SAMPLES = 8; - - // method based on: - // https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space - - // We will shoot multiple rays from the head pinpoint in the direction - // of the pinhead robe (side) surface. The result will be the smallest - // hit distance. - - // Move away slightly from the touching point to avoid raycasting on the - // inner surface of the mesh. - Vec3d v = dir; // Our direction (axis) - Vec3d c = s + width * dir; - const double& sd = m_cfg.safety_distance_mm; - - // Two vectors that will be perpendicular to each other and to the - // axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a - // placeholder. - Vec3d a(0, 1, 0), b; - - // The portions of the circle (the head-back circle) for which we will - // shoot rays. - std::array phis; - for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size(); - - auto& m = m_mesh; - using HitResult = EigenMesh3D::hit_result; - - // Hit results - std::array hits; - - // We have to address the case when the direction vector v (same as - // dir) is coincident with one of the world axes. In this case two of - // its components will be completely zero and one is 1.0. Our method - // becomes dangerous here due to division with zero. Instead, vector - // 'a' can be an element-wise rotated version of 'v' - auto chk1 = [] (double val) { - return std::abs(std::abs(val) - 1) < 1e-20; - }; - - if(chk1(v(X)) || chk1(v(Y)) || chk1(v(Z))) { - a = {v(Z), v(X), v(Y)}; - b = {v(Y), v(Z), v(X)}; - } - else { - a(Z) = -(v(Y)*a(Y)) / v(Z); a.normalize(); - b = a.cross(v); - } - - // Now a and b vectors are perpendicular to v and to each other. - // Together they define the plane where we have to iterate with the - // given angles in the 'phis' vector - ccr_par::enumerate(phis.begin(), phis.end(), - [&hits, &m, sd, r_pin, r_back, s, a, b, c] - (double phi, size_t i) - { - double sinphi = std::sin(phi); - double cosphi = std::cos(phi); - - // Let's have a safety coefficient for the radiuses. - double rpscos = (sd + r_pin) * cosphi; - double rpssin = (sd + r_pin) * sinphi; - double rpbcos = (sd + r_back) * cosphi; - double rpbsin = (sd + r_back) * sinphi; - - // Point on the circle on the pin sphere - Vec3d ps(s(X) + rpscos * a(X) + rpssin * b(X), - s(Y) + rpscos * a(Y) + rpssin * b(Y), - s(Z) + rpscos * a(Z) + rpssin * b(Z)); - - // Point ps is not on mesh but can be inside or outside as well. - // This would cause many problems with ray-casting. To detect the - // position we will use the ray-casting result (which has an - // is_inside predicate). - - // This is the point on the circle on the back sphere - Vec3d p(c(X) + rpbcos * a(X) + rpbsin * b(X), - c(Y) + rpbcos * a(Y) + rpbsin * b(Y), - c(Z) + rpbcos * a(Z) + rpbsin * b(Z)); - - Vec3d n = (p - ps).normalized(); - auto q = m.query_ray_hit(ps + sd*n, n); - - if(q.is_inside()) { // the hit is inside the model - if(q.distance() > r_pin + sd) { - // If we are inside the model and the hit distance is bigger - // than our pin circle diameter, it probably indicates that - // the support point was already inside the model, or there - // is really no space around the point. We will assign a - // zero hit distance to these cases which will enforce the - // function return value to be an invalid ray with zero hit - // distance. (see min_element at the end) - hits[i] = HitResult(0.0); - } - else { - // re-cast the ray from the outside of the object. - // The starting point has an offset of 2*safety_distance - // because the original ray has also had an offset - auto q2 = m.query_ray_hit(ps + (q.distance() + 2*sd)*n, n); - hits[i] = q2; - } - } else hits[i] = q; - }); - - auto mit = std::min_element(hits.begin(), hits.end()); - - return *mit; - } - - // Checking bridge (pillar and stick as well) intersection with the model. - // If the function is used for headless sticks, the ins_check parameter - // have to be true as the beginning of the stick might be inside the model - // geometry. - // The return value is the hit result from the ray casting. If the starting - // point was inside the model, an "invalid" hit_result will be returned - // with a zero distance value instead of a NAN. This way the result can - // be used safely for comparison with other distances. - EigenMesh3D::hit_result bridge_mesh_intersect( - const Vec3d& s, - const Vec3d& dir, - double r, - bool ins_check = false) - { - static const size_t SAMPLES = 8; - - // helper vector calculations - Vec3d a(0, 1, 0), b; - const double& sd = m_cfg.safety_distance_mm; - - // INFO: for explanation of the method used here, see the previous - // method's comments. - - auto chk1 = [] (double val) { - return std::abs(std::abs(val) - 1) < 1e-20; - }; - - if(chk1(dir(X)) || chk1(dir(Y)) || chk1(dir(Z))) { - a = {dir(Z), dir(X), dir(Y)}; - b = {dir(Y), dir(Z), dir(X)}; - } - else { - a(Z) = -(dir(Y)*a(Y)) / dir(Z); a.normalize(); - b = a.cross(dir); - } - - // circle portions - std::array phis; - for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size(); - - auto& m = m_mesh; - using HitResult = EigenMesh3D::hit_result; - - // Hit results - std::array hits; - - ccr_par::enumerate(phis.begin(), phis.end(), - [&m, a, b, sd, dir, r, s, ins_check, &hits] - (double phi, size_t i) - { - double sinphi = std::sin(phi); - double cosphi = std::cos(phi); - - // Let's have a safety coefficient for the radiuses. - double rcos = (sd + r) * cosphi; - double rsin = (sd + r) * sinphi; - - // Point on the circle on the pin sphere - Vec3d p (s(X) + rcos * a(X) + rsin * b(X), - s(Y) + rcos * a(Y) + rsin * b(Y), - s(Z) + rcos * a(Z) + rsin * b(Z)); - - auto hr = m.query_ray_hit(p + sd*dir, dir); - - if(ins_check && hr.is_inside()) { - if(hr.distance() > 2 * r + sd) hits[i] = HitResult(0.0); - else { - // re-cast the ray from the outside of the object - auto hr2 = - m.query_ray_hit(p + (hr.distance() + 2*sd)*dir, dir); - - hits[i] = hr2; - } - } else hits[i] = hr; - }); - - auto mit = std::min_element(hits.begin(), hits.end()); - - return *mit; - } - - // Helper function for interconnecting two pillars with zig-zag bridges. - bool interconnect(const Pillar& pillar, const Pillar& nextpillar) - { - // We need to get the starting point of the zig-zag pattern. We have to - // be aware that the two head junctions are at different heights. We - // may start from the lowest junction and call it a day but this - // strategy would leave unconnected a lot of pillar duos where the - // shorter pillar is too short to start a new bridge but the taller - // pillar could still be bridged with the shorter one. - bool was_connected = false; - - Vec3d supper = pillar.startpoint(); - Vec3d slower = nextpillar.startpoint(); - Vec3d eupper = pillar.endpoint(); - Vec3d elower = nextpillar.endpoint(); - - double zmin = m_result.ground_level + m_cfg.base_height_mm; - eupper(Z) = std::max(eupper(Z), zmin); - elower(Z) = std::max(elower(Z), zmin); - - // The usable length of both pillars should be positive - if(slower(Z) - elower(Z) < 0) return false; - if(supper(Z) - eupper(Z) < 0) return false; - - double pillar_dist = distance(Vec2d{slower(X), slower(Y)}, - Vec2d{supper(X), supper(Y)}); - double bridge_distance = pillar_dist / std::cos(-m_cfg.bridge_slope); - double zstep = pillar_dist * std::tan(-m_cfg.bridge_slope); - - if(pillar_dist < 2 * m_cfg.head_back_radius_mm || - pillar_dist > m_cfg.max_pillar_link_distance_mm) return false; - - if(supper(Z) < slower(Z)) supper.swap(slower); - if(eupper(Z) < elower(Z)) eupper.swap(elower); - - double startz = 0, endz = 0; - - startz = slower(Z) - zstep < supper(Z) ? slower(Z) - zstep : slower(Z); - endz = eupper(Z) + zstep > elower(Z) ? eupper(Z) + zstep : eupper(Z); - - if(slower(Z) - eupper(Z) < std::abs(zstep)) { - // no space for even one cross - - // Get max available space - startz = std::min(supper(Z), slower(Z) - zstep); - endz = std::max(eupper(Z) + zstep, elower(Z)); - - // Align to center - double available_dist = (startz - endz); - double rounds = std::floor(available_dist / std::abs(zstep)); - startz -= 0.5 * (available_dist - rounds * std::abs(zstep));; - } - - auto pcm = m_cfg.pillar_connection_mode; - bool docrosses = - pcm == PillarConnectionMode::cross || - (pcm == PillarConnectionMode::dynamic && - pillar_dist > 2*m_cfg.base_radius_mm); - - // 'sj' means starting junction, 'ej' is the end junction of a bridge. - // They will be swapped in every iteration thus the zig-zag pattern. - // According to a config parameter, a second bridge may be added which - // results in a cross connection between the pillars. - Vec3d sj = supper, ej = slower; sj(Z) = startz; ej(Z) = sj(Z) + zstep; - - // TODO: This is a workaround to not have a faulty last bridge - while(ej(Z) >= eupper(Z) /*endz*/) { - if(bridge_mesh_intersect(sj, - dirv(sj, ej), - pillar.r) >= bridge_distance) - { - m_result.add_bridge(sj, ej, pillar.r); - was_connected = true; - } - - // double bridging: (crosses) - if(docrosses) { - Vec3d sjback(ej(X), ej(Y), sj(Z)); - Vec3d ejback(sj(X), sj(Y), ej(Z)); - if(sjback(Z) <= slower(Z) && ejback(Z) >= eupper(Z) && - bridge_mesh_intersect(sjback, - dirv(sjback, ejback), - pillar.r) >= bridge_distance) - { - // need to check collision for the cross stick - m_result.add_bridge(sjback, ejback, pillar.r); - was_connected = true; - } - } - - sj.swap(ej); - ej(Z) = sj(Z) + zstep; - } - - return was_connected; - } - - // For connecting a head to a nearby pillar. - bool connect_to_nearpillar(const Head& head, long nearpillar_id) { - - auto nearpillar = [this, nearpillar_id]() { - return m_result.pillar(nearpillar_id); - }; - - if (nearpillar().bridges > m_cfg.max_bridges_on_pillar) return false; - - Vec3d headjp = head.junction_point(); - Vec3d nearjp_u = nearpillar().startpoint(); - Vec3d nearjp_l = nearpillar().endpoint(); - - double r = head.r_back_mm; - double d2d = distance(to_2d(headjp), to_2d(nearjp_u)); - double d3d = distance(headjp, nearjp_u); - - double hdiff = nearjp_u(Z) - headjp(Z); - double slope = std::atan2(hdiff, d2d); - - Vec3d bridgestart = headjp; - Vec3d bridgeend = nearjp_u; - double max_len = m_cfg.max_bridge_length_mm; - double max_slope = m_cfg.bridge_slope; - double zdiff = 0.0; - - // check the default situation if feasible for a bridge - if(d3d > max_len || slope > -max_slope) { - // not feasible to connect the two head junctions. We have to search - // for a suitable touch point. - - double Zdown = headjp(Z) + d2d * std::tan(-max_slope); - Vec3d touchjp = bridgeend; touchjp(Z) = Zdown; - double D = distance(headjp, touchjp); - zdiff = Zdown - nearjp_u(Z); - - if(zdiff > 0) { - Zdown -= zdiff; - bridgestart(Z) -= zdiff; - touchjp(Z) = Zdown; - - double t = bridge_mesh_intersect(headjp, {0,0,-1}, r); - - // We can't insert a pillar under the source head to connect - // with the nearby pillar's starting junction - if(t < zdiff) return false; - } - - if(Zdown <= nearjp_u(Z) && Zdown >= nearjp_l(Z) && D < max_len) - bridgeend(Z) = Zdown; - else - return false; - } - - // There will be a minimum distance from the ground where the - // bridge is allowed to connect. This is an empiric value. - double minz = m_result.ground_level + 2 * m_cfg.head_width_mm; - if(bridgeend(Z) < minz) return false; - - double t = bridge_mesh_intersect(bridgestart, - dirv(bridgestart, bridgeend), r); - - // Cannot insert the bridge. (further search might not worth the hassle) - if(t < distance(bridgestart, bridgeend)) return false; - - // A partial pillar is needed under the starting head. - if(zdiff > 0) { - m_result.add_pillar(unsigned(head.id), bridgestart, r); - m_result.add_junction(bridgestart, r); - } - - m_result.add_bridge(bridgestart, bridgeend, r); - m_result.increment_bridges(nearpillar()); - - return true; - } - - bool search_pillar_and_connect(const Head& head) { - PointIndex spindex = m_pillar_index; - - long nearest_id = -1; - - Vec3d querypoint = head.junction_point(); - - while(nearest_id < 0 && !spindex.empty()) { m_thr(); - // loop until a suitable head is not found - // if there is a pillar closer than the cluster center - // (this may happen as the clustering is not perfect) - // than we will bridge to this closer pillar - - Vec3d qp(querypoint(X), querypoint(Y), m_result.ground_level); - auto qres = spindex.nearest(qp, 1); - if(qres.empty()) break; - - auto ne = qres.front(); - nearest_id = ne.second; - - if(nearest_id >= 0) { - auto nearpillarID = unsigned(nearest_id); - if(nearpillarID < m_result.pillarcount()) { - if(!connect_to_nearpillar(head, nearpillarID)) { - nearest_id = -1; // continue searching - spindex.remove(ne); // without the current pillar - } - } - } - } - - return nearest_id >= 0; - } - - // This is a proxy function for pillar creation which will mind the gap - // between the pad and the model bottom in zero elevation mode. - void create_ground_pillar(const Vec3d &jp, - const Vec3d &sourcedir, - double radius, - int head_id = -1) - { - // People were killed for this number (seriously) - static const double SQR2 = std::sqrt(2.0); - static const Vec3d DOWN = {0.0, 0.0, -1.0}; - - double gndlvl = m_result.ground_level; - Vec3d endp = {jp(X), jp(Y), gndlvl}; - double sd = m_cfg.pillar_base_safety_distance_mm; - int pillar_id = -1; - double min_dist = sd + m_cfg.base_radius_mm + EPSILON; - double dist = 0; - bool can_add_base = true; - bool normal_mode = true; - - if (m_cfg.object_elevation_mm < EPSILON - && (dist = std::sqrt(m_mesh.squared_distance(endp))) < min_dist) { - // Get the distance from the mesh. This can be later optimized - // to get the distance in 2D plane because we are dealing with - // the ground level only. - - normal_mode = false; - double mv = min_dist - dist; - double azimuth = std::atan2(sourcedir(Y), sourcedir(X)); - double sinpolar = std::sin(PI - m_cfg.bridge_slope); - double cospolar = std::cos(PI - m_cfg.bridge_slope); - double cosazm = std::cos(azimuth); - double sinazm = std::sin(azimuth); - - auto dir = Vec3d(cosazm * sinpolar, sinazm * sinpolar, cospolar) - .normalized(); - - using namespace libnest2d::opt; - StopCriteria scr; - scr.stop_score = min_dist; - SubplexOptimizer solver(scr); - - auto result = solver.optimize_max( - [this, dir, jp, gndlvl](double mv) { - Vec3d endp = jp + SQR2 * mv * dir; - endp(Z) = gndlvl; - return std::sqrt(m_mesh.squared_distance(endp)); - }, - initvals(mv), bound(0.0, 2 * min_dist)); - - mv = std::get<0>(result.optimum); - endp = jp + SQR2 * mv * dir; - Vec3d pgnd = {endp(X), endp(Y), gndlvl}; - can_add_base = result.score > min_dist; - - double gnd_offs = m_mesh.ground_level_offset(); - auto abort_in_shame = - [gnd_offs, &normal_mode, &can_add_base, &endp, jp, gndlvl]() - { - normal_mode = true; - can_add_base = false; // Nothing left to do, hope for the best - endp = {jp(X), jp(Y), gndlvl - gnd_offs }; - }; - - // We have to check if the bridge is feasible. - if (bridge_mesh_intersect(jp, dir, radius) < (endp - jp).norm()) - abort_in_shame(); - else { - // If the new endpoint is below ground, do not make a pillar - if (endp(Z) < gndlvl) - endp = endp - SQR2 * (gndlvl - endp(Z)) * dir; // back off - else { - - auto hit = bridge_mesh_intersect(endp, DOWN, radius); - if (!std::isinf(hit.distance())) abort_in_shame(); - - Pillar &plr = m_result.add_pillar(endp, pgnd, radius); - - if (can_add_base) - plr.add_base(m_cfg.base_height_mm, - m_cfg.base_radius_mm); - - pillar_id = plr.id; - } - - m_result.add_bridge(jp, endp, radius); - m_result.add_junction(endp, radius); - - // Add a degenerated pillar and the bridge. - // The degenerate pillar will have zero length and it will - // prevent from queries of head_pillar() to have non-existing - // pillar when the head should have one. - if (head_id >= 0) - m_result.add_pillar(unsigned(head_id), jp, radius); - } - } - - if (normal_mode) { - Pillar &plr = head_id >= 0 - ? m_result.add_pillar(unsigned(head_id), - endp, - radius) - : m_result.add_pillar(jp, endp, radius); - - if (can_add_base) - plr.add_base(m_cfg.base_height_mm, m_cfg.base_radius_mm); - - pillar_id = plr.id; - } - - if(pillar_id >= 0) // Save the pillar endpoint in the spatial index - m_pillar_index.insert(endp, pillar_id); - } - -public: - - Algorithm(const SupportConfig& config, - const EigenMesh3D& emesh, - const std::vector& support_pts, - Result& result, - ThrowOnCancel thr) : - m_cfg(config), - m_mesh(emesh), - m_support_pts(support_pts), - m_support_nmls(support_pts.size(), 3), - m_result(result), - m_points(support_pts.size(), 3), - m_thr(thr) - { - // Prepare the support points in Eigen/IGL format as well, we will use - // it mostly in this form. - - long i = 0; - for(const SupportPoint& sp : m_support_pts) { - m_points.row(i)(X) = double(sp.pos(X)); - m_points.row(i)(Y) = double(sp.pos(Y)); - m_points.row(i)(Z) = double(sp.pos(Z)); - ++i; - } - } - - - // Now let's define the individual steps of the support generation algorithm - - // Filtering step: here we will discard inappropriate support points - // and decide the future of the appropriate ones. We will check if a - // pinhead is applicable and adjust its angle at each support point. We - // will also merge the support points that are just too close and can - // be considered as one. - void filter() { - // Get the points that are too close to each other and keep only the - // first one - auto aliases = cluster(m_points, D_SP, 2); - - PtIndices filtered_indices; - filtered_indices.reserve(aliases.size()); - m_iheads.reserve(aliases.size()); - m_iheadless.reserve(aliases.size()); - for(auto& a : aliases) { - // Here we keep only the front point of the cluster. - filtered_indices.emplace_back(a.front()); - } - - // calculate the normals to the triangles for filtered points - auto nmls = sla::normals(m_points, m_mesh, m_cfg.head_front_radius_mm, - m_thr, filtered_indices); - - // Not all of the support points have to be a valid position for - // support creation. The angle may be inappropriate or there may - // not be enough space for the pinhead. Filtering is applied for - // these reasons. - - using libnest2d::opt::bound; - using libnest2d::opt::initvals; - using libnest2d::opt::GeneticOptimizer; - using libnest2d::opt::StopCriteria; - - ccr::Mutex mutex; - auto addfn = [&mutex](PtIndices &container, unsigned val) { - std::lock_guard lk(mutex); - container.emplace_back(val); - }; - - ccr::enumerate(filtered_indices.begin(), filtered_indices.end(), - [this, &nmls, addfn](unsigned fidx, size_t i) - { - m_thr(); - - auto n = nmls.row(i); - - // for all normals we generate the spherical coordinates and - // saturate the polar angle to 45 degrees from the bottom then - // convert back to standard coordinates to get the new normal. - // Then we just create a quaternion from the two normals - // (Quaternion::FromTwoVectors) and apply the rotation to the - // arrow head. - - double z = n(2); - double r = 1.0; // for normalized vector - double polar = std::acos(z / r); - double azimuth = std::atan2(n(1), n(0)); - - // skip if the tilt is not sane - if(polar >= PI - m_cfg.normal_cutoff_angle) { - - // We saturate the polar angle to 3pi/4 - polar = std::max(polar, 3*PI / 4); - - // save the head (pinpoint) position - Vec3d hp = m_points.row(fidx); - - double w = m_cfg.head_width_mm + - m_cfg.head_back_radius_mm + - 2*m_cfg.head_front_radius_mm; - - double pin_r = double(m_support_pts[fidx].head_front_radius); - - // Reassemble the now corrected normal - auto nn = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); - - // check available distance - EigenMesh3D::hit_result t - = pinhead_mesh_intersect(hp, // touching point - nn, // normal - pin_r, - m_cfg.head_back_radius_mm, - w); - - if(t.distance() <= w) { - - // Let's try to optimize this angle, there might be a - // viable normal that doesn't collide with the model - // geometry and its very close to the default. - - StopCriteria stc; - stc.max_iterations = m_cfg.optimizer_max_iterations; - stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; - stc.stop_score = w; // space greater than w is enough - GeneticOptimizer solver(stc); - solver.seed(0); // we want deterministic behavior - - auto oresult = solver.optimize_max( - [this, pin_r, w, hp](double plr, double azm) - { - auto n = Vec3d(std::cos(azm) * std::sin(plr), - std::sin(azm) * std::sin(plr), - std::cos(plr)).normalized(); - - double score = pinhead_mesh_intersect( - hp, n, pin_r, m_cfg.head_back_radius_mm, w); - - return score; - }, - initvals(polar, azimuth), // start with what we have - bound(3*PI/4, PI), // Must not exceed the tilt limit - bound(-PI, PI) // azimuth can be a full search - ); - - if(oresult.score > w) { - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - nn = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); - t = oresult.score; - } - } - - // save the verified and corrected normal - m_support_nmls.row(fidx) = nn; - - if (t.distance() > w) { - // Check distance from ground, we might have zero elevation. - if (hp(Z) + w * nn(Z) < m_result.ground_level) { - addfn(m_iheadless, fidx); - } else { - // mark the point for needing a head. - addfn(m_iheads, fidx); - } - } else if (polar >= 3 * PI / 4) { - // Headless supports do not tilt like the headed ones - // so the normal should point almost to the ground. - addfn(m_iheadless, fidx); - } - } - }); - - m_thr(); - } - - // Pinhead creation: based on the filtering results, the Head objects - // will be constructed (together with their triangle meshes). - void add_pinheads() - { - for (unsigned i : m_iheads) { - m_thr(); - m_result.add_head( - i, - m_cfg.head_back_radius_mm, - m_support_pts[i].head_front_radius, - m_cfg.head_width_mm, - m_cfg.head_penetration_mm, - m_support_nmls.row(i), // dir - m_support_pts[i].pos.cast() // displacement - ); - } - } - - // Further classification of the support points with pinheads. If the - // ground is directly reachable through a vertical line parallel to the - // Z axis we consider a support point as pillar candidate. If touches - // the model geometry, it will be marked as non-ground facing and - // further steps will process it. Also, the pillars will be grouped - // into clusters that can be interconnected with bridges. Elements of - // these groups may or may not be interconnected. Here we only run the - // clustering algorithm. - void classify() - { - // We should first get the heads that reach the ground directly - PtIndices ground_head_indices; - ground_head_indices.reserve(m_iheads.size()); - m_iheads_onmodel.reserve(m_iheads.size()); - - // First we decide which heads reach the ground and can be full - // pillars and which shall be connected to the model surface (or - // search a suitable path around the surface that leads to the - // ground -- TODO) - for(unsigned i : m_iheads) { - m_thr(); - - auto& head = m_result.head(i); - Vec3d n(0, 0, -1); - double r = head.r_back_mm; - Vec3d headjp = head.junction_point(); - - // collision check - auto hit = bridge_mesh_intersect(headjp, n, r); - - if(std::isinf(hit.distance())) ground_head_indices.emplace_back(i); - else if(m_cfg.ground_facing_only) head.invalidate(); - else m_iheads_onmodel.emplace_back(std::make_pair(i, hit)); - } - - // We want to search for clusters of points that are far enough - // from each other in the XY plane to not cross their pillar bases - // These clusters of support points will join in one pillar, - // possibly in their centroid support point. - - auto pointfn = [this](unsigned i) { - return m_result.head(i).junction_point(); - }; - - auto predicate = [this](const PointIndexEl &e1, - const PointIndexEl &e2) { - double d2d = distance(to_2d(e1.first), to_2d(e2.first)); - double d3d = distance(e1.first, e2.first); - return d2d < 2 * m_cfg.base_radius_mm - && d3d < m_cfg.max_bridge_length_mm; - }; - - m_pillar_clusters = cluster(ground_head_indices, - pointfn, - predicate, - m_cfg.max_bridges_on_pillar); - } - - // Step: Routing the ground connected pinheads, and interconnecting - // them with additional (angled) bridges. Not all of these pinheads - // will be a full pillar (ground connected). Some will connect to a - // nearby pillar using a bridge. The max number of such side-heads for - // a central pillar is limited to avoid bad weight distribution. - void routing_to_ground() - { - const double pradius = m_cfg.head_back_radius_mm; - // const double gndlvl = m_result.ground_level; - - ClusterEl cl_centroids; - cl_centroids.reserve(m_pillar_clusters.size()); - - for(auto& cl : m_pillar_clusters) { m_thr(); - // place all the centroid head positions into the index. We - // will query for alternative pillar positions. If a sidehead - // cannot connect to the cluster centroid, we have to search - // for another head with a full pillar. Also when there are two - // elements in the cluster, the centroid is arbitrary and the - // sidehead is allowed to connect to a nearby pillar to - // increase structural stability. - - if(cl.empty()) continue; - - // get the current cluster centroid - auto& thr = m_thr; const auto& points = m_points; - long lcid = cluster_centroid(cl, - [&points](size_t idx) { return points.row(long(idx)); }, - [thr](const Vec3d& p1, const Vec3d& p2) - { - thr(); - return distance(Vec2d(p1(X), p1(Y)), Vec2d(p2(X), p2(Y))); - }); - - assert(lcid >= 0); - unsigned hid = cl[size_t(lcid)]; // Head ID - - cl_centroids.emplace_back(hid); - - Head& h = m_result.head(hid); - h.transform(); - - create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id); - } - - // now we will go through the clusters ones again and connect the - // sidepoints with the cluster centroid (which is a ground pillar) - // or a nearby pillar if the centroid is unreachable. - size_t ci = 0; - for(auto cl : m_pillar_clusters) { m_thr(); - - auto cidx = cl_centroids[ci++]; - - // TODO: don't consider the cluster centroid but calculate a - // central position where the pillar can be placed. this way - // the weight is distributed more effectively on the pillar. - - auto centerpillarID = m_result.head_pillar(cidx).id; - - for(auto c : cl) { m_thr(); - if(c == cidx) continue; - - auto& sidehead = m_result.head(c); - sidehead.transform(); - - if(!connect_to_nearpillar(sidehead, centerpillarID) && - !search_pillar_and_connect(sidehead)) - { - Vec3d pstart = sidehead.junction_point(); - //Vec3d pend = Vec3d{pstart(X), pstart(Y), gndlvl}; - // Could not find a pillar, create one - create_ground_pillar(pstart, - sidehead.dir, - pradius, - sidehead.id); - } - } - } - } - - // Step: routing the pinheads that would connect to the model surface - // along the Z axis downwards. For now these will actually be connected with - // the model surface with a flipped pinhead. In the future here we could use - // some smart algorithms to search for a safe path to the ground or to a - // nearby pillar that can hold the supported weight. - void routing_to_model() - { - - // We need to check if there is an easy way out to the bed surface. - // If it can be routed there with a bridge shorter than - // min_bridge_distance. - - // First we want to index the available pillars. The best is to connect - // these points to the available pillars - - auto routedown = [this](Head& head, const Vec3d& dir, double dist) - { - head.transform(); - Vec3d hjp = head.junction_point(); - Vec3d endp = hjp + dist * dir; - m_result.add_bridge(hjp, endp, head.r_back_mm); - m_result.add_junction(endp, head.r_back_mm); - - this->create_ground_pillar(endp, dir, head.r_back_mm); - }; - - std::vector modelpillars; - ccr::Mutex mutex; - - // TODO: connect these to the ground pillars if possible - ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), - [this, routedown, &modelpillars, &mutex] - (const std::pair &el, - size_t) - { - m_thr(); - unsigned idx = el.first; - EigenMesh3D::hit_result hit = el.second; - - auto& head = m_result.head(idx); - Vec3d hjp = head.junction_point(); - - // ///////////////////////////////////////////////////////////////// - // Search nearby pillar - // ///////////////////////////////////////////////////////////////// - - if(search_pillar_and_connect(head)) { head.transform(); return; } - - // ///////////////////////////////////////////////////////////////// - // Try straight path - // ///////////////////////////////////////////////////////////////// - - // Cannot connect to nearby pillar. We will try to search for - // a route to the ground. - - double t = bridge_mesh_intersect(hjp, head.dir, head.r_back_mm); - double d = 0, tdown = 0; - Vec3d dirdown(0.0, 0.0, -1.0); - - t = std::min(t, m_cfg.max_bridge_length_mm); - - while(d < t && !std::isinf(tdown = bridge_mesh_intersect( - hjp + d*head.dir, - dirdown, head.r_back_mm))) { - d += head.r_back_mm; - } - - if(std::isinf(tdown)) { // we heave found a route to the ground - routedown(head, head.dir, d); return; - } - - // ///////////////////////////////////////////////////////////////// - // Optimize bridge direction - // ///////////////////////////////////////////////////////////////// - - // Straight path failed so we will try to search for a suitable - // direction out of the cavity. - - // Get the spherical representation of the normal. its easier to - // work with. - double z = head.dir(Z); - double r = 1.0; // for normalized vector - double polar = std::acos(z / r); - double azimuth = std::atan2(head.dir(Y), head.dir(X)); - - using libnest2d::opt::bound; - using libnest2d::opt::initvals; - using libnest2d::opt::GeneticOptimizer; - using libnest2d::opt::StopCriteria; - - StopCriteria stc; - stc.max_iterations = m_cfg.optimizer_max_iterations; - stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; - stc.stop_score = 1e6; - GeneticOptimizer solver(stc); - solver.seed(0); // we want deterministic behavior - - double r_back = head.r_back_mm; - - auto oresult = solver.optimize_max( - [this, hjp, r_back](double plr, double azm) - { - Vec3d n = Vec3d(std::cos(azm) * std::sin(plr), - std::sin(azm) * std::sin(plr), - std::cos(plr)).normalized(); - return bridge_mesh_intersect(hjp, n, r_back); - }, - initvals(polar, azimuth), // let's start with what we have - bound(3*PI/4, PI), // Must not exceed the slope limit - bound(-PI, PI) // azimuth can be a full range search - ); - - d = 0; t = oresult.score; - - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - Vec3d bridgedir = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); - - t = std::min(t, m_cfg.max_bridge_length_mm); - - while(d < t && !std::isinf(tdown = bridge_mesh_intersect( - hjp + d*bridgedir, - dirdown, - head.r_back_mm))) { - d += head.r_back_mm; - } - - if(std::isinf(tdown)) { // we heave found a route to the ground - routedown(head, bridgedir, d); return; - } - - // ///////////////////////////////////////////////////////////////// - // Route to model body - // ///////////////////////////////////////////////////////////////// - - double zangle = std::asin(hit.direction()(Z)); - zangle = std::max(zangle, PI/4); - double h = std::sin(zangle) * head.fullwidth(); - - // The width of the tail head that we would like to have... - h = std::min(hit.distance() - head.r_back_mm, h); - - if(h > 0) { - Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h}; - auto center_hit = m_mesh.query_ray_hit(hjp, dirdown); - - double hitdiff = center_hit.distance() - hit.distance(); - Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm? - center_hit.position() : hit.position(); - - head.transform(); - - Pillar& pill = m_result.add_pillar(unsigned(head.id), - endp, - head.r_back_mm); - - Vec3d taildir = endp - hitp; - double dist = distance(endp, hitp) + m_cfg.head_penetration_mm; - double w = dist - 2 * head.r_pin_mm - head.r_back_mm; - - Head tailhead(head.r_back_mm, - head.r_pin_mm, - w, - m_cfg.head_penetration_mm, - taildir, - hitp); - - tailhead.transform(); - pill.base = tailhead.mesh; - - // Experimental: add the pillar to the index for cascading - std::lock_guard lk(mutex); - modelpillars.emplace_back(unsigned(pill.id)); - return; - } - - // We have failed to route this head. - BOOST_LOG_TRIVIAL(warning) - << "Failed to route model facing support point." - << " ID: " << idx; - head.invalidate(); - }); - - for(auto pillid : modelpillars) { - auto& pillar = m_result.pillar(pillid); - m_pillar_index.insert(pillar.endpoint(), pillid); - } - } - - // Helper function for interconnect_pillars where pairs of already connected - // pillars should be checked for not to be processed again. This can be done - // in O(log) or even constant time with a set or an unordered set of hash - // values uniquely representing a pair of integers. The order of numbers - // within the pair should not matter, it has the same unique hash. - template static I pairhash(I a, I b) - { - using std::ceil; using std::log2; using std::max; using std::min; - - static_assert(std::is_integral::value, - "This function works only for integral types."); - - I g = min(a, b), l = max(a, b); - - auto bits_g = g ? int(ceil(log2(g))) : 0; - - // Assume the hash will fit into the output variable - assert((l ? (ceil(log2(l))) : 0) + bits_g < int(sizeof(I) * CHAR_BIT)); - - return (l << bits_g) + g; - } - - void interconnect_pillars() { - // Now comes the algorithm that connects pillars with each other. - // Ideally every pillar should be connected with at least one of its - // neighbors if that neighbor is within max_pillar_link_distance - - // Pillars with height exceeding H1 will require at least one neighbor - // to connect with. Height exceeding H2 require two neighbors. - double H1 = m_cfg.max_solo_pillar_height_mm; - double H2 = m_cfg.max_dual_pillar_height_mm; - double d = m_cfg.max_pillar_link_distance_mm; - - //A connection between two pillars only counts if the height ratio is - // bigger than 50% - double min_height_ratio = 0.5; - - std::set pairs; - - // A function to connect one pillar with its neighbors. THe number of - // neighbors is given in the configuration. This function if called - // for every pillar in the pillar index. A pair of pillar will not - // be connected multiple times this is ensured by the 'pairs' set which - // remembers the processed pillar pairs - auto cascadefn = - [this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el) - { - Vec3d qp = el.first; // endpoint of the pillar - - const Pillar& pillar = m_result.pillar(el.second); // actual pillar - - // Get the max number of neighbors a pillar should connect to - unsigned neighbors = m_cfg.pillar_cascade_neighbors; - - // connections are already enough for the pillar - if(pillar.links >= neighbors) return; - - // Query all remaining points within reach - auto qres = m_pillar_index.query([qp, d](const PointIndexEl& e){ - return distance(e.first, qp) < d; - }); - - // sort the result by distance (have to check if this is needed) - std::sort(qres.begin(), qres.end(), - [qp](const PointIndexEl& e1, const PointIndexEl& e2){ - return distance(e1.first, qp) < distance(e2.first, qp); - }); - - for(auto& re : qres) { // process the queried neighbors - - if(re.second == el.second) continue; // Skip self - - auto a = el.second, b = re.second; - - // Get unique hash for the given pair (order doesn't matter) - auto hashval = pairhash(a, b); - - // Search for the pair amongst the remembered pairs - if(pairs.find(hashval) != pairs.end()) continue; - - const Pillar& neighborpillar = m_result.pillar(re.second); - - // this neighbor is occupied, skip - if(neighborpillar.links >= neighbors) continue; - - if(interconnect(pillar, neighborpillar)) { - pairs.insert(hashval); - - // If the interconnection length between the two pillars is - // less than 50% of the longer pillar's height, don't count - if(pillar.height < H1 || - neighborpillar.height / pillar.height > min_height_ratio) - m_result.increment_links(pillar); - - if(neighborpillar.height < H1 || - pillar.height / neighborpillar.height > min_height_ratio) - m_result.increment_links(neighborpillar); - - } - - // connections are enough for one pillar - if(pillar.links >= neighbors) break; - } - }; - - // Run the cascade for the pillars in the index - m_pillar_index.foreach(cascadefn); - - // We would be done here if we could allow some pillars to not be - // connected with any neighbors. But this might leave the support tree - // unprintable. - // - // The current solution is to insert additional pillars next to these - // lonely pillars. One or even two additional pillar might get inserted - // depending on the length of the lonely pillar. - - size_t pillarcount = m_result.pillarcount(); - - // Again, go through all pillars, this time in the whole support tree - // not just the index. - for(size_t pid = 0; pid < pillarcount; pid++) { - auto pillar = [this, pid]() { return m_result.pillar(pid); }; - - // Decide how many additional pillars will be needed: - - unsigned needpillars = 0; - if (pillar().bridges > m_cfg.max_bridges_on_pillar) - needpillars = 3; - else if (pillar().links < 2 && pillar().height > H2) { - // Not enough neighbors to support this pillar - needpillars = 2 - pillar().links; - } else if (pillar().links < 1 && pillar().height > H1) { - // No neighbors could be found and the pillar is too long. - needpillars = 1; - } - - // Search for new pillar locations: - - bool found = false; - double alpha = 0; // goes to 2Pi - double r = 2 * m_cfg.base_radius_mm; - Vec3d pillarsp = pillar().startpoint(); - - // temp value for starting point detection - Vec3d sp(pillarsp(X), pillarsp(Y), pillarsp(Z) - r); - - // A vector of bool for placement feasbility - std::vector canplace(needpillars, false); - std::vector spts(needpillars); // vector of starting points - - double gnd = m_result.ground_level; - double min_dist = m_cfg.pillar_base_safety_distance_mm + - m_cfg.base_radius_mm + EPSILON; - - while(!found && alpha < 2*PI) { - for (unsigned n = 0; - n < needpillars && (!n || canplace[n - 1]); - n++) - { - double a = alpha + n * PI / 3; - Vec3d s = sp; - s(X) += std::cos(a) * r; - s(Y) += std::sin(a) * r; - spts[n] = s; - - // Check the path vertically down - auto hr = bridge_mesh_intersect(s, {0, 0, -1}, pillar().r); - Vec3d gndsp{s(X), s(Y), gnd}; - - // If the path is clear, check for pillar base collisions - canplace[n] = std::isinf(hr.distance()) && - std::sqrt(m_mesh.squared_distance(gndsp)) > - min_dist; - } - - found = std::all_of(canplace.begin(), canplace.end(), - [](bool v) { return v; }); - - // 20 angles will be tried... - alpha += 0.1 * PI; - } - - std::vector newpills; - newpills.reserve(needpillars); - - if(found) for(unsigned n = 0; n < needpillars; n++) { - Vec3d s = spts[n]; - Pillar p(s, Vec3d(s(X), s(Y), gnd), pillar().r); - p.add_base(m_cfg.base_height_mm, m_cfg.base_radius_mm); - - if(interconnect(pillar(), p)) { - Pillar& pp = m_result.add_pillar(p); - m_pillar_index.insert(pp.endpoint(), unsigned(pp.id)); - - m_result.add_junction(s, pillar().r); - double t = bridge_mesh_intersect(pillarsp, - dirv(pillarsp, s), - pillar().r); - if(distance(pillarsp, s) < t) - m_result.add_bridge(pillarsp, s, pillar().r); - - if(pillar().endpoint()(Z) > m_result.ground_level) - m_result.add_junction(pillar().endpoint(), pillar().r); - - newpills.emplace_back(pp.id); - m_result.increment_links(pillar()); - } - } - - if(!newpills.empty()) { - for(auto it = newpills.begin(), nx = std::next(it); - nx != newpills.end(); ++it, ++nx) { - const Pillar& itpll = m_result.pillar(*it); - const Pillar& nxpll = m_result.pillar(*nx); - if(interconnect(itpll, nxpll)) { - m_result.increment_links(itpll); - m_result.increment_links(nxpll); - } - } - - m_pillar_index.foreach(cascadefn); - } - } - } - - // Step: process the support points where there is not enough space for a - // full pinhead. In this case we will use a rounded sphere as a touching - // point and use a thinner bridge (let's call it a stick). - void routing_headless () - { - // For now we will just generate smaller headless sticks with a sharp - // ending point that connects to the mesh surface. - - // We will sink the pins into the model surface for a distance of 1/3 of - // the pin radius - for(unsigned i : m_iheadless) { m_thr(); - - const auto R = double(m_support_pts[i].head_front_radius); - const double HWIDTH_MM = R/3; - - // Exact support position - Vec3d sph = m_support_pts[i].pos.cast(); - Vec3d n = m_support_nmls.row(i); // mesh outward normal - Vec3d sp = sph - n * HWIDTH_MM; // stick head start point - - Vec3d dir = {0, 0, -1}; - Vec3d sj = sp + R * n; // stick start point - - // This is only for checking - double idist = bridge_mesh_intersect(sph, dir, R, true); - double dist = ray_mesh_intersect(sj, dir); - if (std::isinf(dist)) - dist = sph(Z) - m_mesh.ground_level() - + m_mesh.ground_level_offset(); - - if(std::isnan(idist) || idist < 2*R || - std::isnan(dist) || dist < 2*R) - { - BOOST_LOG_TRIVIAL(warning) << "Can not find route for headless" - << " support stick at: " - << sj.transpose(); - continue; - } - - Vec3d ej = sj + (dist + HWIDTH_MM)* dir; - m_result.add_compact_bridge(sp, ej, n, R, !std::isinf(dist)); - } - } - - void merge_result() { m_result.merge_and_cleanup(); } -}; - -bool SLASupportTree::generate(const std::vector &support_points, - const EigenMesh3D& mesh, - const SupportConfig &cfg, - const Controller &ctl) -{ - if(support_points.empty()) return false; - - Algorithm alg(cfg, mesh, support_points, *m_impl, ctl.cancelfn); - - // Let's define the individual steps of the processing. We can experiment - // later with the ordering and the dependencies between them. - enum Steps { - BEGIN, - FILTER, - PINHEADS, - CLASSIFY, - ROUTING_GROUND, - ROUTING_NONGROUND, - CASCADE_PILLARS, - HEADLESS, - MERGE_RESULT, - DONE, - ABORT, - NUM_STEPS - //... - }; - - // Collect the algorithm steps into a nice sequence - std::array, NUM_STEPS> program = { - [] () { - // Begin... - // Potentially clear up the shared data (not needed for now) - }, - - std::bind(&Algorithm::filter, &alg), - - std::bind(&Algorithm::add_pinheads, &alg), - - std::bind(&Algorithm::classify, &alg), - - std::bind(&Algorithm::routing_to_ground, &alg), - - std::bind(&Algorithm::routing_to_model, &alg), - - std::bind(&Algorithm::interconnect_pillars, &alg), - - std::bind(&Algorithm::routing_headless, &alg), - - std::bind(&Algorithm::merge_result, &alg), - - [] () { - // Done - }, - - [] () { - // Abort - } - }; - - Steps pc = BEGIN; - - if(cfg.ground_facing_only) { - program[ROUTING_NONGROUND] = []() { - BOOST_LOG_TRIVIAL(info) - << "Skipping model-facing supports as requested."; - }; - program[HEADLESS] = []() { - BOOST_LOG_TRIVIAL(info) << "Skipping headless stick generation as" - " requested."; - }; - } - - // Let's define a simple automaton that will run our program. - auto progress = [&ctl, &pc] () { - static const std::array stepstr { - "Starting", - "Filtering", - "Generate pinheads", - "Classification", - "Routing to ground", - "Routing supports to model surface", - "Interconnecting pillars", - "Processing small holes", - "Merging support mesh", - "Done", - "Abort" - }; - - static const std::array stepstate { - 0, - 10, - 30, - 50, - 60, - 70, - 80, - 85, - 99, - 100, - 0 - }; - - if(ctl.stopcondition()) pc = ABORT; - - switch(pc) { - case BEGIN: pc = FILTER; break; - case FILTER: pc = PINHEADS; break; - case PINHEADS: pc = CLASSIFY; break; - case CLASSIFY: pc = ROUTING_GROUND; break; - case ROUTING_GROUND: pc = ROUTING_NONGROUND; break; - case ROUTING_NONGROUND: pc = CASCADE_PILLARS; break; - case CASCADE_PILLARS: pc = HEADLESS; break; - case HEADLESS: pc = MERGE_RESULT; break; - case MERGE_RESULT: pc = DONE; break; - case DONE: - case ABORT: break; - default: ; - } - - ctl.statuscb(stepstate[pc], stepstr[pc]); - }; - - // Just here we run the computation... - while(pc < DONE) { - progress(); - program[pc](); - } - - return pc == ABORT; -} - -SLASupportTree::SLASupportTree(double gnd_lvl): m_impl(new Impl()) { - m_impl->ground_level = gnd_lvl; -} - -const TriangleMesh &SLASupportTree::merged_mesh() const -{ - return m_impl->merged_mesh(); -} - -void SLASupportTree::merged_mesh_with_pad(TriangleMesh &outmesh) const { - outmesh.merge(merged_mesh()); - outmesh.merge(get_pad()); -} - -std::vector SLASupportTree::slice( +std::vector SupportTree::slice( const std::vector &grid, float cr) const { - const TriangleMesh &sup_mesh = m_impl->merged_mesh(); - const TriangleMesh &pad_mesh = get_pad(); + const TriangleMesh &sup_mesh = retrieve_mesh(MeshType::Support); + const TriangleMesh &pad_mesh = retrieve_mesh(MeshType::Pad); using Slices = std::vector; auto slices = reserve_vector(2); @@ -2609,7 +61,7 @@ std::vector SLASupportTree::slice( slices.emplace_back(); TriangleMeshSlicer sup_slicer(&sup_mesh); - sup_slicer.slice(grid, cr, &slices.back(), m_impl->ctl().cancelfn); + sup_slicer.slice(grid, cr, &slices.back(), ctl().cancelfn); } if (!pad_mesh.empty()) { @@ -2617,12 +69,13 @@ std::vector SLASupportTree::slice( auto bb = pad_mesh.bounding_box(); auto maxzit = std::upper_bound(grid.begin(), grid.end(), bb.max.z()); - - auto padgrid = reserve_vector(grid.end() - maxzit); + + auto cap = grid.end() - maxzit; + auto padgrid = reserve_vector(size_t(cap > 0 ? cap : 0)); std::copy(grid.begin(), maxzit, std::back_inserter(padgrid)); TriangleMeshSlicer pad_slicer(&pad_mesh); - pad_slicer.slice(padgrid, cr, &slices.back(), m_impl->ctl().cancelfn); + pad_slicer.slice(padgrid, cr, &slices.back(), ctl().cancelfn); } size_t len = grid.size(); @@ -2644,33 +97,20 @@ std::vector SLASupportTree::slice( return mrg; } -const TriangleMesh &SLASupportTree::add_pad(const ExPolygons& modelbase, - const PoolConfig& pcfg) const +SupportTree::UPtr SupportTree::create(const SupportableMesh &sm, + const JobController & ctl) { - return m_impl->create_pad(merged_mesh(), modelbase, pcfg).tmesh; + auto builder = make_unique(); + builder->m_ctl = ctl; + + if (sm.cfg.enabled) { + builder->build(sm); + builder->merge_and_cleanup(); // clean metadata, leave only the meshes. + } else { + builder->ground_level = sm.emesh.ground_level(); + } + + return std::move(builder); } -const TriangleMesh &SLASupportTree::get_pad() const -{ - return m_impl->pad().tmesh; -} - -void SLASupportTree::remove_pad() -{ - m_impl->remove_pad(); -} - -SLASupportTree::SLASupportTree(const std::vector &points, - const EigenMesh3D& emesh, - const SupportConfig &cfg, - const Controller &ctl): - m_impl(new Impl(ctl)) -{ - m_impl->ground_level = emesh.ground_level() - cfg.object_elevation_mm; - generate(points, emesh, cfg, ctl); -} - -SLASupportTree::~SLASupportTree() {} - -} -} +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SLASupportTree.hpp b/src/libslic3r/SLA/SLASupportTree.hpp index d7f15c17b..322b29251 100644 --- a/src/libslic3r/SLA/SLASupportTree.hpp +++ b/src/libslic3r/SLA/SLASupportTree.hpp @@ -2,24 +2,14 @@ #define SLASUPPORTTREE_HPP #include -#include -#include #include #include #include "SLACommon.hpp" - +#include "SLAPad.hpp" namespace Slic3r { -// Needed types from Point.hpp -typedef int32_t coord_t; -typedef Eigen::Matrix Vec3d; -typedef Eigen::Matrix Vec3f; -typedef Eigen::Matrix Vec3crd; -typedef std::vector Pointf3s; -typedef std::vector Points3; - class TriangleMesh; class Model; class ModelInstance; @@ -32,13 +22,17 @@ using ExPolygons = std::vector; namespace sla { -enum class PillarConnectionMode { +enum class PillarConnectionMode +{ zigzag, cross, dynamic }; -struct SupportConfig { +struct SupportConfig +{ + bool enabled = true; + // Radius in mm of the pointing side of the head. double head_front_radius_mm = 0.2; @@ -85,6 +79,11 @@ struct SupportConfig { // The shortest distance between a pillar base perimeter from the model // body. This is only useful when elevation is set to zero. double pillar_base_safety_distance_mm = 0.5; + + double head_fullwidth() const { + return 2 * head_front_radius_mm + head_width_mm + + 2 * head_back_radius_mm - head_penetration_mm; + } // ///////////////////////////////////////////////////////////////////////// // Compile time configuration values (candidates for runtime) @@ -104,101 +103,78 @@ struct SupportConfig { static const unsigned max_bridges_on_pillar; }; -struct PoolConfig; +enum class MeshType { Support, Pad }; /// A Control structure for the support calculation. Consists of the status /// indicator callback and the stop condition predicate. -struct Controller { - +struct JobController +{ + using StatusFn = std::function; + using StopCond = std::function; + using CancelFn = std::function; + // This will signal the status of the calculation to the front-end - std::function statuscb = - [](unsigned, const std::string&){}; - + StatusFn statuscb = [](unsigned, const std::string&){}; + // Returns true if the calculation should be aborted. - std::function stopcondition = [](){ return false; }; - + StopCond stopcondition = [](){ return false; }; + // Similar to cancel callback. This should check the stop condition and // if true, throw an appropriate exception. (TriangleMeshSlicer needs this) // consider it a hard abort. stopcondition is permits the algorithm to // terminate itself - std::function cancelfn = [](){}; + CancelFn cancelfn = [](){}; }; -using PointSet = Eigen::MatrixXd; +struct SupportableMesh +{ + EigenMesh3D emesh; + SupportPoints pts; + SupportConfig cfg; -//EigenMesh3D to_eigenmesh(const TriangleMesh& m); - -// needed for find best rotation -//EigenMesh3D to_eigenmesh(const ModelObject& model); - -// Simple conversion of 'vector of points' to an Eigen matrix -//PointSet to_point_set(const std::vector&); - - -/* ************************************************************************** */ + explicit SupportableMesh(const TriangleMesh & trmsh, + const SupportPoints &sp, + const SupportConfig &c) + : emesh{trmsh}, pts{sp}, cfg{c} + {} + + explicit SupportableMesh(const EigenMesh3D &em, + const SupportPoints &sp, + const SupportConfig &c) + : emesh{em}, pts{sp}, cfg{c} + {} +}; /// The class containing mesh data for the generated supports. -class SLASupportTree { - class Impl; // persistent support data - std::unique_ptr m_impl; - - Impl& get() { return *m_impl; } - const Impl& get() const { return *m_impl; } - - friend void add_sla_supports(Model&, - const SupportConfig&, - const Controller&); - - // The generation algorithm is quite long and will be captured in a separate - // class with private data, helper methods, etc... This data is only needed - // during the calculation whereas the Impl class contains the persistent - // data, mostly the meshes. - class Algorithm; - - // Generate the 3D supports for a model intended for SLA print. This - // will instantiate the Algorithm class and call its appropriate methods - // with status indication. - bool generate(const std::vector& pts, - const EigenMesh3D& mesh, - const SupportConfig& cfg = {}, - const Controller& ctl = {}); - +class SupportTree +{ + JobController m_ctl; public: - - SLASupportTree(double ground_level = 0.0); - - SLASupportTree(const std::vector& pts, - const EigenMesh3D& em, - const SupportConfig& cfg = {}, - const Controller& ctl = {}); + using UPtr = std::unique_ptr; - SLASupportTree(const SLASupportTree&) = delete; - SLASupportTree& operator=(const SLASupportTree&) = delete; + static UPtr create(const SupportableMesh &input, + const JobController &ctl = {}); - ~SLASupportTree(); + virtual ~SupportTree() = default; - /// Get the whole mesh united into the output TriangleMesh - /// WITHOUT THE PAD - const TriangleMesh& merged_mesh() const; + virtual const TriangleMesh &retrieve_mesh(MeshType meshtype) const = 0; - void merged_mesh_with_pad(TriangleMesh&) const; - - std::vector slice(const std::vector &, - float closing_radius) const; - - /// Adding the "pad" (base pool) under the supports + /// Adding the "pad" under the supports. /// modelbase will be used according to the embed_object flag in PoolConfig. - /// If set, the plate will interpreted as the model's intrinsic pad. + /// If set, the plate will be interpreted as the model's intrinsic pad. /// Otherwise, the modelbase will be unified with the base plate calculated /// from the supports. - const TriangleMesh& add_pad(const ExPolygons& modelbase, - const PoolConfig& pcfg) const; - - /// Get the pad geometry - const TriangleMesh& get_pad() const; - - void remove_pad(); - + virtual const TriangleMesh &add_pad(const ExPolygons &modelbase, + const PadConfig & pcfg) = 0; + + virtual void remove_pad() = 0; + + std::vector slice(const std::vector &, + float closing_radius) const; + + void retrieve_full_mesh(TriangleMesh &outmesh) const; + + const JobController &ctl() const { return m_ctl; } }; } diff --git a/src/libslic3r/SLA/SLASupportTreeBuilder.cpp b/src/libslic3r/SLA/SLASupportTreeBuilder.cpp new file mode 100644 index 000000000..2e0310ed8 --- /dev/null +++ b/src/libslic3r/SLA/SLASupportTreeBuilder.cpp @@ -0,0 +1,525 @@ +#include "SLASupportTreeBuilder.hpp" +#include "SLASupportTreeBuildsteps.hpp" + +namespace Slic3r { +namespace sla { + +Contour3D sphere(double rho, Portion portion, double fa) { + + Contour3D ret; + + // prohibit close to zero radius + if(rho <= 1e-6 && rho >= -1e-6) return ret; + + auto& vertices = ret.points; + auto& facets = ret.indices; + + // Algorithm: + // Add points one-by-one to the sphere grid and form facets using relative + // coordinates. Sphere is composed effectively of a mesh of stacked circles. + + // adjust via rounding to get an even multiple for any provided angle. + double angle = (2*PI / floor(2*PI / fa)); + + // Ring to be scaled to generate the steps of the sphere + std::vector ring; + + for (double i = 0; i < 2*PI; i+=angle) ring.emplace_back(i); + + const auto sbegin = size_t(2*std::get<0>(portion)/angle); + const auto send = size_t(2*std::get<1>(portion)/angle); + + const size_t steps = ring.size(); + const double increment = 1.0 / double(steps); + + // special case: first ring connects to 0,0,0 + // insert and form facets. + if(sbegin == 0) + vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*sbegin*2.0*rho)); + + auto id = coord_t(vertices.size()); + for (size_t i = 0; i < ring.size(); i++) { + // Fixed scaling + const double z = -rho + increment*rho*2.0 * (sbegin + 1.0); + // radius of the circle for this step. + const double r = std::sqrt(std::abs(rho*rho - z*z)); + Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); + vertices.emplace_back(Vec3d(b(0), b(1), z)); + + if (sbegin == 0) + facets.emplace_back((i == 0) ? + Vec3crd(coord_t(ring.size()), 0, 1) : + Vec3crd(id - 1, 0, id)); + ++id; + } + + // General case: insert and form facets for each step, + // joining it to the ring below it. + for (size_t s = sbegin + 2; s < send - 1; s++) { + const double z = -rho + increment*double(s*2.0*rho); + const double r = std::sqrt(std::abs(rho*rho - z*z)); + + for (size_t i = 0; i < ring.size(); i++) { + Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); + vertices.emplace_back(Vec3d(b(0), b(1), z)); + auto id_ringsize = coord_t(id - int(ring.size())); + if (i == 0) { + // wrap around + facets.emplace_back(Vec3crd(id - 1, id, + id + coord_t(ring.size() - 1))); + facets.emplace_back(Vec3crd(id - 1, id_ringsize, id)); + } else { + facets.emplace_back(Vec3crd(id_ringsize - 1, id_ringsize, id)); + facets.emplace_back(Vec3crd(id - 1, id_ringsize - 1, id)); + } + id++; + } + } + + // special case: last ring connects to 0,0,rho*2.0 + // only form facets. + if(send >= size_t(2*PI / angle)) { + vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*send*2.0*rho)); + for (size_t i = 0; i < ring.size(); i++) { + auto id_ringsize = coord_t(id - int(ring.size())); + if (i == 0) { + // third vertex is on the other side of the ring. + facets.emplace_back(Vec3crd(id - 1, id_ringsize, id)); + } else { + auto ci = coord_t(id_ringsize + coord_t(i)); + facets.emplace_back(Vec3crd(ci - 1, ci, id)); + } + } + } + id++; + + return ret; +} + +Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp) +{ + Contour3D ret; + + auto steps = int(ssteps); + auto& points = ret.points; + auto& indices = ret.indices; + points.reserve(2*ssteps); + double a = 2*PI/steps; + + Vec3d jp = sp; + Vec3d endp = {sp(X), sp(Y), sp(Z) + h}; + + // Upper circle points + for(int i = 0; i < steps; ++i) { + double phi = i*a; + double ex = endp(X) + r*std::cos(phi); + double ey = endp(Y) + r*std::sin(phi); + points.emplace_back(ex, ey, endp(Z)); + } + + // Lower circle points + for(int i = 0; i < steps; ++i) { + double phi = i*a; + double x = jp(X) + r*std::cos(phi); + double y = jp(Y) + r*std::sin(phi); + points.emplace_back(x, y, jp(Z)); + } + + // Now create long triangles connecting upper and lower circles + indices.reserve(2*ssteps); + auto offs = steps; + for(int i = 0; i < steps - 1; ++i) { + indices.emplace_back(i, i + offs, offs + i + 1); + indices.emplace_back(i, offs + i + 1, i + 1); + } + + // Last triangle connecting the first and last vertices + auto last = steps - 1; + indices.emplace_back(0, last, offs); + indices.emplace_back(last, offs + last, offs); + + // According to the slicing algorithms, we need to aid them with generating + // a watertight body. So we create a triangle fan for the upper and lower + // ending of the cylinder to close the geometry. + points.emplace_back(jp); int ci = int(points.size() - 1); + for(int i = 0; i < steps - 1; ++i) + indices.emplace_back(i + offs + 1, i + offs, ci); + + indices.emplace_back(offs, steps + offs - 1, ci); + + points.emplace_back(endp); ci = int(points.size() - 1); + for(int i = 0; i < steps - 1; ++i) + indices.emplace_back(ci, i, i + 1); + + indices.emplace_back(steps - 1, 0, ci); + + return ret; +} + +Head::Head(double r_big_mm, + double r_small_mm, + double length_mm, + double penetration, + const Vec3d &direction, + const Vec3d &offset, + const size_t circlesteps) + : steps(circlesteps) + , dir(direction) + , tr(offset) + , r_back_mm(r_big_mm) + , r_pin_mm(r_small_mm) + , width_mm(length_mm) + , penetration_mm(penetration) +{ + assert(width_mm > 0.); + assert(r_back_mm > 0.); + assert(r_pin_mm > 0.); + + // We create two spheres which will be connected with a robe that fits + // both circles perfectly. + + // Set up the model detail level + const double detail = 2*PI/steps; + + // We don't generate whole circles. Instead, we generate only the + // portions which are visible (not covered by the robe) To know the + // exact portion of the bottom and top circles we need to use some + // rules of tangent circles from which we can derive (using simple + // triangles the following relations: + + // The height of the whole mesh + const double h = r_big_mm + r_small_mm + width_mm; + double phi = PI/2 - std::acos( (r_big_mm - r_small_mm) / h ); + + // To generate a whole circle we would pass a portion of (0, Pi) + // To generate only a half horizontal circle we can pass (0, Pi/2) + // The calculated phi is an offset to the half circles needed to smooth + // the transition from the circle to the robe geometry + + auto&& s1 = sphere(r_big_mm, make_portion(0, PI/2 + phi), detail); + auto&& s2 = sphere(r_small_mm, make_portion(PI/2 + phi, PI), detail); + + for(auto& p : s2.points) p.z() += h; + + mesh.merge(s1); + mesh.merge(s2); + + for(size_t idx1 = s1.points.size() - steps, idx2 = s1.points.size(); + idx1 < s1.points.size() - 1; + idx1++, idx2++) + { + coord_t i1s1 = coord_t(idx1), i1s2 = coord_t(idx2); + coord_t i2s1 = i1s1 + 1, i2s2 = i1s2 + 1; + + mesh.indices.emplace_back(i1s1, i2s1, i2s2); + mesh.indices.emplace_back(i1s1, i2s2, i1s2); + } + + auto i1s1 = coord_t(s1.points.size()) - coord_t(steps); + auto i2s1 = coord_t(s1.points.size()) - 1; + auto i1s2 = coord_t(s1.points.size()); + auto i2s2 = coord_t(s1.points.size()) + coord_t(steps) - 1; + + mesh.indices.emplace_back(i2s2, i2s1, i1s1); + mesh.indices.emplace_back(i1s2, i2s2, i1s1); + + // To simplify further processing, we translate the mesh so that the + // last vertex of the pointing sphere (the pinpoint) will be at (0,0,0) + for(auto& p : mesh.points) p.z() -= (h + r_small_mm - penetration_mm); +} + +Pillar::Pillar(const Vec3d &jp, const Vec3d &endp, double radius, size_t st): + r(radius), steps(st), endpt(endp), starts_from_head(false) +{ + assert(steps > 0); + + height = jp(Z) - endp(Z); + if(height > EPSILON) { // Endpoint is below the starting point + + // We just create a bridge geometry with the pillar parameters and + // move the data. + Contour3D body = cylinder(radius, height, st, endp); + mesh.points.swap(body.points); + mesh.indices.swap(body.indices); + } +} + +Pillar &Pillar::add_base(double baseheight, double radius) +{ + if(baseheight <= 0) return *this; + if(baseheight > height) baseheight = height; + + assert(steps >= 0); + auto last = int(steps - 1); + + if(radius < r ) radius = r; + + double a = 2*PI/steps; + double z = endpt(Z) + baseheight; + + for(size_t i = 0; i < steps; ++i) { + double phi = i*a; + double x = endpt(X) + r*std::cos(phi); + double y = endpt(Y) + r*std::sin(phi); + base.points.emplace_back(x, y, z); + } + + for(size_t i = 0; i < steps; ++i) { + double phi = i*a; + double x = endpt(X) + radius*std::cos(phi); + double y = endpt(Y) + radius*std::sin(phi); + base.points.emplace_back(x, y, z - baseheight); + } + + auto ep = endpt; ep(Z) += baseheight; + base.points.emplace_back(endpt); + base.points.emplace_back(ep); + + auto& indices = base.indices; + auto hcenter = int(base.points.size() - 1); + auto lcenter = int(base.points.size() - 2); + auto offs = int(steps); + for(int i = 0; i < last; ++i) { + indices.emplace_back(i, i + offs, offs + i + 1); + indices.emplace_back(i, offs + i + 1, i + 1); + indices.emplace_back(i, i + 1, hcenter); + indices.emplace_back(lcenter, offs + i + 1, offs + i); + } + + indices.emplace_back(0, last, offs); + indices.emplace_back(last, offs + last, offs); + indices.emplace_back(hcenter, last, 0); + indices.emplace_back(offs, offs + last, lcenter); + return *this; +} + +Bridge::Bridge(const Vec3d &j1, const Vec3d &j2, double r_mm, size_t steps): + r(r_mm), startp(j1), endp(j2) +{ + using Quaternion = Eigen::Quaternion; + Vec3d dir = (j2 - j1).normalized(); + double d = distance(j2, j1); + + mesh = cylinder(r, d, steps); + + auto quater = Quaternion::FromTwoVectors(Vec3d{0,0,1}, dir); + for(auto& p : mesh.points) p = quater * p + j1; +} + +CompactBridge::CompactBridge(const Vec3d &sp, + const Vec3d &ep, + const Vec3d &n, + double r, + bool endball, + size_t steps) +{ + Vec3d startp = sp + r * n; + Vec3d dir = (ep - startp).normalized(); + Vec3d endp = ep - r * dir; + + Bridge br(startp, endp, r, steps); + mesh.merge(br.mesh); + + // now add the pins + double fa = 2*PI/steps; + auto upperball = sphere(r, Portion{PI / 2 - fa, PI}, fa); + for(auto& p : upperball.points) p += startp; + + if(endball) { + auto lowerball = sphere(r, Portion{0, PI/2 + 2*fa}, fa); + for(auto& p : lowerball.points) p += endp; + mesh.merge(lowerball); + } + + mesh.merge(upperball); +} + +Pad::Pad(const TriangleMesh &support_mesh, + const ExPolygons & model_contours, + double ground_level, + const PadConfig & pcfg, + ThrowOnCancel thr) + : cfg(pcfg) + , zlevel(ground_level + pcfg.full_height() - pcfg.required_elevation()) +{ + thr(); + + ExPolygons sup_contours; + + float zstart = float(zlevel); + float zend = zstart + float(pcfg.full_height() + EPSILON); + + pad_blueprint(support_mesh, sup_contours, grid(zstart, zend, 0.1f), thr); + create_pad(sup_contours, model_contours, tmesh, pcfg); + + tmesh.translate(0, 0, float(zlevel)); + if (!tmesh.empty()) tmesh.require_shared_vertices(); +} + +const TriangleMesh &SupportTreeBuilder::add_pad(const ExPolygons &modelbase, + const PadConfig & cfg) +{ + m_pad = Pad{merged_mesh(), modelbase, ground_level, cfg, ctl().cancelfn}; + return m_pad.tmesh; +} + +SupportTreeBuilder::SupportTreeBuilder(SupportTreeBuilder &&o) + : m_heads(std::move(o.m_heads)) + , m_head_indices{std::move(o.m_head_indices)} + , m_pillars{std::move(o.m_pillars)} + , m_bridges{std::move(o.m_bridges)} + , m_crossbridges{std::move(o.m_crossbridges)} + , m_compact_bridges{std::move(o.m_compact_bridges)} + , m_pad{std::move(o.m_pad)} + , m_meshcache{std::move(o.m_meshcache)} + , m_meshcache_valid{o.m_meshcache_valid} + , m_model_height{o.m_model_height} + , ground_level{o.ground_level} +{} + +SupportTreeBuilder::SupportTreeBuilder(const SupportTreeBuilder &o) + : m_heads(o.m_heads) + , m_head_indices{o.m_head_indices} + , m_pillars{o.m_pillars} + , m_bridges{o.m_bridges} + , m_crossbridges{o.m_crossbridges} + , m_compact_bridges{o.m_compact_bridges} + , m_pad{o.m_pad} + , m_meshcache{o.m_meshcache} + , m_meshcache_valid{o.m_meshcache_valid} + , m_model_height{o.m_model_height} + , ground_level{o.ground_level} +{} + +SupportTreeBuilder &SupportTreeBuilder::operator=(SupportTreeBuilder &&o) +{ + m_heads = std::move(o.m_heads); + m_head_indices = std::move(o.m_head_indices); + m_pillars = std::move(o.m_pillars); + m_bridges = std::move(o.m_bridges); + m_crossbridges = std::move(o.m_crossbridges); + m_compact_bridges = std::move(o.m_compact_bridges); + m_pad = std::move(o.m_pad); + m_meshcache = std::move(o.m_meshcache); + m_meshcache_valid = o.m_meshcache_valid; + m_model_height = o.m_model_height; + ground_level = o.ground_level; + return *this; +} + +SupportTreeBuilder &SupportTreeBuilder::operator=(const SupportTreeBuilder &o) +{ + m_heads = o.m_heads; + m_head_indices = o.m_head_indices; + m_pillars = o.m_pillars; + m_bridges = o.m_bridges; + m_crossbridges = o.m_crossbridges; + m_compact_bridges = o.m_compact_bridges; + m_pad = o.m_pad; + m_meshcache = o.m_meshcache; + m_meshcache_valid = o.m_meshcache_valid; + m_model_height = o.m_model_height; + ground_level = o.ground_level; + return *this; +} + +const TriangleMesh &SupportTreeBuilder::merged_mesh() const +{ + if (m_meshcache_valid) return m_meshcache; + + Contour3D merged; + + for (auto &head : m_heads) { + if (ctl().stopcondition()) break; + if (head.is_valid()) merged.merge(head.mesh); + } + + for (auto &stick : m_pillars) { + if (ctl().stopcondition()) break; + merged.merge(stick.mesh); + merged.merge(stick.base); + } + + for (auto &j : m_junctions) { + if (ctl().stopcondition()) break; + merged.merge(j.mesh); + } + + for (auto &cb : m_compact_bridges) { + if (ctl().stopcondition()) break; + merged.merge(cb.mesh); + } + + for (auto &bs : m_bridges) { + if (ctl().stopcondition()) break; + merged.merge(bs.mesh); + } + + for (auto &bs : m_crossbridges) { + if (ctl().stopcondition()) break; + merged.merge(bs.mesh); + } + + if (ctl().stopcondition()) { + // In case of failure we have to return an empty mesh + m_meshcache = TriangleMesh(); + return m_meshcache; + } + + m_meshcache = mesh(merged); + + // The mesh will be passed by const-pointer to TriangleMeshSlicer, + // which will need this. + if (!m_meshcache.empty()) m_meshcache.require_shared_vertices(); + + BoundingBoxf3 &&bb = m_meshcache.bounding_box(); + m_model_height = bb.max(Z) - bb.min(Z); + + m_meshcache_valid = true; + return m_meshcache; +} + +double SupportTreeBuilder::full_height() const +{ + if (merged_mesh().empty() && !pad().empty()) + return pad().cfg.full_height(); + + double h = mesh_height(); + if (!pad().empty()) h += pad().cfg.required_elevation(); + return h; +} + +const TriangleMesh &SupportTreeBuilder::merge_and_cleanup() +{ + // in case the mesh is not generated, it should be... + auto &ret = merged_mesh(); + + // Doing clear() does not garantee to release the memory. + m_heads = {}; + m_head_indices = {}; + m_pillars = {}; + m_junctions = {}; + m_bridges = {}; + m_compact_bridges = {}; + + return ret; +} + +const TriangleMesh &SupportTreeBuilder::retrieve_mesh(MeshType meshtype) const +{ + switch(meshtype) { + case MeshType::Support: return merged_mesh(); + case MeshType::Pad: return pad().tmesh; + } + + return m_meshcache; +} + +bool SupportTreeBuilder::build(const SupportableMesh &sm) +{ + ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm; + return SupportTreeBuildsteps::execute(*this, sm); +} + +} +} diff --git a/src/libslic3r/SLA/SLASupportTreeBuilder.hpp b/src/libslic3r/SLA/SLASupportTreeBuilder.hpp new file mode 100644 index 000000000..c0d9f04c0 --- /dev/null +++ b/src/libslic3r/SLA/SLASupportTreeBuilder.hpp @@ -0,0 +1,496 @@ +#ifndef SUPPORTTREEBUILDER_HPP +#define SUPPORTTREEBUILDER_HPP + +#include "SLAConcurrency.hpp" +#include "SLABoilerPlate.hpp" +#include "SLASupportTree.hpp" +#include "SLAPad.hpp" +#include + +namespace Slic3r { +namespace sla { + +/** + * Terminology: + * + * Support point: + * The point on the model surface that needs support. + * + * Pillar: + * A thick column that spans from a support point to the ground and has + * a thick cone shaped base where it touches the ground. + * + * Ground facing support point: + * A support point that can be directly connected with the ground with a pillar + * that does not collide or cut through the model. + * + * Non ground facing support point: + * A support point that cannot be directly connected with the ground (only with + * the model surface). + * + * Head: + * The pinhead that connects to the model surface with the sharp end end + * to a pillar or bridge stick with the dull end. + * + * Headless support point: + * A support point on the model surface for which there is not enough place for + * the head. It is either in a hole or there is some barrier that would collide + * with the head geometry. The headless support point can be ground facing and + * non ground facing as well. + * + * Bridge: + * A stick that connects two pillars or a head with a pillar. + * + * Junction: + * A small ball in the intersection of two or more sticks (pillar, bridge, ...) + * + * CompactBridge: + * A bridge that connects a headless support point with the model surface or a + * nearby pillar. + */ + +using Coordf = double; +using Portion = std::tuple; + +inline Portion make_portion(double a, double b) { + return std::make_tuple(a, b); +} + +template double distance(const Vec& p) { + return std::sqrt(p.transpose() * p); +} + +template double distance(const Vec& pp1, const Vec& pp2) { + auto p = pp2 - pp1; + return distance(p); +} + +Contour3D sphere(double rho, Portion portion = make_portion(0.0, 2.0*PI), + double fa=(2*PI/360)); + +// Down facing cylinder in Z direction with arguments: +// r: radius +// h: Height +// ssteps: how many edges will create the base circle +// sp: starting point +Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp = {0,0,0}); + +const constexpr long ID_UNSET = -1; + +struct Head { + Contour3D mesh; + + size_t steps = 45; + Vec3d dir = {0, 0, -1}; + Vec3d tr = {0, 0, 0}; + + double r_back_mm = 1; + double r_pin_mm = 0.5; + double width_mm = 2; + double penetration_mm = 0.5; + + // For identification purposes. This will be used as the index into the + // container holding the head structures. See SLASupportTree::Impl + long id = ID_UNSET; + + // If there is a pillar connecting to this head, then the id will be set. + long pillar_id = ID_UNSET; + + long bridge_id = ID_UNSET; + + inline void invalidate() { id = ID_UNSET; } + inline bool is_valid() const { return id >= 0; } + + Head(double r_big_mm, + double r_small_mm, + double length_mm, + double penetration, + const Vec3d &direction = {0, 0, -1}, // direction (normal to the dull end) + const Vec3d &offset = {0, 0, 0}, // displacement + const size_t circlesteps = 45); + + void transform() + { + using Quaternion = Eigen::Quaternion; + + // We rotate the head to the specified direction The head's pointing + // side is facing upwards so this means that it would hold a support + // point with a normal pointing straight down. This is the reason of + // the -1 z coordinate + auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, -1}, dir); + + for(auto& p : mesh.points) p = quatern * p + tr; + } + + inline double fullwidth() const + { + return 2 * r_pin_mm + width_mm + 2*r_back_mm - penetration_mm; + } + + inline Vec3d junction_point() const + { + return tr + ( 2 * r_pin_mm + width_mm + r_back_mm - penetration_mm)*dir; + } + + inline double request_pillar_radius(double radius) const + { + const double rmax = r_back_mm; + return radius > 0 && radius < rmax ? radius : rmax; + } +}; + +struct Junction { + Contour3D mesh; + double r = 1; + size_t steps = 45; + Vec3d pos; + + long id = ID_UNSET; + + Junction(const Vec3d& tr, double r_mm, size_t stepnum = 45): + r(r_mm), steps(stepnum), pos(tr) + { + mesh = sphere(r_mm, make_portion(0, PI), 2*PI/steps); + for(auto& p : mesh.points) p += tr; + } +}; + +struct Pillar { + Contour3D mesh; + Contour3D base; + double r = 1; + size_t steps = 0; + Vec3d endpt; + double height = 0; + + long id = ID_UNSET; + + // If the pillar connects to a head, this is the id of that head + bool starts_from_head = true; // Could start from a junction as well + long start_junction_id = ID_UNSET; + + // How many bridges are connected to this pillar + unsigned bridges = 0; + + // How many pillars are cascaded with this one + unsigned links = 0; + + Pillar(const Vec3d& jp, const Vec3d& endp, + double radius = 1, size_t st = 45); + + Pillar(const Junction &junc, const Vec3d &endp) + : Pillar(junc.pos, endp, junc.r, junc.steps) + {} + + Pillar(const Head &head, const Vec3d &endp, double radius = 1) + : Pillar(head.junction_point(), endp, + head.request_pillar_radius(radius), head.steps) + {} + + inline Vec3d startpoint() const + { + return {endpt(X), endpt(Y), endpt(Z) + height}; + } + + inline const Vec3d& endpoint() const { return endpt; } + + Pillar& add_base(double baseheight = 3, double radius = 2); +}; + +// A Bridge between two pillars (with junction endpoints) +struct Bridge { + Contour3D mesh; + double r = 0.8; + long id = ID_UNSET; + Vec3d startp = Vec3d::Zero(), endp = Vec3d::Zero(); + + Bridge(const Vec3d &j1, + const Vec3d &j2, + double r_mm = 0.8, + size_t steps = 45); +}; + +// A bridge that spans from model surface to model surface with small connecting +// edges on the endpoints. Used for headless support points. +struct CompactBridge { + Contour3D mesh; + long id = ID_UNSET; + + CompactBridge(const Vec3d& sp, + const Vec3d& ep, + const Vec3d& n, + double r, + bool endball = true, + size_t steps = 45); +}; + +// A wrapper struct around the pad +struct Pad { + TriangleMesh tmesh; + PadConfig cfg; + double zlevel = 0; + + Pad() = default; + + Pad(const TriangleMesh &support_mesh, + const ExPolygons & model_contours, + double ground_level, + const PadConfig & pcfg, + ThrowOnCancel thr); + + bool empty() const { return tmesh.facets_count() == 0; } +}; + +// This class will hold the support tree meshes with some additional +// bookkeeping as well. Various parts of the support geometry are stored +// separately and are merged when the caller queries the merged mesh. The +// merged result is cached for fast subsequent delivery of the merged mesh +// which can be quite complex. The support tree creation algorithm can use an +// instance of this class as a somewhat higher level tool for crafting the 3D +// support mesh. Parts can be added with the appropriate methods such as +// add_head or add_pillar which forwards the constructor arguments and fills +// the IDs of these substructures. The IDs are basically indices into the +// arrays of the appropriate type (heads, pillars, etc...). One can later query +// e.g. a pillar for a specific head... +// +// The support pad is considered an auxiliary geometry and is not part of the +// merged mesh. It can be retrieved using a dedicated method (pad()) +class SupportTreeBuilder: public SupportTree { + // For heads it is beneficial to use the same IDs as for the support points. + std::vector m_heads; + std::vector m_head_indices; + std::vector m_pillars; + std::vector m_junctions; + std::vector m_bridges; + std::vector m_crossbridges; + std::vector m_compact_bridges; + Pad m_pad; + + using Mutex = ccr::SpinningMutex; + + mutable TriangleMesh m_meshcache; + mutable Mutex m_mutex; + mutable bool m_meshcache_valid = false; + mutable double m_model_height = 0; // the full height of the model + + template + const Bridge& _add_bridge(std::vector &br, Args&&... args) + { + std::lock_guard lk(m_mutex); + br.emplace_back(std::forward(args)...); + br.back().id = long(br.size() - 1); + m_meshcache_valid = false; + return br.back(); + } + +public: + double ground_level = 0; + + SupportTreeBuilder() = default; + SupportTreeBuilder(SupportTreeBuilder &&o); + SupportTreeBuilder(const SupportTreeBuilder &o); + SupportTreeBuilder& operator=(SupportTreeBuilder &&o); + SupportTreeBuilder& operator=(const SupportTreeBuilder &o); + + template Head& add_head(unsigned id, Args&&... args) + { + std::lock_guard lk(m_mutex); + m_heads.emplace_back(std::forward(args)...); + m_heads.back().id = id; + + if (id >= m_head_indices.size()) m_head_indices.resize(id + 1); + m_head_indices[id] = m_heads.size() - 1; + + m_meshcache_valid = false; + return m_heads.back(); + } + + template long add_pillar(long headid, Args&&... args) + { + std::lock_guard lk(m_mutex); + if (m_pillars.capacity() < m_heads.size()) + m_pillars.reserve(m_heads.size() * 10); + + assert(headid >= 0 && size_t(headid) < m_head_indices.size()); + Head &head = m_heads[m_head_indices[size_t(headid)]]; + + m_pillars.emplace_back(head, std::forward(args)...); + Pillar& pillar = m_pillars.back(); + pillar.id = long(m_pillars.size() - 1); + head.pillar_id = pillar.id; + pillar.start_junction_id = head.id; + pillar.starts_from_head = true; + + m_meshcache_valid = false; + return pillar.id; + } + + void add_pillar_base(long pid, double baseheight = 3, double radius = 2) + { + std::lock_guard lk(m_mutex); + assert(pid >= 0 && size_t(pid) < m_pillars.size()); + m_pillars[size_t(pid)].add_base(baseheight, radius); + } + + void increment_bridges(const Pillar& pillar) + { + std::lock_guard lk(m_mutex); + assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()); + + if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()) + m_pillars[size_t(pillar.id)].bridges++; + } + + void increment_links(const Pillar& pillar) + { + std::lock_guard lk(m_mutex); + assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()); + + if(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()) + m_pillars[size_t(pillar.id)].links++; + } + + unsigned bridgecount(const Pillar &pillar) const { + std::lock_guard lk(m_mutex); + assert(pillar.id >= 0 && size_t(pillar.id) < m_pillars.size()); + return pillar.bridges; + } + + template long add_pillar(Args&&...args) + { + std::lock_guard lk(m_mutex); + if (m_pillars.capacity() < m_heads.size()) + m_pillars.reserve(m_heads.size() * 10); + + m_pillars.emplace_back(std::forward(args)...); + Pillar& pillar = m_pillars.back(); + pillar.id = long(m_pillars.size() - 1); + pillar.starts_from_head = false; + m_meshcache_valid = false; + return pillar.id; + } + + const Pillar& head_pillar(unsigned headid) const + { + std::lock_guard lk(m_mutex); + assert(headid < m_head_indices.size()); + + const Head& h = m_heads[m_head_indices[headid]]; + assert(h.pillar_id >= 0 && h.pillar_id < long(m_pillars.size())); + + return m_pillars[size_t(h.pillar_id)]; + } + + template const Junction& add_junction(Args&&... args) + { + std::lock_guard lk(m_mutex); + m_junctions.emplace_back(std::forward(args)...); + m_junctions.back().id = long(m_junctions.size() - 1); + m_meshcache_valid = false; + return m_junctions.back(); + } + + const Bridge& add_bridge(const Vec3d &s, const Vec3d &e, double r, size_t n = 45) + { + return _add_bridge(m_bridges, s, e, r, n); + } + + const Bridge& add_bridge(long headid, const Vec3d &endp, size_t s = 45) + { + std::lock_guard lk(m_mutex); + assert(headid >= 0 && size_t(headid) < m_head_indices.size()); + + Head &h = m_heads[m_head_indices[size_t(headid)]]; + m_bridges.emplace_back(h.junction_point(), endp, h.r_back_mm, s); + m_bridges.back().id = long(m_bridges.size() - 1); + + h.bridge_id = m_bridges.back().id; + m_meshcache_valid = false; + return m_bridges.back(); + } + + template const Bridge& add_crossbridge(Args&&... args) + { + return _add_bridge(m_crossbridges, std::forward(args)...); + } + + template const CompactBridge& add_compact_bridge(Args&&...args) + { + std::lock_guard lk(m_mutex); + m_compact_bridges.emplace_back(std::forward(args)...); + m_compact_bridges.back().id = long(m_compact_bridges.size() - 1); + m_meshcache_valid = false; + return m_compact_bridges.back(); + } + + Head &head(unsigned id) + { + std::lock_guard lk(m_mutex); + assert(id < m_head_indices.size()); + + m_meshcache_valid = false; + return m_heads[m_head_indices[id]]; + } + + inline size_t pillarcount() const { + std::lock_guard lk(m_mutex); + return m_pillars.size(); + } + + inline const std::vector &pillars() const { return m_pillars; } + inline const std::vector &heads() const { return m_heads; } + inline const std::vector &bridges() const { return m_bridges; } + inline const std::vector &crossbridges() const { return m_crossbridges; } + + template inline IntegerOnly pillar(T id) const + { + std::lock_guard lk(m_mutex); + assert(id >= 0 && size_t(id) < m_pillars.size() && + size_t(id) < std::numeric_limits::max()); + + return m_pillars[size_t(id)]; + } + + template inline IntegerOnly pillar(T id) + { + std::lock_guard lk(m_mutex); + assert(id >= 0 && size_t(id) < m_pillars.size() && + size_t(id) < std::numeric_limits::max()); + + return m_pillars[size_t(id)]; + } + + const Pad& pad() const { return m_pad; } + + // WITHOUT THE PAD!!! + const TriangleMesh &merged_mesh() const; + + // WITH THE PAD + double full_height() const; + + // WITHOUT THE PAD!!! + inline double mesh_height() const + { + if (!m_meshcache_valid) merged_mesh(); + return m_model_height; + } + + // Intended to be called after the generation is fully complete + const TriangleMesh & merge_and_cleanup(); + + // Implement SupportTree interface: + + const TriangleMesh &add_pad(const ExPolygons &modelbase, + const PadConfig & pcfg) override; + + void remove_pad() override { m_pad = Pad(); } + + virtual const TriangleMesh &retrieve_mesh( + MeshType meshtype = MeshType::Support) const override; + + bool build(const SupportableMesh &supportable_mesh); +}; + +}} // namespace Slic3r::sla + +#endif // SUPPORTTREEBUILDER_HPP diff --git a/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp b/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp new file mode 100644 index 000000000..392a963e1 --- /dev/null +++ b/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp @@ -0,0 +1,1387 @@ +#include "SLASupportTreeBuildsteps.hpp" + +#include +#include +#include + +namespace Slic3r { +namespace sla { + +SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder, + const SupportableMesh &sm) + : m_cfg(sm.cfg) + , m_mesh(sm.emesh) + , m_support_pts(sm.pts) + , m_support_nmls(sm.pts.size(), 3) + , m_builder(builder) + , m_points(sm.pts.size(), 3) + , m_thr(builder.ctl().cancelfn) +{ + // Prepare the support points in Eigen/IGL format as well, we will use + // it mostly in this form. + + long i = 0; + for (const SupportPoint &sp : m_support_pts) { + m_points.row(i)(X) = double(sp.pos(X)); + m_points.row(i)(Y) = double(sp.pos(Y)); + m_points.row(i)(Z) = double(sp.pos(Z)); + ++i; + } +} + +bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, + const SupportableMesh &sm) +{ + if(sm.pts.empty()) return false; + + SupportTreeBuildsteps alg(builder, sm); + + // Let's define the individual steps of the processing. We can experiment + // later with the ordering and the dependencies between them. + enum Steps { + BEGIN, + FILTER, + PINHEADS, + CLASSIFY, + ROUTING_GROUND, + ROUTING_NONGROUND, + CASCADE_PILLARS, + HEADLESS, + MERGE_RESULT, + DONE, + ABORT, + NUM_STEPS + //... + }; + + // Collect the algorithm steps into a nice sequence + std::array, NUM_STEPS> program = { + [] () { + // Begin... + // Potentially clear up the shared data (not needed for now) + }, + + std::bind(&SupportTreeBuildsteps::filter, &alg), + + std::bind(&SupportTreeBuildsteps::add_pinheads, &alg), + + std::bind(&SupportTreeBuildsteps::classify, &alg), + + std::bind(&SupportTreeBuildsteps::routing_to_ground, &alg), + + std::bind(&SupportTreeBuildsteps::routing_to_model, &alg), + + std::bind(&SupportTreeBuildsteps::interconnect_pillars, &alg), + + std::bind(&SupportTreeBuildsteps::routing_headless, &alg), + + std::bind(&SupportTreeBuildsteps::merge_result, &alg), + + [] () { + // Done + }, + + [] () { + // Abort + } + }; + + Steps pc = BEGIN; + + if(sm.cfg.ground_facing_only) { + program[ROUTING_NONGROUND] = []() { + BOOST_LOG_TRIVIAL(info) + << "Skipping model-facing supports as requested."; + }; + program[HEADLESS] = []() { + BOOST_LOG_TRIVIAL(info) << "Skipping headless stick generation as" + " requested."; + }; + } + + // Let's define a simple automaton that will run our program. + auto progress = [&builder, &pc] () { + static const std::array stepstr { + "Starting", + "Filtering", + "Generate pinheads", + "Classification", + "Routing to ground", + "Routing supports to model surface", + "Interconnecting pillars", + "Processing small holes", + "Merging support mesh", + "Done", + "Abort" + }; + + static const std::array stepstate { + 0, + 10, + 30, + 50, + 60, + 70, + 80, + 85, + 99, + 100, + 0 + }; + + if(builder.ctl().stopcondition()) pc = ABORT; + + switch(pc) { + case BEGIN: pc = FILTER; break; + case FILTER: pc = PINHEADS; break; + case PINHEADS: pc = CLASSIFY; break; + case CLASSIFY: pc = ROUTING_GROUND; break; + case ROUTING_GROUND: pc = ROUTING_NONGROUND; break; + case ROUTING_NONGROUND: pc = CASCADE_PILLARS; break; + case CASCADE_PILLARS: pc = HEADLESS; break; + case HEADLESS: pc = MERGE_RESULT; break; + case MERGE_RESULT: pc = DONE; break; + case DONE: + case ABORT: break; + default: ; + } + + builder.ctl().statuscb(stepstate[pc], stepstr[pc]); + }; + + // Just here we run the computation... + while(pc < DONE) { + progress(); + program[pc](); + } + + return pc == ABORT; +} + +EigenMesh3D::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect( + const Vec3d &s, const Vec3d &dir, double r_pin, double r_back, double width) +{ + static const size_t SAMPLES = 8; + + // method based on: + // https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space + + // We will shoot multiple rays from the head pinpoint in the direction + // of the pinhead robe (side) surface. The result will be the smallest + // hit distance. + + // Move away slightly from the touching point to avoid raycasting on the + // inner surface of the mesh. + Vec3d v = dir; // Our direction (axis) + Vec3d c = s + width * dir; + const double& sd = m_cfg.safety_distance_mm; + + // Two vectors that will be perpendicular to each other and to the + // axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a + // placeholder. + Vec3d a(0, 1, 0), b; + + // The portions of the circle (the head-back circle) for which we will + // shoot rays. + std::array phis; + for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size(); + + auto& m = m_mesh; + using HitResult = EigenMesh3D::hit_result; + + // Hit results + std::array hits; + + // We have to address the case when the direction vector v (same as + // dir) is coincident with one of the world axes. In this case two of + // its components will be completely zero and one is 1.0. Our method + // becomes dangerous here due to division with zero. Instead, vector + // 'a' can be an element-wise rotated version of 'v' + auto chk1 = [] (double val) { + return std::abs(std::abs(val) - 1) < 1e-20; + }; + + if(chk1(v(X)) || chk1(v(Y)) || chk1(v(Z))) { + a = {v(Z), v(X), v(Y)}; + b = {v(Y), v(Z), v(X)}; + } + else { + a(Z) = -(v(Y)*a(Y)) / v(Z); a.normalize(); + b = a.cross(v); + } + + // Now a and b vectors are perpendicular to v and to each other. + // Together they define the plane where we have to iterate with the + // given angles in the 'phis' vector + ccr::enumerate( + phis.begin(), phis.end(), + [&hits, &m, sd, r_pin, r_back, s, a, b, c](double phi, size_t i) { + double sinphi = std::sin(phi); + double cosphi = std::cos(phi); + + // Let's have a safety coefficient for the radiuses. + double rpscos = (sd + r_pin) * cosphi; + double rpssin = (sd + r_pin) * sinphi; + double rpbcos = (sd + r_back) * cosphi; + double rpbsin = (sd + r_back) * sinphi; + + // Point on the circle on the pin sphere + Vec3d ps(s(X) + rpscos * a(X) + rpssin * b(X), + s(Y) + rpscos * a(Y) + rpssin * b(Y), + s(Z) + rpscos * a(Z) + rpssin * b(Z)); + + // Point ps is not on mesh but can be inside or + // outside as well. This would cause many problems + // with ray-casting. To detect the position we will + // use the ray-casting result (which has an is_inside + // predicate). + + // This is the point on the circle on the back sphere + Vec3d p(c(X) + rpbcos * a(X) + rpbsin * b(X), + c(Y) + rpbcos * a(Y) + rpbsin * b(Y), + c(Z) + rpbcos * a(Z) + rpbsin * b(Z)); + + Vec3d n = (p - ps).normalized(); + auto q = m.query_ray_hit(ps + sd * n, n); + + if (q.is_inside()) { // the hit is inside the model + if (q.distance() > r_pin + sd) { + // If we are inside the model and the hit + // distance is bigger than our pin circle + // diameter, it probably indicates that the + // support point was already inside the + // model, or there is really no space + // around the point. We will assign a zero + // hit distance to these cases which will + // enforce the function return value to be + // an invalid ray with zero hit distance. + // (see min_element at the end) + hits[i] = HitResult(0.0); + } else { + // re-cast the ray from the outside of the + // object. The starting point has an offset + // of 2*safety_distance because the + // original ray has also had an offset + auto q2 = m.query_ray_hit( + ps + (q.distance() + 2 * sd) * n, n); + hits[i] = q2; + } + } else + hits[i] = q; + }); + + auto mit = std::min_element(hits.begin(), hits.end()); + + return *mit; +} + +EigenMesh3D::hit_result SupportTreeBuildsteps::bridge_mesh_intersect( + const Vec3d &s, const Vec3d &dir, double r, bool ins_check) +{ + static const size_t SAMPLES = 8; + + // helper vector calculations + Vec3d a(0, 1, 0), b; + const double& sd = m_cfg.safety_distance_mm; + + // INFO: for explanation of the method used here, see the previous + // method's comments. + + auto chk1 = [] (double val) { + return std::abs(std::abs(val) - 1) < 1e-20; + }; + + if(chk1(dir(X)) || chk1(dir(Y)) || chk1(dir(Z))) { + a = {dir(Z), dir(X), dir(Y)}; + b = {dir(Y), dir(Z), dir(X)}; + } + else { + a(Z) = -(dir(Y)*a(Y)) / dir(Z); a.normalize(); + b = a.cross(dir); + } + + // circle portions + std::array phis; + for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size(); + + auto& m = m_mesh; + using HitResult = EigenMesh3D::hit_result; + + // Hit results + std::array hits; + + ccr::enumerate( + phis.begin(), phis.end(), + [&m, a, b, sd, dir, r, s, ins_check, &hits] (double phi, size_t i) { + double sinphi = std::sin(phi); + double cosphi = std::cos(phi); + + // Let's have a safety coefficient for the radiuses. + double rcos = (sd + r) * cosphi; + double rsin = (sd + r) * sinphi; + + // Point on the circle on the pin sphere + Vec3d p (s(X) + rcos * a(X) + rsin * b(X), + s(Y) + rcos * a(Y) + rsin * b(Y), + s(Z) + rcos * a(Z) + rsin * b(Z)); + + auto hr = m.query_ray_hit(p + sd*dir, dir); + + if(ins_check && hr.is_inside()) { + if(hr.distance() > 2 * r + sd) hits[i] = HitResult(0.0); + else { + // re-cast the ray from the outside of the object + auto hr2 = + m.query_ray_hit(p + (hr.distance() + 2*sd)*dir, dir); + + hits[i] = hr2; + } + } else hits[i] = hr; + }); + + auto mit = std::min_element(hits.begin(), hits.end()); + + return *mit; +} + +bool SupportTreeBuildsteps::interconnect(const Pillar &pillar, + const Pillar &nextpillar) +{ + // We need to get the starting point of the zig-zag pattern. We have to + // be aware that the two head junctions are at different heights. We + // may start from the lowest junction and call it a day but this + // strategy would leave unconnected a lot of pillar duos where the + // shorter pillar is too short to start a new bridge but the taller + // pillar could still be bridged with the shorter one. + bool was_connected = false; + + Vec3d supper = pillar.startpoint(); + Vec3d slower = nextpillar.startpoint(); + Vec3d eupper = pillar.endpoint(); + Vec3d elower = nextpillar.endpoint(); + + double zmin = m_builder.ground_level + m_cfg.base_height_mm; + eupper(Z) = std::max(eupper(Z), zmin); + elower(Z) = std::max(elower(Z), zmin); + + // The usable length of both pillars should be positive + if(slower(Z) - elower(Z) < 0) return false; + if(supper(Z) - eupper(Z) < 0) return false; + + double pillar_dist = distance(Vec2d{slower(X), slower(Y)}, + Vec2d{supper(X), supper(Y)}); + double bridge_distance = pillar_dist / std::cos(-m_cfg.bridge_slope); + double zstep = pillar_dist * std::tan(-m_cfg.bridge_slope); + + if(pillar_dist < 2 * m_cfg.head_back_radius_mm || + pillar_dist > m_cfg.max_pillar_link_distance_mm) return false; + + if(supper(Z) < slower(Z)) supper.swap(slower); + if(eupper(Z) < elower(Z)) eupper.swap(elower); + + double startz = 0, endz = 0; + + startz = slower(Z) - zstep < supper(Z) ? slower(Z) - zstep : slower(Z); + endz = eupper(Z) + zstep > elower(Z) ? eupper(Z) + zstep : eupper(Z); + + if(slower(Z) - eupper(Z) < std::abs(zstep)) { + // no space for even one cross + + // Get max available space + startz = std::min(supper(Z), slower(Z) - zstep); + endz = std::max(eupper(Z) + zstep, elower(Z)); + + // Align to center + double available_dist = (startz - endz); + double rounds = std::floor(available_dist / std::abs(zstep)); + startz -= 0.5 * (available_dist - rounds * std::abs(zstep)); + } + + auto pcm = m_cfg.pillar_connection_mode; + bool docrosses = + pcm == PillarConnectionMode::cross || + (pcm == PillarConnectionMode::dynamic && + pillar_dist > 2*m_cfg.base_radius_mm); + + // 'sj' means starting junction, 'ej' is the end junction of a bridge. + // They will be swapped in every iteration thus the zig-zag pattern. + // According to a config parameter, a second bridge may be added which + // results in a cross connection between the pillars. + Vec3d sj = supper, ej = slower; sj(Z) = startz; ej(Z) = sj(Z) + zstep; + + // TODO: This is a workaround to not have a faulty last bridge + while(ej(Z) >= eupper(Z) /*endz*/) { + if(bridge_mesh_intersect(sj, dirv(sj, ej), pillar.r) >= bridge_distance) + { + m_builder.add_crossbridge(sj, ej, pillar.r); + was_connected = true; + } + + // double bridging: (crosses) + if(docrosses) { + Vec3d sjback(ej(X), ej(Y), sj(Z)); + Vec3d ejback(sj(X), sj(Y), ej(Z)); + if (sjback(Z) <= slower(Z) && ejback(Z) >= eupper(Z) && + bridge_mesh_intersect(sjback, dirv(sjback, ejback), + pillar.r) >= bridge_distance) { + // need to check collision for the cross stick + m_builder.add_crossbridge(sjback, ejback, pillar.r); + was_connected = true; + } + } + + sj.swap(ej); + ej(Z) = sj(Z) + zstep; + } + + return was_connected; +} + +bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head, + long nearpillar_id) +{ + auto nearpillar = [this, nearpillar_id]() -> const Pillar& { + return m_builder.pillar(nearpillar_id); + }; + + if (m_builder.bridgecount(nearpillar()) > m_cfg.max_bridges_on_pillar) + return false; + + Vec3d headjp = head.junction_point(); + Vec3d nearjp_u = nearpillar().startpoint(); + Vec3d nearjp_l = nearpillar().endpoint(); + + double r = head.r_back_mm; + double d2d = distance(to_2d(headjp), to_2d(nearjp_u)); + double d3d = distance(headjp, nearjp_u); + + double hdiff = nearjp_u(Z) - headjp(Z); + double slope = std::atan2(hdiff, d2d); + + Vec3d bridgestart = headjp; + Vec3d bridgeend = nearjp_u; + double max_len = m_cfg.max_bridge_length_mm; + double max_slope = m_cfg.bridge_slope; + double zdiff = 0.0; + + // check the default situation if feasible for a bridge + if(d3d > max_len || slope > -max_slope) { + // not feasible to connect the two head junctions. We have to search + // for a suitable touch point. + + double Zdown = headjp(Z) + d2d * std::tan(-max_slope); + Vec3d touchjp = bridgeend; touchjp(Z) = Zdown; + double D = distance(headjp, touchjp); + zdiff = Zdown - nearjp_u(Z); + + if(zdiff > 0) { + Zdown -= zdiff; + bridgestart(Z) -= zdiff; + touchjp(Z) = Zdown; + + double t = bridge_mesh_intersect(headjp, {0,0,-1}, r); + + // We can't insert a pillar under the source head to connect + // with the nearby pillar's starting junction + if(t < zdiff) return false; + } + + if(Zdown <= nearjp_u(Z) && Zdown >= nearjp_l(Z) && D < max_len) + bridgeend(Z) = Zdown; + else + return false; + } + + // There will be a minimum distance from the ground where the + // bridge is allowed to connect. This is an empiric value. + double minz = m_builder.ground_level + 2 * m_cfg.head_width_mm; + if(bridgeend(Z) < minz) return false; + + double t = bridge_mesh_intersect(bridgestart, + dirv(bridgestart, bridgeend), r); + + // Cannot insert the bridge. (further search might not worth the hassle) + if(t < distance(bridgestart, bridgeend)) return false; + + std::lock_guard lk(m_bridge_mutex); + + if (m_builder.bridgecount(nearpillar()) < m_cfg.max_bridges_on_pillar) { + // A partial pillar is needed under the starting head. + if(zdiff > 0) { + m_builder.add_pillar(head.id, bridgestart, r); + m_builder.add_junction(bridgestart, r); + m_builder.add_bridge(bridgestart, bridgeend, head.r_back_mm); + } else { + m_builder.add_bridge(head.id, bridgeend); + } + + m_builder.increment_bridges(nearpillar()); + } else return false; + + return true; +} + +bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &head) +{ + PointIndex spindex = m_pillar_index.guarded_clone(); + + long nearest_id = ID_UNSET; + + Vec3d querypoint = head.junction_point(); + + while(nearest_id < 0 && !spindex.empty()) { m_thr(); + // loop until a suitable head is not found + // if there is a pillar closer than the cluster center + // (this may happen as the clustering is not perfect) + // than we will bridge to this closer pillar + + Vec3d qp(querypoint(X), querypoint(Y), m_builder.ground_level); + auto qres = spindex.nearest(qp, 1); + if(qres.empty()) break; + + auto ne = qres.front(); + nearest_id = ne.second; + + if(nearest_id >= 0) { + if(size_t(nearest_id) < m_builder.pillarcount()) { + if(!connect_to_nearpillar(head, nearest_id)) { + nearest_id = ID_UNSET; // continue searching + spindex.remove(ne); // without the current pillar + } + } + } + } + + return nearest_id >= 0; +} + +void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, + const Vec3d &sourcedir, + double radius, + long head_id) +{ + // People were killed for this number (seriously) + static const double SQR2 = std::sqrt(2.0); + static const Vec3d DOWN = {0.0, 0.0, -1.0}; + + double gndlvl = m_builder.ground_level; + Vec3d endp = {jp(X), jp(Y), gndlvl}; + double sd = m_cfg.pillar_base_safety_distance_mm; + long pillar_id = ID_UNSET; + double min_dist = sd + m_cfg.base_radius_mm + EPSILON; + double dist = 0; + bool can_add_base = true; + bool normal_mode = true; + + if (m_cfg.object_elevation_mm < EPSILON + && (dist = std::sqrt(m_mesh.squared_distance(endp))) < min_dist) { + // Get the distance from the mesh. This can be later optimized + // to get the distance in 2D plane because we are dealing with + // the ground level only. + + normal_mode = false; + double mind = min_dist - dist; + double azimuth = std::atan2(sourcedir(Y), sourcedir(X)); + double sinpolar = std::sin(PI - m_cfg.bridge_slope); + double cospolar = std::cos(PI - m_cfg.bridge_slope); + double cosazm = std::cos(azimuth); + double sinazm = std::sin(azimuth); + + auto dir = Vec3d(cosazm * sinpolar, sinazm * sinpolar, cospolar) + .normalized(); + + using namespace libnest2d::opt; + StopCriteria scr; + scr.stop_score = min_dist; + SubplexOptimizer solver(scr); + + auto result = solver.optimize_max( + [this, dir, jp, gndlvl](double mv) { + Vec3d endpt = jp + SQR2 * mv * dir; + endpt(Z) = gndlvl; + return std::sqrt(m_mesh.squared_distance(endpt)); + }, + initvals(mind), bound(0.0, 2 * min_dist)); + + mind = std::get<0>(result.optimum); + endp = jp + SQR2 * mind * dir; + Vec3d pgnd = {endp(X), endp(Y), gndlvl}; + can_add_base = result.score > min_dist; + + double gnd_offs = m_mesh.ground_level_offset(); + auto abort_in_shame = + [gnd_offs, &normal_mode, &can_add_base, &endp, jp, gndlvl]() + { + normal_mode = true; + can_add_base = false; // Nothing left to do, hope for the best + endp = {jp(X), jp(Y), gndlvl - gnd_offs }; + }; + + // We have to check if the bridge is feasible. + if (bridge_mesh_intersect(jp, dir, radius) < (endp - jp).norm()) + abort_in_shame(); + else { + // If the new endpoint is below ground, do not make a pillar + if (endp(Z) < gndlvl) + endp = endp - SQR2 * (gndlvl - endp(Z)) * dir; // back off + else { + + auto hit = bridge_mesh_intersect(endp, DOWN, radius); + if (!std::isinf(hit.distance())) abort_in_shame(); + + pillar_id = m_builder.add_pillar(endp, pgnd, radius); + + if (can_add_base) + m_builder.add_pillar_base(pillar_id, m_cfg.base_height_mm, + m_cfg.base_radius_mm); + } + + m_builder.add_bridge(jp, endp, radius); + m_builder.add_junction(endp, radius); + + // Add a degenerated pillar and the bridge. + // The degenerate pillar will have zero length and it will + // prevent from queries of head_pillar() to have non-existing + // pillar when the head should have one. + if (head_id >= 0) + m_builder.add_pillar(head_id, jp, radius); + } + } + + if (normal_mode) { + pillar_id = head_id >= 0 ? m_builder.add_pillar(head_id, endp, radius) : + m_builder.add_pillar(jp, endp, radius); + + if (can_add_base) + m_builder.add_pillar_base(pillar_id, m_cfg.base_height_mm, + m_cfg.base_radius_mm); + } + + if(pillar_id >= 0) // Save the pillar endpoint in the spatial index + m_pillar_index.guarded_insert(endp, unsigned(pillar_id)); +} + +void SupportTreeBuildsteps::filter() +{ + // Get the points that are too close to each other and keep only the + // first one + auto aliases = cluster(m_points, D_SP, 2); + + PtIndices filtered_indices; + filtered_indices.reserve(aliases.size()); + m_iheads.reserve(aliases.size()); + m_iheadless.reserve(aliases.size()); + for(auto& a : aliases) { + // Here we keep only the front point of the cluster. + filtered_indices.emplace_back(a.front()); + } + + // calculate the normals to the triangles for filtered points + auto nmls = sla::normals(m_points, m_mesh, m_cfg.head_front_radius_mm, + m_thr, filtered_indices); + + // Not all of the support points have to be a valid position for + // support creation. The angle may be inappropriate or there may + // not be enough space for the pinhead. Filtering is applied for + // these reasons. + + using libnest2d::opt::bound; + using libnest2d::opt::initvals; + using libnest2d::opt::GeneticOptimizer; + using libnest2d::opt::StopCriteria; + + ccr::SpinningMutex mutex; + auto addfn = [&mutex](PtIndices &container, unsigned val) { + std::lock_guard lk(mutex); + container.emplace_back(val); + }; + + auto filterfn = [this, &nmls, addfn](unsigned fidx, size_t i) { + m_thr(); + + auto n = nmls.row(Eigen::Index(i)); + + // for all normals we generate the spherical coordinates and + // saturate the polar angle to 45 degrees from the bottom then + // convert back to standard coordinates to get the new normal. + // Then we just create a quaternion from the two normals + // (Quaternion::FromTwoVectors) and apply the rotation to the + // arrow head. + + double z = n(2); + double r = 1.0; // for normalized vector + double polar = std::acos(z / r); + double azimuth = std::atan2(n(1), n(0)); + + // skip if the tilt is not sane + if(polar >= PI - m_cfg.normal_cutoff_angle) { + + // We saturate the polar angle to 3pi/4 + polar = std::max(polar, 3*PI / 4); + + // save the head (pinpoint) position + Vec3d hp = m_points.row(fidx); + + double w = m_cfg.head_width_mm + + m_cfg.head_back_radius_mm + + 2*m_cfg.head_front_radius_mm; + + double pin_r = double(m_support_pts[fidx].head_front_radius); + + // Reassemble the now corrected normal + auto nn = Vec3d(std::cos(azimuth) * std::sin(polar), + std::sin(azimuth) * std::sin(polar), + std::cos(polar)).normalized(); + + // check available distance + EigenMesh3D::hit_result t + = pinhead_mesh_intersect(hp, // touching point + nn, // normal + pin_r, + m_cfg.head_back_radius_mm, + w); + + if(t.distance() <= w) { + + // Let's try to optimize this angle, there might be a + // viable normal that doesn't collide with the model + // geometry and its very close to the default. + + StopCriteria stc; + stc.max_iterations = m_cfg.optimizer_max_iterations; + stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; + stc.stop_score = w; // space greater than w is enough + GeneticOptimizer solver(stc); + solver.seed(0); // we want deterministic behavior + + auto oresult = solver.optimize_max( + [this, pin_r, w, hp](double plr, double azm) + { + auto dir = Vec3d(std::cos(azm) * std::sin(plr), + std::sin(azm) * std::sin(plr), + std::cos(plr)).normalized(); + + double score = pinhead_mesh_intersect( + hp, dir, pin_r, m_cfg.head_back_radius_mm, w); + + return score; + }, + initvals(polar, azimuth), // start with what we have + bound(3 * PI / 4, + PI), // Must not exceed the tilt limit + bound(-PI, PI) // azimuth can be a full search + ); + + if(oresult.score > w) { + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + nn = Vec3d(std::cos(azimuth) * std::sin(polar), + std::sin(azimuth) * std::sin(polar), + std::cos(polar)).normalized(); + t = oresult.score; + } + } + + // save the verified and corrected normal + m_support_nmls.row(fidx) = nn; + + if (t.distance() > w) { + // Check distance from ground, we might have zero elevation. + if (hp(Z) + w * nn(Z) < m_builder.ground_level) { + addfn(m_iheadless, fidx); + } else { + // mark the point for needing a head. + addfn(m_iheads, fidx); + } + } else if (polar >= 3 * PI / 4) { + // Headless supports do not tilt like the headed ones + // so the normal should point almost to the ground. + addfn(m_iheadless, fidx); + } + } + }; + + ccr::enumerate(filtered_indices.begin(), filtered_indices.end(), filterfn); + + m_thr(); +} + +void SupportTreeBuildsteps::add_pinheads() +{ + for (unsigned i : m_iheads) { + m_thr(); + m_builder.add_head( + i, + m_cfg.head_back_radius_mm, + m_support_pts[i].head_front_radius, + m_cfg.head_width_mm, + m_cfg.head_penetration_mm, + m_support_nmls.row(i), // dir + m_support_pts[i].pos.cast() // displacement + ); + } +} + +void SupportTreeBuildsteps::classify() +{ + // We should first get the heads that reach the ground directly + PtIndices ground_head_indices; + ground_head_indices.reserve(m_iheads.size()); + m_iheads_onmodel.reserve(m_iheads.size()); + + // First we decide which heads reach the ground and can be full + // pillars and which shall be connected to the model surface (or + // search a suitable path around the surface that leads to the + // ground -- TODO) + for(unsigned i : m_iheads) { + m_thr(); + + auto& head = m_builder.head(i); + Vec3d n(0, 0, -1); + double r = head.r_back_mm; + Vec3d headjp = head.junction_point(); + + // collision check + auto hit = bridge_mesh_intersect(headjp, n, r); + + if(std::isinf(hit.distance())) ground_head_indices.emplace_back(i); + else if(m_cfg.ground_facing_only) head.invalidate(); + else m_iheads_onmodel.emplace_back(std::make_pair(i, hit)); + } + + // We want to search for clusters of points that are far enough + // from each other in the XY plane to not cross their pillar bases + // These clusters of support points will join in one pillar, + // possibly in their centroid support point. + + auto pointfn = [this](unsigned i) { + return m_builder.head(i).junction_point(); + }; + + auto predicate = [this](const PointIndexEl &e1, + const PointIndexEl &e2) { + double d2d = distance(to_2d(e1.first), to_2d(e2.first)); + double d3d = distance(e1.first, e2.first); + return d2d < 2 * m_cfg.base_radius_mm + && d3d < m_cfg.max_bridge_length_mm; + }; + + m_pillar_clusters = cluster(ground_head_indices, pointfn, predicate, + m_cfg.max_bridges_on_pillar); +} + +void SupportTreeBuildsteps::routing_to_ground() +{ + const double pradius = m_cfg.head_back_radius_mm; + + ClusterEl cl_centroids; + cl_centroids.reserve(m_pillar_clusters.size()); + + for (auto &cl : m_pillar_clusters) { + m_thr(); + + // place all the centroid head positions into the index. We + // will query for alternative pillar positions. If a sidehead + // cannot connect to the cluster centroid, we have to search + // for another head with a full pillar. Also when there are two + // elements in the cluster, the centroid is arbitrary and the + // sidehead is allowed to connect to a nearby pillar to + // increase structural stability. + + if (cl.empty()) continue; + + // get the current cluster centroid + auto & thr = m_thr; + const auto &points = m_points; + long lcid = cluster_centroid( + cl, [&points](size_t idx) { return points.row(long(idx)); }, + [thr](const Vec3d &p1, const Vec3d &p2) { + thr(); + return distance(Vec2d(p1(X), p1(Y)), Vec2d(p2(X), p2(Y))); + }); + + assert(lcid >= 0); + unsigned hid = cl[size_t(lcid)]; // Head ID + + cl_centroids.emplace_back(hid); + + Head &h = m_builder.head(hid); + h.transform(); + + create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id); + } + + // now we will go through the clusters ones again and connect the + // sidepoints with the cluster centroid (which is a ground pillar) + // or a nearby pillar if the centroid is unreachable. + size_t ci = 0; + for (auto cl : m_pillar_clusters) { + m_thr(); + + auto cidx = cl_centroids[ci++]; + + // TODO: don't consider the cluster centroid but calculate a + // central position where the pillar can be placed. this way + // the weight is distributed more effectively on the pillar. + + auto centerpillarID = m_builder.head_pillar(cidx).id; + + for (auto c : cl) { + m_thr(); + if (c == cidx) continue; + + auto &sidehead = m_builder.head(c); + sidehead.transform(); + + if (!connect_to_nearpillar(sidehead, centerpillarID) && + !search_pillar_and_connect(sidehead)) { + Vec3d pstart = sidehead.junction_point(); + // Vec3d pend = Vec3d{pstart(X), pstart(Y), gndlvl}; + // Could not find a pillar, create one + create_ground_pillar(pstart, sidehead.dir, pradius, sidehead.id); + } + } + } +} + +void SupportTreeBuildsteps::routing_to_model() +{ + // We need to check if there is an easy way out to the bed surface. + // If it can be routed there with a bridge shorter than + // min_bridge_distance. + + // First we want to index the available pillars. The best is to connect + // these points to the available pillars + + auto routedown = [this](Head& head, const Vec3d& dir, double dist) + { + head.transform(); + Vec3d endp = head.junction_point() + dist * dir; + m_builder.add_bridge(head.id, endp); + m_builder.add_junction(endp, head.r_back_mm); + + this->create_ground_pillar(endp, dir, head.r_back_mm); + }; + + std::vector modelpillars; + ccr::SpinningMutex mutex; + + auto onmodelfn = + [this, routedown, &modelpillars, &mutex] + (const std::pair &el, size_t) + { + m_thr(); + unsigned idx = el.first; + EigenMesh3D::hit_result hit = el.second; + + auto& head = m_builder.head(idx); + Vec3d hjp = head.junction_point(); + + // ///////////////////////////////////////////////////////////////// + // Search nearby pillar + // ///////////////////////////////////////////////////////////////// + + if(search_pillar_and_connect(head)) { head.transform(); return; } + + // ///////////////////////////////////////////////////////////////// + // Try straight path + // ///////////////////////////////////////////////////////////////// + + // Cannot connect to nearby pillar. We will try to search for + // a route to the ground. + + double t = bridge_mesh_intersect(hjp, head.dir, head.r_back_mm); + double d = 0, tdown = 0; + Vec3d dirdown(0.0, 0.0, -1.0); + + t = std::min(t, m_cfg.max_bridge_length_mm); + + while(d < t && !std::isinf(tdown = bridge_mesh_intersect( + hjp + d*head.dir, + dirdown, head.r_back_mm))) { + d += head.r_back_mm; + } + + if(std::isinf(tdown)) { // we heave found a route to the ground + routedown(head, head.dir, d); return; + } + + // ///////////////////////////////////////////////////////////////// + // Optimize bridge direction + // ///////////////////////////////////////////////////////////////// + + // Straight path failed so we will try to search for a suitable + // direction out of the cavity. + + // Get the spherical representation of the normal. its easier to + // work with. + double z = head.dir(Z); + double r = 1.0; // for normalized vector + double polar = std::acos(z / r); + double azimuth = std::atan2(head.dir(Y), head.dir(X)); + + using libnest2d::opt::bound; + using libnest2d::opt::initvals; + using libnest2d::opt::GeneticOptimizer; + using libnest2d::opt::StopCriteria; + + StopCriteria stc; + stc.max_iterations = m_cfg.optimizer_max_iterations; + stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; + stc.stop_score = 1e6; + GeneticOptimizer solver(stc); + solver.seed(0); // we want deterministic behavior + + double r_back = head.r_back_mm; + + auto oresult = solver.optimize_max( + [this, hjp, r_back](double plr, double azm) + { + Vec3d n = Vec3d(std::cos(azm) * std::sin(plr), + std::sin(azm) * std::sin(plr), + std::cos(plr)).normalized(); + return bridge_mesh_intersect(hjp, n, r_back); + }, + initvals(polar, azimuth), // let's start with what we have + bound(3*PI/4, PI), // Must not exceed the slope limit + bound(-PI, PI) // azimuth can be a full range search + ); + + d = 0; t = oresult.score; + + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + Vec3d bridgedir = Vec3d(std::cos(azimuth) * std::sin(polar), + std::sin(azimuth) * std::sin(polar), + std::cos(polar)).normalized(); + + t = std::min(t, m_cfg.max_bridge_length_mm); + + while(d < t && !std::isinf(tdown = bridge_mesh_intersect( + hjp + d*bridgedir, + dirdown, + head.r_back_mm))) { + d += head.r_back_mm; + } + + if(std::isinf(tdown)) { // we heave found a route to the ground + routedown(head, bridgedir, d); return; + } + + // ///////////////////////////////////////////////////////////////// + // Route to model body + // ///////////////////////////////////////////////////////////////// + + double zangle = std::asin(hit.direction()(Z)); + zangle = std::max(zangle, PI/4); + double h = std::sin(zangle) * head.fullwidth(); + + // The width of the tail head that we would like to have... + h = std::min(hit.distance() - head.r_back_mm, h); + + if(h > 0) { + Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h}; + auto center_hit = m_mesh.query_ray_hit(hjp, dirdown); + + double hitdiff = center_hit.distance() - hit.distance(); + Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm? + center_hit.position() : hit.position(); + + head.transform(); + + long pillar_id = m_builder.add_pillar(head.id, endp, head.r_back_mm); + Pillar &pill = m_builder.pillar(pillar_id); + + Vec3d taildir = endp - hitp; + double dist = distance(endp, hitp) + m_cfg.head_penetration_mm; + double w = dist - 2 * head.r_pin_mm - head.r_back_mm; + + if (w < 0.) { + BOOST_LOG_TRIVIAL(error) << "Pinhead width is negative!"; + w = 0.; + } + + Head tailhead(head.r_back_mm, + head.r_pin_mm, + w, + m_cfg.head_penetration_mm, + taildir, + hitp); + + tailhead.transform(); + pill.base = tailhead.mesh; + + // Experimental: add the pillar to the index for cascading + std::lock_guard lk(mutex); + modelpillars.emplace_back(unsigned(pill.id)); + return; + } + + // We have failed to route this head. + BOOST_LOG_TRIVIAL(warning) + << "Failed to route model facing support point." + << " ID: " << idx; + head.invalidate(); + }; + + ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), onmodelfn); + + for(auto pillid : modelpillars) { + auto& pillar = m_builder.pillar(pillid); + m_pillar_index.insert(pillar.endpoint(), pillid); + } +} + +void SupportTreeBuildsteps::interconnect_pillars() +{ + // Now comes the algorithm that connects pillars with each other. + // Ideally every pillar should be connected with at least one of its + // neighbors if that neighbor is within max_pillar_link_distance + + // Pillars with height exceeding H1 will require at least one neighbor + // to connect with. Height exceeding H2 require two neighbors. + double H1 = m_cfg.max_solo_pillar_height_mm; + double H2 = m_cfg.max_dual_pillar_height_mm; + double d = m_cfg.max_pillar_link_distance_mm; + + //A connection between two pillars only counts if the height ratio is + // bigger than 50% + double min_height_ratio = 0.5; + + std::set pairs; + + // A function to connect one pillar with its neighbors. THe number of + // neighbors is given in the configuration. This function if called + // for every pillar in the pillar index. A pair of pillar will not + // be connected multiple times this is ensured by the 'pairs' set which + // remembers the processed pillar pairs + auto cascadefn = + [this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el) + { + Vec3d qp = el.first; // endpoint of the pillar + + const Pillar& pillar = m_builder.pillar(el.second); // actual pillar + + // Get the max number of neighbors a pillar should connect to + unsigned neighbors = m_cfg.pillar_cascade_neighbors; + + // connections are already enough for the pillar + if(pillar.links >= neighbors) return; + + // Query all remaining points within reach + auto qres = m_pillar_index.query([qp, d](const PointIndexEl& e){ + return distance(e.first, qp) < d; + }); + + // sort the result by distance (have to check if this is needed) + std::sort(qres.begin(), qres.end(), + [qp](const PointIndexEl& e1, const PointIndexEl& e2){ + return distance(e1.first, qp) < distance(e2.first, qp); + }); + + for(auto& re : qres) { // process the queried neighbors + + if(re.second == el.second) continue; // Skip self + + auto a = el.second, b = re.second; + + // Get unique hash for the given pair (order doesn't matter) + auto hashval = pairhash(a, b); + + // Search for the pair amongst the remembered pairs + if(pairs.find(hashval) != pairs.end()) continue; + + const Pillar& neighborpillar = m_builder.pillar(re.second); + + // this neighbor is occupied, skip + if(neighborpillar.links >= neighbors) continue; + + if(interconnect(pillar, neighborpillar)) { + pairs.insert(hashval); + + // If the interconnection length between the two pillars is + // less than 50% of the longer pillar's height, don't count + if(pillar.height < H1 || + neighborpillar.height / pillar.height > min_height_ratio) + m_builder.increment_links(pillar); + + if(neighborpillar.height < H1 || + pillar.height / neighborpillar.height > min_height_ratio) + m_builder.increment_links(neighborpillar); + + } + + // connections are enough for one pillar + if(pillar.links >= neighbors) break; + } + }; + + // Run the cascade for the pillars in the index + m_pillar_index.foreach(cascadefn); + + // We would be done here if we could allow some pillars to not be + // connected with any neighbors. But this might leave the support tree + // unprintable. + // + // The current solution is to insert additional pillars next to these + // lonely pillars. One or even two additional pillar might get inserted + // depending on the length of the lonely pillar. + + size_t pillarcount = m_builder.pillarcount(); + + // Again, go through all pillars, this time in the whole support tree + // not just the index. + for(size_t pid = 0; pid < pillarcount; pid++) { + auto pillar = [this, pid]() { return m_builder.pillar(pid); }; + + // Decide how many additional pillars will be needed: + + unsigned needpillars = 0; + if (pillar().bridges > m_cfg.max_bridges_on_pillar) + needpillars = 3; + else if (pillar().links < 2 && pillar().height > H2) { + // Not enough neighbors to support this pillar + needpillars = 2; + } else if (pillar().links < 1 && pillar().height > H1) { + // No neighbors could be found and the pillar is too long. + needpillars = 1; + } + + needpillars = std::max(pillar().links, needpillars) - pillar().links; + if (needpillars == 0) continue; + + // Search for new pillar locations: + + bool found = false; + double alpha = 0; // goes to 2Pi + double r = 2 * m_cfg.base_radius_mm; + Vec3d pillarsp = pillar().startpoint(); + + // temp value for starting point detection + Vec3d sp(pillarsp(X), pillarsp(Y), pillarsp(Z) - r); + + // A vector of bool for placement feasbility + std::vector canplace(needpillars, false); + std::vector spts(needpillars); // vector of starting points + + double gnd = m_builder.ground_level; + double min_dist = m_cfg.pillar_base_safety_distance_mm + + m_cfg.base_radius_mm + EPSILON; + + while(!found && alpha < 2*PI) { + for (unsigned n = 0; + n < needpillars && (!n || canplace[n - 1]); + n++) + { + double a = alpha + n * PI / 3; + Vec3d s = sp; + s(X) += std::cos(a) * r; + s(Y) += std::sin(a) * r; + spts[n] = s; + + // Check the path vertically down + auto hr = bridge_mesh_intersect(s, {0, 0, -1}, pillar().r); + Vec3d gndsp{s(X), s(Y), gnd}; + + // If the path is clear, check for pillar base collisions + canplace[n] = std::isinf(hr.distance()) && + std::sqrt(m_mesh.squared_distance(gndsp)) > + min_dist; + } + + found = std::all_of(canplace.begin(), canplace.end(), + [](bool v) { return v; }); + + // 20 angles will be tried... + alpha += 0.1 * PI; + } + + std::vector newpills; + newpills.reserve(needpillars); + + if (found) + for (unsigned n = 0; n < needpillars; n++) { + Vec3d s = spts[n]; + Pillar p(s, Vec3d(s(X), s(Y), gnd), pillar().r); + p.add_base(m_cfg.base_height_mm, m_cfg.base_radius_mm); + + if (interconnect(pillar(), p)) { + Pillar &pp = m_builder.pillar(m_builder.add_pillar(p)); + m_pillar_index.insert(pp.endpoint(), unsigned(pp.id)); + + m_builder.add_junction(s, pillar().r); + double t = bridge_mesh_intersect(pillarsp, + dirv(pillarsp, s), + pillar().r); + if (distance(pillarsp, s) < t) + m_builder.add_bridge(pillarsp, s, pillar().r); + + if (pillar().endpoint()(Z) > m_builder.ground_level) + m_builder.add_junction(pillar().endpoint(), + pillar().r); + + newpills.emplace_back(pp.id); + m_builder.increment_links(pillar()); + m_builder.increment_links(pp); + } + } + + if(!newpills.empty()) { + for(auto it = newpills.begin(), nx = std::next(it); + nx != newpills.end(); ++it, ++nx) { + const Pillar& itpll = m_builder.pillar(*it); + const Pillar& nxpll = m_builder.pillar(*nx); + if(interconnect(itpll, nxpll)) { + m_builder.increment_links(itpll); + m_builder.increment_links(nxpll); + } + } + + m_pillar_index.foreach(cascadefn); + } + } +} + +void SupportTreeBuildsteps::routing_headless() +{ + // For now we will just generate smaller headless sticks with a sharp + // ending point that connects to the mesh surface. + + // We will sink the pins into the model surface for a distance of 1/3 of + // the pin radius + for(unsigned i : m_iheadless) { + m_thr(); + + const auto R = double(m_support_pts[i].head_front_radius); + const double HWIDTH_MM = m_cfg.head_penetration_mm; + + // Exact support position + Vec3d sph = m_support_pts[i].pos.cast(); + Vec3d n = m_support_nmls.row(i); // mesh outward normal + Vec3d sp = sph - n * HWIDTH_MM; // stick head start point + + Vec3d dir = {0, 0, -1}; + Vec3d sj = sp + R * n; // stick start point + + // This is only for checking + double idist = bridge_mesh_intersect(sph, dir, R, true); + double realdist = ray_mesh_intersect(sj, dir); + double dist = realdist; + + if (std::isinf(dist)) dist = sph(Z) - m_builder.ground_level; + + if(std::isnan(idist) || idist < 2*R || std::isnan(dist) || dist < 2*R) { + BOOST_LOG_TRIVIAL(warning) << "Can not find route for headless" + << " support stick at: " + << sj.transpose(); + continue; + } + + bool use_endball = !std::isinf(realdist); + Vec3d ej = sj + (dist + HWIDTH_MM) * dir; + m_builder.add_compact_bridge(sp, ej, n, R, use_endball); + } +} + +} +} diff --git a/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp new file mode 100644 index 000000000..b92e44dbd --- /dev/null +++ b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp @@ -0,0 +1,289 @@ +#ifndef SLASUPPORTTREEALGORITHM_H +#define SLASUPPORTTREEALGORITHM_H + +#include + +#include "SLASupportTreeBuilder.hpp" + +namespace Slic3r { +namespace sla { + +// The minimum distance for two support points to remain valid. +const double /*constexpr*/ D_SP = 0.1; + +enum { // For indexing Eigen vectors as v(X), v(Y), v(Z) instead of numbers + X, Y, Z +}; + +inline Vec2d to_vec2(const Vec3d& v3) { + return {v3(X), v3(Y)}; +} + +// This function returns the position of the centroid in the input 'clust' +// vector of point indices. +template +long cluster_centroid(const ClusterEl& clust, + const std::function &pointfn, + DistFn df) +{ + switch(clust.size()) { + case 0: /* empty cluster */ return ID_UNSET; + case 1: /* only one element */ return 0; + case 2: /* if two elements, there is no center */ return 0; + default: ; + } + + // The function works by calculating for each point the average distance + // from all the other points in the cluster. We create a selector bitmask of + // the same size as the cluster. The bitmask will have two true bits and + // false bits for the rest of items and we will loop through all the + // permutations of the bitmask (combinations of two points). Get the + // distance for the two points and add the distance to the averages. + // The point with the smallest average than wins. + + // The complexity should be O(n^2) but we will mostly apply this function + // for small clusters only (cca 3 elements) + + std::vector sel(clust.size(), false); // create full zero bitmask + std::fill(sel.end() - 2, sel.end(), true); // insert the two ones + std::vector avgs(clust.size(), 0.0); // store the average distances + + do { + std::array idx; + for(size_t i = 0, j = 0; i < clust.size(); i++) if(sel[i]) idx[j++] = i; + + double d = df(pointfn(clust[idx[0]]), + pointfn(clust[idx[1]])); + + // add the distance to the sums for both associated points + for(auto i : idx) avgs[i] += d; + + // now continue with the next permutation of the bitmask with two 1s + } while(std::next_permutation(sel.begin(), sel.end())); + + // Divide by point size in the cluster to get the average (may be redundant) + for(auto& a : avgs) a /= clust.size(); + + // get the lowest average distance and return the index + auto minit = std::min_element(avgs.begin(), avgs.end()); + return long(minit - avgs.begin()); +} + +inline Vec3d dirv(const Vec3d& startp, const Vec3d& endp) { + return (endp - startp).normalized(); +} + +class PillarIndex { + PointIndex m_index; + using Mutex = ccr::BlockingMutex; + mutable Mutex m_mutex; + +public: + + template inline void guarded_insert(Args&&...args) + { + std::lock_guard lck(m_mutex); + m_index.insert(std::forward(args)...); + } + + template + inline std::vector guarded_query(Args&&...args) const + { + std::lock_guard lck(m_mutex); + return m_index.query(std::forward(args)...); + } + + template inline void insert(Args&&...args) + { + m_index.insert(std::forward(args)...); + } + + template + inline std::vector query(Args&&...args) const + { + return m_index.query(std::forward(args)...); + } + + template inline void foreach(Fn fn) { m_index.foreach(fn); } + template inline void guarded_foreach(Fn fn) + { + std::lock_guard lck(m_mutex); + m_index.foreach(fn); + } + + PointIndex guarded_clone() + { + std::lock_guard lck(m_mutex); + return m_index; + } +}; + +// Helper function for pillar interconnection where pairs of already connected +// pillars should be checked for not to be processed again. This can be done +// in constant time with a set of hash values uniquely representing a pair of +// integers. The order of numbers within the pair should not matter, it has +// the same unique hash. The hash value has to have twice as many bits as the +// arguments need. If the same integral type is used for args and return val, +// make sure the arguments use only the half of the type's bit depth. +template> +IntegerOnly pairhash(I a, I b) +{ + using std::ceil; using std::log2; using std::max; using std::min; + static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); + static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); + static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; + + I g = min(a, b), l = max(a, b); + + // Assume the hash will fit into the output variable + assert((g ? (ceil(log2(g))) : 0) <= shift); + assert((l ? (ceil(log2(l))) : 0) <= shift); + + return (DoubleI(g) << shift) + l; +} + +class SupportTreeBuildsteps { + const SupportConfig& m_cfg; + const EigenMesh3D& m_mesh; + const std::vector& m_support_pts; + + using PtIndices = std::vector; + + PtIndices m_iheads; // support points with pinhead + PtIndices m_iheadless; // headless support points + + // supp. pts. connecting to model: point index and the ray hit data + std::vector> m_iheads_onmodel; + + // normals for support points from model faces. + PointSet m_support_nmls; + + // Clusters of points which can reach the ground directly and can be + // bridged to one central pillar + std::vector m_pillar_clusters; + + // This algorithm uses the SupportTreeBuilder class to fill gradually + // the support elements (heads, pillars, bridges, ...) + SupportTreeBuilder& m_builder; + + // support points in Eigen/IGL format + PointSet m_points; + + // throw if canceled: It will be called many times so a shorthand will + // come in handy. + ThrowOnCancel m_thr; + + // A spatial index to easily find strong pillars to connect to. + PillarIndex m_pillar_index; + + // When bridging heads to pillars... TODO: find a cleaner solution + ccr::BlockingMutex m_bridge_mutex; + + inline double ray_mesh_intersect(const Vec3d& s, + const Vec3d& dir) + { + return m_mesh.query_ray_hit(s, dir).distance(); + } + + // This function will test if a future pinhead would not collide with the + // model geometry. It does not take a 'Head' object because those are + // created after this test. Parameters: s: The touching point on the model + // surface. dir: This is the direction of the head from the pin to the back + // r_pin, r_back: the radiuses of the pin and the back sphere width: This + // is the full width from the pin center to the back center m: The object + // mesh. + // The return value is the hit result from the ray casting. If the starting + // point was inside the model, an "invalid" hit_result will be returned + // with a zero distance value instead of a NAN. This way the result can + // be used safely for comparison with other distances. + EigenMesh3D::hit_result pinhead_mesh_intersect( + const Vec3d& s, + const Vec3d& dir, + double r_pin, + double r_back, + double width); + + // Checking bridge (pillar and stick as well) intersection with the model. + // If the function is used for headless sticks, the ins_check parameter + // have to be true as the beginning of the stick might be inside the model + // geometry. + // The return value is the hit result from the ray casting. If the starting + // point was inside the model, an "invalid" hit_result will be returned + // with a zero distance value instead of a NAN. This way the result can + // be used safely for comparison with other distances. + EigenMesh3D::hit_result bridge_mesh_intersect( + const Vec3d& s, + const Vec3d& dir, + double r, + bool ins_check = false); + + // Helper function for interconnecting two pillars with zig-zag bridges. + bool interconnect(const Pillar& pillar, const Pillar& nextpillar); + + // For connecting a head to a nearby pillar. + bool connect_to_nearpillar(const Head& head, long nearpillar_id); + + bool search_pillar_and_connect(const Head& head); + + // This is a proxy function for pillar creation which will mind the gap + // between the pad and the model bottom in zero elevation mode. + void create_ground_pillar(const Vec3d &jp, + const Vec3d &sourcedir, + double radius, + long head_id = ID_UNSET); +public: + SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm); + + // Now let's define the individual steps of the support generation algorithm + + // Filtering step: here we will discard inappropriate support points + // and decide the future of the appropriate ones. We will check if a + // pinhead is applicable and adjust its angle at each support point. We + // will also merge the support points that are just too close and can + // be considered as one. + void filter(); + + // Pinhead creation: based on the filtering results, the Head objects + // will be constructed (together with their triangle meshes). + void add_pinheads(); + + // Further classification of the support points with pinheads. If the + // ground is directly reachable through a vertical line parallel to the + // Z axis we consider a support point as pillar candidate. If touches + // the model geometry, it will be marked as non-ground facing and + // further steps will process it. Also, the pillars will be grouped + // into clusters that can be interconnected with bridges. Elements of + // these groups may or may not be interconnected. Here we only run the + // clustering algorithm. + void classify(); + + // Step: Routing the ground connected pinheads, and interconnecting + // them with additional (angled) bridges. Not all of these pinheads + // will be a full pillar (ground connected). Some will connect to a + // nearby pillar using a bridge. The max number of such side-heads for + // a central pillar is limited to avoid bad weight distribution. + void routing_to_ground(); + + // Step: routing the pinheads that would connect to the model surface + // along the Z axis downwards. For now these will actually be connected with + // the model surface with a flipped pinhead. In the future here we could use + // some smart algorithms to search for a safe path to the ground or to a + // nearby pillar that can hold the supported weight. + void routing_to_model(); + + void interconnect_pillars(); + + // Step: process the support points where there is not enough space for a + // full pinhead. In this case we will use a rounded sphere as a touching + // point and use a thinner bridge (let's call it a stick). + void routing_headless (); + + inline void merge_result() { m_builder.merged_mesh(); } + + static bool execute(SupportTreeBuilder & builder, const SupportableMesh &sm); +}; + +} +} + +#endif // SLASUPPORTTREEALGORITHM_H diff --git a/src/libslic3r/SLA/SLASupportTreeIGL.cpp b/src/libslic3r/SLA/SLASupportTreeIGL.cpp index 95d451271..05f8b1984 100644 --- a/src/libslic3r/SLA/SLASupportTreeIGL.cpp +++ b/src/libslic3r/SLA/SLASupportTreeIGL.cpp @@ -77,7 +77,7 @@ bool PointIndex::remove(const PointIndexEl& el) } std::vector -PointIndex::query(std::function fn) +PointIndex::query(std::function fn) const { namespace bgi = boost::geometry::index; @@ -86,7 +86,7 @@ PointIndex::query(std::function fn) return ret; } -std::vector PointIndex::nearest(const Vec3d &el, unsigned k = 1) +std::vector PointIndex::nearest(const Vec3d &el, unsigned k = 1) const { namespace bgi = boost::geometry::index; std::vector ret; ret.reserve(k); @@ -104,6 +104,11 @@ void PointIndex::foreach(std::function fn) for(auto& el : m_impl->m_store) fn(el); } +void PointIndex::foreach(std::function fn) const +{ + for(const auto &el : m_impl->m_store) fn(el); +} + /* ************************************************************************** * BoxIndex implementation * ************************************************************************** */ @@ -274,6 +279,8 @@ double EigenMesh3D::squared_distance(const Vec3d &p, int& i, Vec3d& c) const { * Misc functions * ****************************************************************************/ +namespace { + bool point_on_edge(const Vec3d& p, const Vec3d& e1, const Vec3d& e2, double eps = 0.05) { @@ -289,11 +296,13 @@ template double distance(const Vec& pp1, const Vec& pp2) { return std::sqrt(p.transpose() * p); } +} + PointSet normals(const PointSet& points, const EigenMesh3D& mesh, double eps, std::function thr, // throw on cancel - const std::vector& pt_indices = {}) + const std::vector& pt_indices) { if(points.rows() == 0 || mesh.V().rows() == 0 || mesh.F().rows() == 0) return {}; @@ -419,9 +428,17 @@ PointSet normals(const PointSet& points, return ret; } + namespace bgi = boost::geometry::index; using Index3D = bgi::rtree< PointIndexEl, bgi::rstar<16, 4> /* ? */ >; +namespace { + +bool cmp_ptidx_elements(const PointIndexEl& e1, const PointIndexEl& e2) +{ + return e1.second < e2.second; +}; + ClusteredPoints cluster(Index3D &sindex, unsigned max_points, std::function( @@ -433,25 +450,22 @@ ClusteredPoints cluster(Index3D &sindex, // each other std::function group = [&sindex, &group, max_points, qfn](Elems& pts, Elems& cluster) - { + { for(auto& p : pts) { std::vector tmp = qfn(sindex, p); - auto cmp = [](const PointIndexEl& e1, const PointIndexEl& e2){ - return e1.second < e2.second; - }; - - std::sort(tmp.begin(), tmp.end(), cmp); + + std::sort(tmp.begin(), tmp.end(), cmp_ptidx_elements); Elems newpts; std::set_difference(tmp.begin(), tmp.end(), cluster.begin(), cluster.end(), - std::back_inserter(newpts), cmp); + std::back_inserter(newpts), cmp_ptidx_elements); int c = max_points && newpts.size() + cluster.size() > max_points? int(max_points - cluster.size()) : int(newpts.size()); cluster.insert(cluster.end(), newpts.begin(), newpts.begin() + c); - std::sort(cluster.begin(), cluster.end(), cmp); + std::sort(cluster.begin(), cluster.end(), cmp_ptidx_elements); if(!newpts.empty() && (!max_points || cluster.size() < max_points)) group(newpts, cluster); @@ -479,7 +493,6 @@ ClusteredPoints cluster(Index3D &sindex, return result; } -namespace { std::vector distance_queryfn(const Index3D& sindex, const PointIndexEl& p, double dist, @@ -496,7 +509,8 @@ std::vector distance_queryfn(const Index3D& sindex, return tmp; } -} + +} // namespace // Clustering a set of points by the given criteria ClusteredPoints cluster( @@ -558,5 +572,5 @@ ClusteredPoints cluster(const PointSet& pts, double dist, unsigned max_points) }); } -} -} +} // namespace sla +} // namespace Slic3r diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 46d039c1f..2a1ae74d7 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -1,6 +1,6 @@ #include "SLAPrint.hpp" #include "SLA/SLASupportTree.hpp" -#include "SLA/SLABasePool.hpp" +#include "SLA/SLAPad.hpp" #include "SLA/SLAAutoSupports.hpp" #include "ClipperUtils.hpp" #include "Geometry.hpp" @@ -32,17 +32,19 @@ namespace Slic3r { -using SupportTreePtr = std::unique_ptr; - -class SLAPrintObject::SupportData +class SLAPrintObject::SupportData : public sla::SupportableMesh { public: - sla::EigenMesh3D emesh; // index-triangle representation - std::vector support_points; // all the support points (manual/auto) - SupportTreePtr support_tree_ptr; // the supports + sla::SupportTree::UPtr support_tree_ptr; // the supports std::vector support_slices; // sliced supports - - inline SupportData(const TriangleMesh &trmesh) : emesh(trmesh) {} + + inline SupportData(const TriangleMesh &t): sla::SupportableMesh{t, {}, {}} {} + + sla::SupportTree::UPtr &create_support_tree(const sla::JobController &ctl) + { + support_tree_ptr = sla::SupportTree::create(*this, ctl); + return support_tree_ptr; + } }; namespace { @@ -53,7 +55,7 @@ const std::array OBJ_STEP_LEVELS = 30, // slaposObjectSlice, 20, // slaposSupportPoints, 10, // slaposSupportTree, - 10, // slaposBasePool, + 10, // slaposPad, 30, // slaposSliceSupports, }; @@ -64,7 +66,7 @@ std::string OBJ_STEP_LABELS(size_t idx) case slaposObjectSlice: return L("Slicing model"); case slaposSupportPoints: return L("Generating support points"); case slaposSupportTree: return L("Generating support tree"); - case slaposBasePool: return L("Generating pad"); + case slaposPad: return L("Generating pad"); case slaposSliceSupports: return L("Slicing supports"); default:; } @@ -583,7 +585,8 @@ bool is_zero_elevation(const SLAPrintObjectConfig &c) { // Compile the argument for support creation from the static print config. sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) { sla::SupportConfig scfg; - + + scfg.enabled = c.supports_enable.getBool(); scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); scfg.head_back_radius_mm = 0.5*c.support_pillar_diameter.getFloat(); scfg.head_penetration_mm = c.support_head_penetration.getFloat(); @@ -612,12 +615,13 @@ sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) { return scfg; } -sla::PoolConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) { - sla::PoolConfig::EmbedObject ret; +sla::PadConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) { + sla::PadConfig::EmbedObject ret; ret.enabled = is_zero_elevation(c); if(ret.enabled) { + ret.everywhere = c.pad_around_object_everywhere.getBool(); ret.object_gap_mm = c.pad_object_gap.getFloat(); ret.stick_width_mm = c.pad_object_connector_width.getFloat(); ret.stick_stride_mm = c.pad_object_connector_stride.getFloat(); @@ -628,17 +632,15 @@ sla::PoolConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) { return ret; } -sla::PoolConfig make_pool_config(const SLAPrintObjectConfig& c) { - sla::PoolConfig pcfg; +sla::PadConfig make_pad_cfg(const SLAPrintObjectConfig& c) { + sla::PadConfig pcfg; - pcfg.min_wall_thickness_mm = c.pad_wall_thickness.getFloat(); + pcfg.wall_thickness_mm = c.pad_wall_thickness.getFloat(); pcfg.wall_slope = c.pad_wall_slope.getFloat() * PI / 180.0; - // We do not support radius for now - pcfg.edge_radius_mm = 0.0; //c.pad_edge_radius.getFloat(); - - pcfg.max_merge_distance_mm = c.pad_max_merge_distance.getFloat(); - pcfg.min_wall_height_mm = c.pad_wall_height.getFloat(); + pcfg.max_merge_dist_mm = c.pad_max_merge_distance.getFloat(); + pcfg.wall_height_mm = c.pad_wall_height.getFloat(); + pcfg.brim_size_mm = c.pad_brim_size.getFloat(); // set builtin pad implicitly ON pcfg.embed_object = builtin_pad_cfg(c); @@ -646,6 +648,13 @@ sla::PoolConfig make_pool_config(const SLAPrintObjectConfig& c) { return pcfg; } +bool validate_pad(const TriangleMesh &pad, const sla::PadConfig &pcfg) +{ + // An empty pad can only be created if embed_object mode is enabled + // and the pad is not forced everywhere + return !pad.empty() || (pcfg.embed_object.enabled && !pcfg.embed_object.everywhere); +} + } std::string SLAPrint::validate() const @@ -663,17 +672,12 @@ std::string SLAPrint::validate() const sla::SupportConfig cfg = make_support_cfg(po->config()); - double pinhead_width = - 2 * cfg.head_front_radius_mm + - cfg.head_width_mm + - 2 * cfg.head_back_radius_mm - - cfg.head_penetration_mm; - double elv = cfg.object_elevation_mm; - - sla::PoolConfig::EmbedObject builtinpad = builtin_pad_cfg(po->config()); - if(supports_en && !builtinpad.enabled && elv < pinhead_width ) + sla::PadConfig padcfg = make_pad_cfg(po->config()); + sla::PadConfig::EmbedObject &builtinpad = padcfg.embed_object; + + if(supports_en && !builtinpad.enabled && elv < cfg.head_fullwidth()) return L( "Elevation is too low for object. Use the \"Pad around " "object\" feature to print the object without elevation."); @@ -686,6 +690,9 @@ std::string SLAPrint::validate() const "distance' has to be greater than the 'Pad object gap' " "parameter to avoid this."); } + + std::string pval = padcfg.validate(); + if (!pval.empty()) return pval; } double expt_max = m_printer_config.max_exposure_time.getFloat(); @@ -876,8 +883,7 @@ void SLAPrint::process() // Construction of this object does the calculation. this->throw_if_canceled(); - SLAAutoSupports auto_supports(po.transformed_mesh(), - po.m_supportdata->emesh, + SLAAutoSupports auto_supports(po.m_supportdata->emesh, po.get_model_slices(), heights, config, @@ -887,10 +893,10 @@ void SLAPrint::process() // Now let's extract the result. const std::vector& points = auto_supports.output(); this->throw_if_canceled(); - po.m_supportdata->support_points = points; + po.m_supportdata->pts = points; BOOST_LOG_TRIVIAL(debug) << "Automatic support points: " - << po.m_supportdata->support_points.size(); + << po.m_supportdata->pts.size(); // Using RELOAD_SLA_SUPPORT_POINTS to tell the Plater to pass // the update status to GLGizmoSlaSupports @@ -902,29 +908,19 @@ void SLAPrint::process() else { // There are either some points on the front-end, or the user // removed them on purpose. No calculation will be done. - po.m_supportdata->support_points = po.transformed_support_points(); + po.m_supportdata->pts = po.transformed_support_points(); } // If the zero elevation mode is engaged, we have to filter out all the // points that are on the bottom of the object if (is_zero_elevation(po.config())) { - double gnd = po.m_supportdata->emesh.ground_level(); - auto & pts = po.m_supportdata->support_points; double tolerance = po.config().pad_enable.getBool() ? po.m_config.pad_wall_thickness.getFloat() : po.m_config.support_base_height.getFloat(); - // get iterator to the reorganized vector end - auto endit = std::remove_if( - pts.begin(), - pts.end(), - [tolerance, gnd](const sla::SupportPoint &sp) { - double diff = std::abs(gnd - double(sp.pos(Z))); - return diff <= tolerance; - }); - - // erase all elements after the new end - pts.erase(endit, pts.end()); + remove_bottom_points(po.m_supportdata->pts, + po.m_supportdata->emesh.ground_level(), + tolerance); } }; @@ -933,45 +929,31 @@ void SLAPrint::process() { if(!po.m_supportdata) return; - sla::PoolConfig pcfg = make_pool_config(po.m_config); + sla::PadConfig pcfg = make_pad_cfg(po.m_config); if (pcfg.embed_object) - po.m_supportdata->emesh.ground_level_offset( - pcfg.min_wall_thickness_mm); - - if(!po.m_config.supports_enable.getBool()) { - - // Generate empty support tree. It can still host a pad - po.m_supportdata->support_tree_ptr.reset( - new SLASupportTree(po.m_supportdata->emesh.ground_level())); - - return; - } - - sla::SupportConfig scfg = make_support_cfg(po.m_config); - sla::Controller ctl; - + po.m_supportdata->emesh.ground_level_offset(pcfg.wall_thickness_mm); + + po.m_supportdata->cfg = make_support_cfg(po.m_config); + // scaling for the sub operations double d = ostepd * OBJ_STEP_LEVELS[slaposSupportTree] / 100.0; double init = m_report_status.status(); + JobController ctl; - ctl.statuscb = [this, d, init](unsigned st, const std::string &logmsg) - { + ctl.statuscb = [this, d, init](unsigned st, const std::string &logmsg) { double current = init + st * d; - if(std::round(m_report_status.status()) < std::round(current)) + if (std::round(m_report_status.status()) < std::round(current)) m_report_status(*this, current, OBJ_STEP_LABELS(slaposSupportTree), - SlicingStatus::DEFAULT, - logmsg); - + SlicingStatus::DEFAULT, logmsg); }; - - ctl.stopcondition = [this](){ return canceled(); }; + ctl.stopcondition = [this]() { return canceled(); }; ctl.cancelfn = [this]() { throw_if_canceled(); }; - - po.m_supportdata->support_tree_ptr.reset( - new SLASupportTree(po.m_supportdata->support_points, - po.m_supportdata->emesh, scfg, ctl)); + + po.m_supportdata->create_support_tree(ctl); + + if (!po.m_config.supports_enable.getBool()) return; throw_if_canceled(); @@ -980,10 +962,9 @@ void SLAPrint::process() // This is to prevent "Done." being displayed during merged_mesh() m_report_status(*this, -1, L("Visualizing supports")); - po.m_supportdata->support_tree_ptr->merged_mesh(); BOOST_LOG_TRIVIAL(debug) << "Processed support point count " - << po.m_supportdata->support_points.size(); + << po.m_supportdata->pts.size(); // Check the mesh for later troubleshooting. if(po.support_mesh().empty()) @@ -993,7 +974,7 @@ void SLAPrint::process() }; // This step generates the sla base pad - auto base_pool = [this](SLAPrintObject& po) { + auto generate_pad = [this](SLAPrintObject& po) { // this step can only go after the support tree has been created // and before the supports had been sliced. (or the slicing has to be // repeated) @@ -1001,10 +982,10 @@ void SLAPrint::process() if(po.m_config.pad_enable.getBool()) { // Get the distilled pad configuration from the config - sla::PoolConfig pcfg = make_pool_config(po.m_config); + sla::PadConfig pcfg = make_pad_cfg(po.m_config); ExPolygons bp; // This will store the base plate of the pad. - double pad_h = sla::get_pad_fullheight(pcfg); + double pad_h = pcfg.full_height(); const TriangleMesh &trmesh = po.transformed_mesh(); // This call can get pretty time consuming @@ -1015,15 +996,19 @@ void SLAPrint::process() // we sometimes call it "builtin pad" is enabled so we will // get a sample from the bottom of the mesh and use it for pad // creation. - sla::base_plate(trmesh, - bp, - float(pad_h), - float(po.m_config.layer_height.getFloat()), - thrfn); + sla::pad_blueprint(trmesh, bp, float(pad_h), + float(po.m_config.layer_height.getFloat()), + thrfn); } - pcfg.throw_on_cancel = thrfn; po.m_supportdata->support_tree_ptr->add_pad(bp, pcfg); + auto &pad_mesh = po.m_supportdata->support_tree_ptr->retrieve_mesh(MeshType::Pad); + + if (!validate_pad(pad_mesh, pcfg)) + throw std::runtime_error( + L("No pad can be generated for this model with the " + "current configuration")); + } else if(po.m_supportdata && po.m_supportdata->support_tree_ptr) { po.m_supportdata->support_tree_ptr->remove_pad(); } @@ -1191,9 +1176,8 @@ void SLAPrint::process() { ClipperPolygon poly; - // We need to reverse if flpXY OR is_lefthanded is true but - // not if both are true which is a logical inequality (XOR) - bool needreverse = /*flpXY !=*/ is_lefthanded; + // We need to reverse if is_lefthanded is true but + bool needreverse = is_lefthanded; // should be a move poly.Contour.reserve(polygon.contour.size() + 1); @@ -1396,7 +1380,7 @@ void SLAPrint::process() if(canceled()) return; // Set up the printer, allocate space for all the layers - sla::SLARasterWriter &printer = init_printer(); + sla::RasterWriter &printer = init_printer(); auto lvlcnt = unsigned(m_printer_input.size()); printer.layers(lvlcnt); @@ -1416,11 +1400,9 @@ void SLAPrint::process() SpinMutex slck; - auto orientation = get_printer_orientation(); - // procedure to process one height level. This will run in parallel auto lvlfn = - [this, &slck, &printer, increment, &dstatus, &pst, orientation] + [this, &slck, &printer, increment, &dstatus, &pst] (unsigned level_id) { if(canceled()) return; @@ -1431,7 +1413,7 @@ void SLAPrint::process() printer.begin_layer(level_id); for(const ClipperLib::Polygon& poly : printlayer.transformed_slices()) - printer.draw_polygon(poly, level_id, orientation); + printer.draw_polygon(poly, level_id); // Finish the layer for later saving it. printer.finish_layer(level_id); @@ -1459,7 +1441,7 @@ void SLAPrint::process() tbb::parallel_for(0, lvlcnt, lvlfn); // Set statistics values to the printer - sla::SLARasterWriter::PrintStatistics stats; + sla::RasterWriter::PrintStatistics stats; stats.used_material = (m_print_statistics.objects_used_material + m_print_statistics.support_used_material) / 1000; @@ -1478,12 +1460,12 @@ void SLAPrint::process() slaposFn pobj_program[] = { - slice_model, support_points, support_tree, base_pool, slice_supports + slice_model, support_points, support_tree, generate_pad, slice_supports }; // We want to first process all objects... std::vector level1_obj_steps = { - slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposBasePool + slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad }; // and then slice all supports to allow preview to be displayed ASAP @@ -1654,12 +1636,11 @@ bool SLAPrint::invalidate_state_by_config_options(const std::vector mirror; - double gamma; double w = m_printer_config.display_width.getFloat(); double h = m_printer_config.display_height.getFloat(); @@ -1669,20 +1650,18 @@ sla::SLARasterWriter & SLAPrint::init_printer() mirror[X] = m_printer_config.display_mirror_x.getBool(); mirror[Y] = m_printer_config.display_mirror_y.getBool(); - if (get_printer_orientation() == sla::SLARasterWriter::roPortrait) { + auto orientation = get_printer_orientation(); + if (orientation == sla::Raster::roPortrait) { std::swap(w, h); std::swap(pw, ph); - - // XY flipping implicitly does an X mirror - mirror[X] = !mirror[X]; } res = sla::Raster::Resolution{pw, ph}; pxdim = sla::Raster::PixelDim{w / pw, h / ph}; - - gamma = m_printer_config.gamma_correction.getFloat(); - - m_printer.reset(new sla::SLARasterWriter(res, pxdim, mirror, gamma)); + sla::Raster::Trafo tr{orientation, mirror}; + tr.gamma = m_printer_config.gamma_correction.getFloat(); + + m_printer.reset(new sla::RasterWriter(res, pxdim, tr)); m_printer->set_config(m_full_print_config); return *m_printer; } @@ -1730,6 +1709,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vectorinvalidate_all_steps(); } else if (step == slaposSupportPoints) { - invalidated |= this->invalidate_steps({ slaposSupportTree, slaposBasePool, slaposSliceSupports }); + invalidated |= this->invalidate_steps({ slaposSupportTree, slaposPad, slaposSliceSupports }); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); } else if (step == slaposSupportTree) { - invalidated |= this->invalidate_steps({ slaposBasePool, slaposSliceSupports }); + invalidated |= this->invalidate_steps({ slaposPad, slaposSliceSupports }); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); - } else if (step == slaposBasePool) { + } else if (step == slaposPad) { invalidated |= this->invalidate_steps({slaposSliceSupports}); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); } else if (step == slaposSliceSupports) { @@ -1813,8 +1794,8 @@ double SLAPrintObject::get_elevation() const { // its walls but currently it is half of its thickness. Whatever it // will be in the future, we provide the config to the get_pad_elevation // method and we will have the correct value - sla::PoolConfig pcfg = make_pool_config(m_config); - if(!pcfg.embed_object) ret += sla::get_pad_elevation(pcfg); + sla::PadConfig pcfg = make_pad_cfg(m_config); + if(!pcfg.embed_object) ret += pcfg.required_elevation(); } return ret; @@ -1825,7 +1806,7 @@ double SLAPrintObject::get_current_elevation() const if (is_zero_elevation(m_config)) return 0.; bool has_supports = is_step_done(slaposSupportTree); - bool has_pad = is_step_done(slaposBasePool); + bool has_pad = is_step_done(slaposPad); if(!has_supports && !has_pad) return 0; @@ -1866,7 +1847,7 @@ const SliceRecord SliceRecord::EMPTY(0, std::nanf(""), 0.f); const std::vector& SLAPrintObject::get_support_points() const { - return m_supportdata? m_supportdata->support_points : EMPTY_SUPPORT_POINTS; + return m_supportdata? m_supportdata->pts : EMPTY_SUPPORT_POINTS; } const std::vector &SLAPrintObject::get_support_slices() const @@ -1896,7 +1877,7 @@ bool SLAPrintObject::has_mesh(SLAPrintObjectStep step) const switch (step) { case slaposSupportTree: return ! this->support_mesh().empty(); - case slaposBasePool: + case slaposPad: return ! this->pad_mesh().empty(); default: return false; @@ -1908,7 +1889,7 @@ TriangleMesh SLAPrintObject::get_mesh(SLAPrintObjectStep step) const switch (step) { case slaposSupportTree: return this->support_mesh(); - case slaposBasePool: + case slaposPad: return this->pad_mesh(); default: return TriangleMesh(); @@ -1917,18 +1898,20 @@ TriangleMesh SLAPrintObject::get_mesh(SLAPrintObjectStep step) const const TriangleMesh& SLAPrintObject::support_mesh() const { - if(m_config.supports_enable.getBool() && m_supportdata && - m_supportdata->support_tree_ptr) { - return m_supportdata->support_tree_ptr->merged_mesh(); - } - + sla::SupportTree::UPtr &stree = m_supportdata->support_tree_ptr; + + if(m_config.supports_enable.getBool() && m_supportdata && stree) + return stree->retrieve_mesh(sla::MeshType::Support); + return EMPTY_MESH; } const TriangleMesh& SLAPrintObject::pad_mesh() const { - if(m_config.pad_enable.getBool() && m_supportdata && m_supportdata->support_tree_ptr) - return m_supportdata->support_tree_ptr->get_pad(); + sla::SupportTree::UPtr &stree = m_supportdata->support_tree_ptr; + + if(m_config.pad_enable.getBool() && m_supportdata && stree) + return stree->retrieve_mesh(sla::MeshType::Pad); return EMPTY_MESH; } diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index a2bc1325a..2dc2a9040 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -21,7 +21,7 @@ enum SLAPrintObjectStep : unsigned int { slaposObjectSlice, slaposSupportPoints, slaposSupportTree, - slaposBasePool, + slaposPad, slaposSliceSupports, slaposCount }; @@ -54,7 +54,7 @@ public: bool is_left_handed() const { return m_left_handed; } struct Instance { - Instance(ObjectID instance_id, const Point &shift, float rotation) : instance_id(instance_id), shift(shift), rotation(rotation) {} + Instance(ObjectID inst_id, const Point &shft, float rot) : instance_id(inst_id), shift(shft), rotation(rot) {} bool operator==(const Instance &rhs) const { return this->instance_id == rhs.instance_id && this->shift == rhs.shift && this->rotation == rhs.rotation; } // ID of the corresponding ModelInstance. ObjectID instance_id; @@ -440,7 +440,7 @@ private: std::vector m_printer_input; // The printer itself - std::unique_ptr m_printer; + std::unique_ptr m_printer; // Estimated print time, material consumed. SLAPrintStatistics m_print_statistics; @@ -459,14 +459,13 @@ private: double status() const { return m_st; } } m_report_status; - sla::SLARasterWriter &init_printer(); + sla::RasterWriter &init_printer(); - inline sla::SLARasterWriter::Orientation get_printer_orientation() const + inline sla::Raster::Orientation get_printer_orientation() const { auto ro = m_printer_config.display_orientation.getInt(); - return ro == sla::SLARasterWriter::roPortrait ? - sla::SLARasterWriter::roPortrait : - sla::SLARasterWriter::roLandscape; + return ro == sla::Raster::roPortrait ? sla::Raster::roPortrait : + sla::Raster::roLandscape; } friend SLAPrintObject; diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index a6648f108..179b35f59 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -445,8 +445,8 @@ Polygons collect_region_slices_by_type(const Layer &layer, SurfaceType surface_t Polygons collect_slices_outer(const Layer &layer) { Polygons out; - out.reserve(out.size() + layer.slices.expolygons.size()); - for (const ExPolygon &expoly : layer.slices.expolygons) + out.reserve(out.size() + layer.slices.size()); + for (const ExPolygon &expoly : layer.slices) out.emplace_back(expoly.contour); return out; } @@ -907,9 +907,13 @@ namespace SupportMaterialInternal { polyline.extend_start(fw); polyline.extend_end(fw); // Is the straight perimeter segment supported at both sides? - if (lower_layer.slices.contains(polyline.first_point()) && lower_layer.slices.contains(polyline.last_point())) - // Offset a polyline into a thick line. - polygons_append(bridges, offset(polyline, 0.5f * w + 10.f)); + for (size_t i = 0; i < lower_layer.slices.size(); ++ i) + if (lower_layer.slices_bboxes[i].contains(polyline.first_point()) && lower_layer.slices_bboxes[i].contains(polyline.last_point()) && + lower_layer.slices[i].contains(polyline.first_point()) && lower_layer.slices[i].contains(polyline.last_point())) { + // Offset a polyline into a thick line. + polygons_append(bridges, offset(polyline, 0.5f * w + 10.f)); + break; + } } bridges = union_(bridges); } @@ -994,7 +998,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::top_contact_ // inflate the polygons over and over. Polygons &covered = buildplate_covered[layer_id]; covered = buildplate_covered[layer_id - 1]; - polygons_append(covered, offset(lower_layer.slices.expolygons, scale_(0.01))); + polygons_append(covered, offset(lower_layer.slices, scale_(0.01))); covered = union_(covered, false); // don't apply the safety offset. } } @@ -1023,7 +1027,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::top_contact_ Polygons contact_polygons; Polygons slices_margin_cached; float slices_margin_cached_offset = -1.; - Polygons lower_layer_polygons = (layer_id == 0) ? Polygons() : to_polygons(object.layers()[layer_id-1]->slices.expolygons); + Polygons lower_layer_polygons = (layer_id == 0) ? Polygons() : to_polygons(object.layers()[layer_id-1]->slices); // Offset of the lower layer, to trim the support polygons with to calculate dense supports. float no_interface_offset = 0.f; if (layer_id == 0) { @@ -1162,7 +1166,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::top_contact_ slices_margin_cached_offset = slices_margin_offset; slices_margin_cached = (slices_margin_offset == 0.f) ? lower_layer_polygons : - offset2(to_polygons(lower_layer.slices.expolygons), - no_interface_offset * 0.5f, slices_margin_offset + no_interface_offset * 0.5f, SUPPORT_SURFACES_OFFSET_PARAMETERS); + offset2(to_polygons(lower_layer.slices), - no_interface_offset * 0.5f, slices_margin_offset + no_interface_offset * 0.5f, SUPPORT_SURFACES_OFFSET_PARAMETERS); if (! buildplate_covered.empty()) { // Trim the inflated contact surfaces by the top surfaces as well. polygons_append(slices_margin_cached, buildplate_covered[layer_id]); @@ -1468,7 +1472,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::bottom_conta svg.draw(union_ex(top, false), "blue", 0.5f); svg.draw(union_ex(projection_raw, true), "red", 0.5f); svg.draw_outline(union_ex(projection_raw, true), "red", "blue", scale_(0.1f)); - svg.draw(layer.slices.expolygons, "green", 0.5f); + svg.draw(layer.slices, "green", 0.5f); } #endif /* SLIC3R_DEBUG */ @@ -1568,8 +1572,8 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::bottom_conta Polygons &layer_support_area = layer_support_areas[layer_id]; task_group.run([this, &projection, &projection_raw, &layer, &layer_support_area, layer_id] { // Remove the areas that touched from the projection that will continue on next, lower, top surfaces. - // Polygons trimming = union_(to_polygons(layer.slices.expolygons), touching, true); - Polygons trimming = offset(layer.slices.expolygons, float(SCALED_EPSILON)); + // Polygons trimming = union_(to_polygons(layer.slices), touching, true); + Polygons trimming = offset(layer.slices, float(SCALED_EPSILON)); projection = diff(projection_raw, trimming, false); #ifdef SLIC3R_DEBUG { @@ -2101,7 +2105,7 @@ void PrintObjectSupportMaterial::trim_support_layers_by_object( const Layer &object_layer = *object.layers()[i]; if (object_layer.print_z - object_layer.height > support_layer.print_z + gap_extra_above - EPSILON) break; - polygons_append(polygons_trimming, offset(object_layer.slices.expolygons, gap_xy_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + polygons_append(polygons_trimming, offset(object_layer.slices, gap_xy_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS)); } if (! m_slicing_params.soluble_interface) { // Collect all bottom surfaces, which will be extruded with a bridging flow. @@ -2214,7 +2218,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::generate_raf // Expand the bases of the support columns in the 1st layer. columns_base->polygons = diff( offset(columns_base->polygons, inflate_factor_1st_layer), - offset(m_object->layers().front()->slices.expolygons, (float)scale_(m_gap_xy), SUPPORT_SURFACES_OFFSET_PARAMETERS)); + offset(m_object->layers().front()->slices, (float)scale_(m_gap_xy), SUPPORT_SURFACES_OFFSET_PARAMETERS)); if (contacts != nullptr) columns_base->polygons = diff(columns_base->polygons, interface_polygons); } diff --git a/src/libslic3r/Tesselate.hpp b/src/libslic3r/Tesselate.hpp index 02e86eb33..2dbe6caa1 100644 --- a/src/libslic3r/Tesselate.hpp +++ b/src/libslic3r/Tesselate.hpp @@ -10,12 +10,15 @@ namespace Slic3r { class ExPolygon; typedef std::vector ExPolygons; -extern std::vector triangulate_expolygon_3d (const ExPolygon &poly, coordf_t z = 0, bool flip = false); -extern std::vector triangulate_expolygons_3d(const ExPolygons &polys, coordf_t z = 0, bool flip = false); -extern std::vector triangulate_expolygon_2d (const ExPolygon &poly, bool flip = false); -extern std::vector triangulate_expolygons_2d(const ExPolygons &polys, bool flip = false); -extern std::vector triangulate_expolygon_2f (const ExPolygon &poly, bool flip = false); -extern std::vector triangulate_expolygons_2f(const ExPolygons &polys, bool flip = false); +const bool constexpr NORMALS_UP = false; +const bool constexpr NORMALS_DOWN = true; + +extern std::vector triangulate_expolygon_3d (const ExPolygon &poly, coordf_t z = 0, bool flip = NORMALS_UP); +extern std::vector triangulate_expolygons_3d(const ExPolygons &polys, coordf_t z = 0, bool flip = NORMALS_UP); +extern std::vector triangulate_expolygon_2d (const ExPolygon &poly, bool flip = NORMALS_UP); +extern std::vector triangulate_expolygons_2d(const ExPolygons &polys, bool flip = NORMALS_UP); +extern std::vector triangulate_expolygon_2f (const ExPolygon &poly, bool flip = NORMALS_UP); +extern std::vector triangulate_expolygons_2f(const ExPolygons &polys, bool flip = NORMALS_UP); } // namespace Slic3r diff --git a/src/libslic3r/Time.cpp b/src/libslic3r/Time.cpp index 1f65189b8..063dbb41c 100644 --- a/src/libslic3r/Time.cpp +++ b/src/libslic3r/Time.cpp @@ -3,116 +3,232 @@ #include #include #include +#include +#include +#include -//#include -//#include +#ifdef _MSC_VER +#include +#endif - -#ifdef WIN32 - #define WIN32_LEAN_AND_MEAN - #include - #undef WIN32_LEAN_AND_MEAN -#endif /* WIN32 */ +#include "libslic3r/Utils.hpp" namespace Slic3r { namespace Utils { -namespace { +// "YYYY-MM-DD at HH:MM::SS [UTC]" +// If TimeZone::utc is used with the conversion functions, it will append the +// UTC letters to the end. +static const constexpr char *const SLICER_UTC_TIME_FMT = "%Y-%m-%d at %T"; -// FIXME: after we switch to gcc > 4.9 on the build server, please remove me -#if defined(__GNUC__) && __GNUC__ <= 4 -std::string put_time(const std::tm *tm, const char *fmt) +// ISO8601Z representation of time, without time zone info +static const constexpr char *const ISO8601Z_TIME_FMT = "%Y%m%dT%H%M%SZ"; + +static const char * get_fmtstr(TimeFormat fmt) { - static const constexpr int MAX_CHARS = 200; - char out[MAX_CHARS]; - std::strftime(out, MAX_CHARS, fmt, tm); - return out; + switch (fmt) { + case TimeFormat::gcode: return SLICER_UTC_TIME_FMT; + case TimeFormat::iso8601Z: return ISO8601Z_TIME_FMT; + } + + return ""; } -#else -auto put_time(const std::tm *tm, const char *fmt) -> decltype (std::put_time(tm, fmt)) + +namespace __get_put_time_emulation { +// FIXME: Implementations with the cpp11 put_time and get_time either not +// compile or do not pass the tests on the build server. If we switch to newer +// compilers, this namespace can be deleted with all its content. + +#ifdef _MSC_VER +// VS2019 implementation fails with ISO8601Z_TIME_FMT. +// VS2019 does not have std::strptime either. See bug: +// https://developercommunity.visualstudio.com/content/problem/140618/c-stdget-time-not-parsing-correctly.html + +static const std::map sscanf_fmt_map = { + {SLICER_UTC_TIME_FMT, "%04d-%02d-%02d at %02d:%02d:%02d"}, + {std::string(SLICER_UTC_TIME_FMT) + " UTC", "%04d-%02d-%02d at %02d:%02d:%02d UTC"}, + {ISO8601Z_TIME_FMT, "%04d%02d%02dT%02d%02d%02dZ"} +}; + +static const char * strptime(const char *str, const char *const fmt, std::tm *tms) { - return std::put_time(tm, fmt); + auto it = sscanf_fmt_map.find(fmt); + if (it == sscanf_fmt_map.end()) return nullptr; + + int y, M, d, h, m, s; + if (sscanf(str, it->second.c_str(), &y, &M, &d, &h, &m, &s) != 6) + return nullptr; + + tms->tm_year = y - 1900; // Year since 1900 + tms->tm_mon = M - 1; // 0-11 + tms->tm_mday = d; // 1-31 + tms->tm_hour = h; // 0-23 + tms->tm_min = m; // 0-59 + tms->tm_sec = s; // 0-61 (0-60 in C++11) + + return str; // WARN strptime return val should point after the parsed string } #endif +template +struct GetPutTimeReturnT { + Ttm *tms; + const char *fmt; + GetPutTimeReturnT(Ttm *_tms, const char *_fmt): tms(_tms), fmt(_fmt) {} +}; + +using GetTimeReturnT = GetPutTimeReturnT; +using PutTimeReturnT = GetPutTimeReturnT; + +std::ostream &operator<<(std::ostream &stream, PutTimeReturnT &&pt) +{ + static const constexpr int MAX_CHARS = 200; + char _out[MAX_CHARS]; + strftime(_out, MAX_CHARS, pt.fmt, pt.tms); + stream << _out; + return stream; } -time_t parse_time_ISO8601Z(const std::string &sdate) +inline PutTimeReturnT put_time(const std::tm *tms, const char *fmt) { - int y, M, d, h, m, s; - if (sscanf(sdate.c_str(), "%04d%02d%02dT%02d%02d%02dZ", &y, &M, &d, &h, &m, &s) != 6) - return time_t(-1); - struct tm tms; - tms.tm_year = y - 1900; // Year since 1900 - tms.tm_mon = M - 1; // 0-11 - tms.tm_mday = d; // 1-31 - tms.tm_hour = h; // 0-23 - tms.tm_min = m; // 0-59 - tms.tm_sec = s; // 0-61 (0-60 in C++11) + return {tms, fmt}; +} + +std::istream &operator>>(std::istream &stream, GetTimeReturnT &>) +{ + std::string line; + std::getline(stream, line); + + if (strptime(line.c_str(), gt.fmt, gt.tms) == nullptr) + stream.setstate(std::ios::failbit); + + return stream; +} + +inline GetTimeReturnT get_time(std::tm *tms, const char *fmt) +{ + return {tms, fmt}; +} + +} + +namespace { + +// Platform independent versions of gmtime and localtime. Completely thread +// safe only on Linux. MSVC gtime_s and localtime_s sets global errno thus not +// thread safe. +struct std::tm * _gmtime_r(const time_t *timep, struct tm *result) +{ + assert(timep != nullptr && result != nullptr); #ifdef WIN32 - return _mkgmtime(&tms); + time_t t = *timep; + gmtime_s(result, &t); + return result; +#else + return gmtime_r(timep, result); +#endif +} + +struct std::tm * _localtime_r(const time_t *timep, struct tm *result) +{ + assert(timep != nullptr && result != nullptr); +#ifdef WIN32 + // Converts a time_t time value to a tm structure, and corrects for the + // local time zone. + time_t t = *timep; + localtime_s(result, &t); + return result; +#else + return localtime_r(timep, result); +#endif +} + +time_t _mktime(const struct std::tm *tms) +{ + assert(tms != nullptr); + std::tm _tms = *tms; + return mktime(&_tms); +} + +time_t _timegm(const struct std::tm *tms) +{ + std::tm _tms = *tms; +#ifdef WIN32 + return _mkgmtime(&_tms); #else /* WIN32 */ - return timegm(&tms); + return timegm(&_tms); #endif /* WIN32 */ } -std::string format_time_ISO8601Z(time_t time) +std::string process_format(const char *fmt, TimeZone zone) { - struct tm tms; -#ifdef WIN32 - gmtime_s(&tms, &time); -#else - gmtime_r(&time, &tms); -#endif - char buf[128]; - sprintf(buf, "%04d%02d%02dT%02d%02d%02dZ", - tms.tm_year + 1900, - tms.tm_mon + 1, - tms.tm_mday, - tms.tm_hour, - tms.tm_min, - tms.tm_sec); - return buf; + std::string fmtstr(fmt); + + if (fmtstr == SLICER_UTC_TIME_FMT && zone == TimeZone::utc) + fmtstr += " UTC"; + + return fmtstr; } -std::string format_local_date_time(time_t time) -{ - struct tm tms; -#ifdef WIN32 - // Converts a time_t time value to a tm structure, and corrects for the local time zone. - localtime_s(&tms, &time); -#else - localtime_r(&time, &tms); -#endif - char buf[80]; - strftime(buf, 80, "%x %X", &tms); - return buf; -} +} // namespace time_t get_current_time_utc() -{ +{ using clk = std::chrono::system_clock; return clk::to_time_t(clk::now()); } -static std::string tm2str(const std::tm *tm, const char *fmt) +static std::string tm2str(const std::tm *tms, const char *fmt) { std::stringstream ss; - ss << put_time(tm, fmt); + ss.imbue(std::locale("C")); + ss << __get_put_time_emulation::put_time(tms, fmt); return ss.str(); } -std::string time2str(const time_t &t, TimeZone zone, const char *fmt) +std::string time2str(const time_t &t, TimeZone zone, TimeFormat fmt) { std::string ret; - + std::tm tms = {}; + tms.tm_isdst = -1; + std::string fmtstr = process_format(get_fmtstr(fmt), zone); + switch (zone) { - case TimeZone::local: ret = tm2str(std::localtime(&t), fmt); break; - case TimeZone::utc: ret = tm2str(std::gmtime(&t), fmt) + " UTC"; break; + case TimeZone::local: + ret = tm2str(_localtime_r(&t, &tms), fmtstr.c_str()); break; + case TimeZone::utc: + ret = tm2str(_gmtime_r(&t, &tms), fmtstr.c_str()); break; } - + return ret; } +static time_t str2time(std::istream &stream, TimeZone zone, const char *fmt) +{ + std::tm tms = {}; + tms.tm_isdst = -1; + + stream >> __get_put_time_emulation::get_time(&tms, fmt); + time_t ret = time_t(-1); + + switch (zone) { + case TimeZone::local: ret = _mktime(&tms); break; + case TimeZone::utc: ret = _timegm(&tms); break; + } + + if (stream.fail() || ret < time_t(0)) ret = time_t(-1); + + return ret; +} + +time_t str2time(const std::string &str, TimeZone zone, TimeFormat fmt) +{ + std::string fmtstr = process_format(get_fmtstr(fmt), zone).c_str(); + std::stringstream ss(str); + + ss.imbue(std::locale("C")); + return str2time(ss, zone, fmtstr.c_str()); +} + }; // namespace Utils }; // namespace Slic3r diff --git a/src/libslic3r/Time.hpp b/src/libslic3r/Time.hpp index b314e47f7..c03251986 100644 --- a/src/libslic3r/Time.hpp +++ b/src/libslic3r/Time.hpp @@ -7,41 +7,61 @@ namespace Slic3r { namespace Utils { -// Utilities to convert an UTC time_t to/from an ISO8601 time format, -// useful for putting timestamps into file and directory names. -// Returns (time_t)-1 on error. -time_t parse_time_ISO8601Z(const std::string &s); -std::string format_time_ISO8601Z(time_t time); - -// Format the date and time from an UTC time according to the active locales and a local time zone. -// TODO: make sure time2str is a suitable replacement -std::string format_local_date_time(time_t time); - -// There is no gmtime() on windows. +// Should be thread safe. time_t get_current_time_utc(); -const constexpr char *const SLIC3R_TIME_FMT = "%Y-%m-%d at %T"; - enum class TimeZone { local, utc }; +enum class TimeFormat { gcode, iso8601Z }; -std::string time2str(const time_t &t, TimeZone zone, const char *fmt = SLIC3R_TIME_FMT); +// time_t to string functions... -inline std::string current_time2str(TimeZone zone, const char *fmt = SLIC3R_TIME_FMT) +std::string time2str(const time_t &t, TimeZone zone, TimeFormat fmt); + +inline std::string time2str(TimeZone zone, TimeFormat fmt) { return time2str(get_current_time_utc(), zone, fmt); } -inline std::string current_local_time2str(const char * fmt = SLIC3R_TIME_FMT) +inline std::string utc_timestamp(time_t t) { - return current_time2str(TimeZone::local, fmt); + return time2str(t, TimeZone::utc, TimeFormat::gcode); } -inline std::string current_utc_time2str(const char * fmt = SLIC3R_TIME_FMT) +inline std::string utc_timestamp() { - return current_time2str(TimeZone::utc, fmt); + return utc_timestamp(get_current_time_utc()); } -}; // namespace Utils -}; // namespace Slic3r +// String to time_t function. Returns time_t(-1) if fails to parse the input. +time_t str2time(const std::string &str, TimeZone zone, TimeFormat fmt); + + +// ///////////////////////////////////////////////////////////////////////////// +// Utilities to convert an UTC time_t to/from an ISO8601 time format, +// useful for putting timestamps into file and directory names. +// Returns (time_t)-1 on error. + +// Use these functions to convert safely to and from the ISO8601 format on +// all platforms + +inline std::string iso_utc_timestamp(time_t t) +{ + return time2str(t, TimeZone::utc, TimeFormat::iso8601Z); +} + +inline std::string iso_utc_timestamp() +{ + return iso_utc_timestamp(get_current_time_utc()); +} + +inline time_t parse_iso_utc_timestamp(const std::string &str) +{ + return str2time(str, TimeZone::utc, TimeFormat::iso8601Z); +} + +// ///////////////////////////////////////////////////////////////////////////// + +} // namespace Utils +} // namespace Slic3r #endif /* slic3r_Utils_Time_hpp_ */ diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index c2fcb11bd..16d289d9c 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -236,7 +236,7 @@ bool TriangleMesh::needed_repair() const || this->stl.stats.backwards_edges > 0; } -void TriangleMesh::WriteOBJFile(const char* output_file) +void TriangleMesh::WriteOBJFile(const char* output_file) const { its_write_obj(this->its, output_file); } diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 81390b79b..86ca1625e 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -31,7 +31,7 @@ public: float volume(); void check_topology(); bool is_manifold() const { return this->stl.stats.connected_facets_3_edge == (int)this->stl.stats.number_of_facets; } - void WriteOBJFile(const char* output_file); + void WriteOBJFile(const char* output_file) const; void scale(float factor); void scale(const Vec3d &versor); void translate(float x, float y, float z); diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 5d847573d..9af2adcc6 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "libslic3r.h" diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 895efdb4d..8d2a6a866 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -543,7 +543,7 @@ std::string string_printf(const char *format, ...) std::string header_slic3r_generated() { - return std::string("generated by " SLIC3R_APP_NAME " " SLIC3R_VERSION " on " ) + Utils::current_utc_time2str(); + return std::string("generated by " SLIC3R_APP_NAME " " SLIC3R_VERSION " on " ) + Utils::utc_timestamp(); } unsigned get_current_pid() diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 7e17542b9..fa756f49d 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -66,7 +66,7 @@ void Snapshot::load_ini(const std::string &path) if (kvp.first == "id") this->id = kvp.second.data(); else if (kvp.first == "time_captured") { - this->time_captured = Slic3r::Utils::parse_time_ISO8601Z(kvp.second.data()); + this->time_captured = Slic3r::Utils::parse_iso_utc_timestamp(kvp.second.data()); if (this->time_captured == (time_t)-1) throw_on_parse_error("invalid timestamp"); } else if (kvp.first == "slic3r_version_captured") { @@ -165,7 +165,7 @@ void Snapshot::save_ini(const std::string &path) // Export the common "snapshot". c << std::endl << "[snapshot]" << std::endl; c << "id = " << this->id << std::endl; - c << "time_captured = " << Slic3r::Utils::format_time_ISO8601Z(this->time_captured) << std::endl; + c << "time_captured = " << Slic3r::Utils::iso_utc_timestamp(this->time_captured) << std::endl; c << "slic3r_version_captured = " << this->slic3r_version_captured.to_string() << std::endl; c << "comment = " << this->comment << std::endl; c << "reason = " << reason_string(this->reason) << std::endl; @@ -365,7 +365,7 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot: Snapshot snapshot; // Snapshot header. snapshot.time_captured = Slic3r::Utils::get_current_time_utc(); - snapshot.id = Slic3r::Utils::format_time_ISO8601Z(snapshot.time_captured); + snapshot.id = Slic3r::Utils::iso_utc_timestamp(snapshot.time_captured); snapshot.slic3r_version_captured = Slic3r::SEMVER; snapshot.comment = comment; snapshot.reason = reason; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 8094cdde1..086ba7a74 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -417,7 +417,7 @@ void GLVolume::render(int color_id, int detection_id, int worldmatrix_id) const } bool GLVolume::is_sla_support() const { return this->composite_id.volume_id == -int(slaposSupportTree); } -bool GLVolume::is_sla_pad() const { return this->composite_id.volume_id == -int(slaposBasePool); } +bool GLVolume::is_sla_pad() const { return this->composite_id.volume_id == -int(slaposPad); } std::vector GLVolumeCollection::load_object( const ModelObject *model_object, @@ -501,7 +501,7 @@ void GLVolumeCollection::load_object_auxiliary( TriangleMesh convex_hull = mesh.convex_hull_3d(); for (const std::pair& instance_idx : instances) { const ModelInstance& model_instance = *print_object->model_object()->instances[instance_idx.first]; - this->volumes.emplace_back(new GLVolume((milestone == slaposBasePool) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR)); + this->volumes.emplace_back(new GLVolume((milestone == slaposPad) ? GLVolume::SLA_PAD_COLOR : GLVolume::SLA_SUPPORT_COLOR)); GLVolume& v = *this->volumes.back(); v.indexed_vertex_array.load_mesh(mesh); v.indexed_vertex_array.finalize_geometry(opengl_initialized); @@ -1717,13 +1717,18 @@ static void thick_point_to_verts(const Vec3crd& point, point_to_indexed_vertex_array(point, width, height, volume.indexed_vertex_array); } +void _3DScene::extrusionentity_to_verts(const Polyline &polyline, float width, float height, float print_z, GLVolume& volume) +{ + if (polyline.size() >= 2) { + size_t num_segments = polyline.size() - 1; + thick_lines_to_verts(polyline.lines(), std::vector(num_segments, width), std::vector(num_segments, height), false, print_z, volume); + } +} + // Fill in the qverts and tverts with quads and triangles for the extrusion_path. void _3DScene::extrusionentity_to_verts(const ExtrusionPath &extrusion_path, float print_z, GLVolume &volume) { - Lines lines = extrusion_path.polyline.lines(); - 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, volume); + extrusionentity_to_verts(extrusion_path.polyline, extrusion_path.width, extrusion_path.height, print_z, volume); } // Fill in the qverts and tverts with quads and triangles for the extrusion_path. diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 8c1d68f77..8c5040eee 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -656,6 +656,7 @@ public: static void thick_lines_to_verts(const Lines& lines, const std::vector& widths, const std::vector& heights, bool closed, double top_z, GLVolume& volume); static void thick_lines_to_verts(const Lines3& lines, const std::vector& widths, const std::vector& heights, bool closed, GLVolume& volume); + static void extrusionentity_to_verts(const Polyline &polyline, float width, float height, float print_z, GLVolume& volume); static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, GLVolume& volume); static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, const Point& copy, GLVolume& volume); static void extrusionentity_to_verts(const ExtrusionLoop& extrusion_loop, float print_z, const Point& copy, GLVolume& volume); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index c9cdff162..f76f752f0 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -349,16 +349,18 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) toggle_field("pad_wall_thickness", pad_en); toggle_field("pad_wall_height", pad_en); + toggle_field("pad_brim_size", pad_en); toggle_field("pad_max_merge_distance", pad_en); // toggle_field("pad_edge_radius", supports_en); toggle_field("pad_wall_slope", pad_en); toggle_field("pad_around_object", pad_en); + toggle_field("pad_around_object_everywhere", pad_en); - bool has_suppad = pad_en && supports_en; - bool zero_elev = config->opt_bool("pad_around_object") && has_suppad; + bool zero_elev = config->opt_bool("pad_around_object") && pad_en; toggle_field("support_object_elevation", supports_en && !zero_elev); toggle_field("pad_object_gap", zero_elev); + toggle_field("pad_around_object_everywhere", zero_elev); toggle_field("pad_object_connector_stride", zero_elev); toggle_field("pad_object_connector_width", zero_elev); toggle_field("pad_object_connector_penetration", zero_elev); diff --git a/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/src/slic3r/GUI/ConfigSnapshotDialog.cpp index c89e4895e..d48dfccc9 100644 --- a/src/slic3r/GUI/ConfigSnapshotDialog.cpp +++ b/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -35,9 +35,14 @@ static wxString generate_html_row(const Config::Snapshot &snapshot, bool row_eve text += snapshot_active ? "#B3FFCB" : (row_even ? "#FFFFFF" : "#D5D5D5"); text += "\">"; text += ""; + + static const constexpr char *LOCALE_TIME_FMT = "%x %X"; + wxString datetime = wxDateTime(snapshot.time_captured).Format(LOCALE_TIME_FMT); + // Format the row header. - text += wxString("") + (snapshot_active ? _(L("Active")) + ": " : "") + - Utils::format_local_date_time(snapshot.time_captured) + ": " + format_reason(snapshot.reason); + text += wxString("") + (snapshot_active ? _(L("Active")) + ": " : "") + + datetime + ": " + format_reason(snapshot.reason); + if (! snapshot.comment.empty()) text += " (" + wxString::FromUTF8(snapshot.comment.data()) + ")"; text += "
"; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 9f5dd16e7..04ef6fd42 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1767,7 +1767,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re // SLA steps to pull the preview meshes for. typedef std::array SLASteps; - SLASteps sla_steps = { slaposSupportTree, slaposBasePool }; + SLASteps sla_steps = { slaposSupportTree, slaposPad }; struct SLASupportState { std::array::value> step; }; @@ -4987,13 +4987,13 @@ void GLCanvas3D::_load_gcode_extrusion_paths(const GCodePreviewData& preview_dat // helper functions to select data in dependence of the extrusion view type struct Helper { - static float path_filter(GCodePreviewData::Extrusion::EViewType type, const ExtrusionPath& path) + static float path_filter(GCodePreviewData::Extrusion::EViewType type, const GCodePreviewData::Extrusion::Path& path) { switch (type) { case GCodePreviewData::Extrusion::FeatureType: // The role here is used for coloring. - return (float)path.role(); + return (float)path.extrusion_role; case GCodePreviewData::Extrusion::Height: return path.height; case GCodePreviewData::Extrusion::Width: @@ -5071,15 +5071,15 @@ void GLCanvas3D::_load_gcode_extrusion_paths(const GCodePreviewData& preview_dat { std::vector num_paths_per_role(size_t(erCount), 0); for (const GCodePreviewData::Extrusion::Layer &layer : preview_data.extrusion.layers) - for (const ExtrusionPath &path : layer.paths) - ++ num_paths_per_role[size_t(path.role())]; + for (const GCodePreviewData::Extrusion::Path &path : layer.paths) + ++ num_paths_per_role[size_t(path.extrusion_role)]; std::vector> roles_values; roles_values.assign(size_t(erCount), std::vector()); for (size_t i = 0; i < roles_values.size(); ++ i) roles_values[i].reserve(num_paths_per_role[i]); for (const GCodePreviewData::Extrusion::Layer& layer : preview_data.extrusion.layers) - for (const ExtrusionPath& path : layer.paths) - roles_values[size_t(path.role())].emplace_back(Helper::path_filter(preview_data.extrusion.view_type, path)); + for (const GCodePreviewData::Extrusion::Path &path : layer.paths) + roles_values[size_t(path.extrusion_role)].emplace_back(Helper::path_filter(preview_data.extrusion.view_type, path)); roles_filters.reserve(size_t(erCount)); size_t num_buffers = 0; for (std::vector &values : roles_values) { @@ -5107,9 +5107,9 @@ void GLCanvas3D::_load_gcode_extrusion_paths(const GCodePreviewData& preview_dat // populates volumes for (const GCodePreviewData::Extrusion::Layer& layer : preview_data.extrusion.layers) { - for (const ExtrusionPath& path : layer.paths) + for (const GCodePreviewData::Extrusion::Path& path : layer.paths) { - std::vector> &filters = roles_filters[size_t(path.role())]; + std::vector> &filters = roles_filters[size_t(path.extrusion_role)]; auto key = std::make_pair(Helper::path_filter(preview_data.extrusion.view_type, path), nullptr); auto it_filter = std::lower_bound(filters.begin(), filters.end(), key); assert(it_filter != filters.end() && key.first == it_filter->first); @@ -5119,7 +5119,7 @@ void GLCanvas3D::_load_gcode_extrusion_paths(const GCodePreviewData& preview_dat vol.offsets.push_back(vol.indexed_vertex_array.quad_indices.size()); vol.offsets.push_back(vol.indexed_vertex_array.triangle_indices.size()); - _3DScene::extrusionentity_to_verts(path, layer.z, vol); + _3DScene::extrusionentity_to_verts(path.polyline, path.width, path.height, layer.z, vol); } // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one. for (std::vector> &filters : roles_filters) { @@ -5340,8 +5340,8 @@ void GLCanvas3D::_load_sla_shells() m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id(); if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree)) add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true); - if (obj->is_step_done(slaposBasePool) && obj->has_mesh(slaposBasePool)) - add_volume(*obj, -int(slaposBasePool), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false); + if (obj->is_step_done(slaposPad) && obj->has_mesh(slaposPad)) + add_volume(*obj, -int(slaposPad), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false); } double shift_z = obj->get_current_elevation(); for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) { diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp index 55ca5f723..62129cfc0 100644 --- a/src/slic3r/GUI/GLTexture.cpp +++ b/src/slic3r/GUI/GLTexture.cpp @@ -107,8 +107,8 @@ void GLTexture::Compressor::compress() break; // stb_dxt library, despite claiming that the needed size of the destination buffer is equal to (source buffer size)/4, - // crashes if doing so, so we start with twice the required size - level.compressed_data = std::vector(level.w * level.h * 2, 0); + // crashes if doing so, requiring a minimum of 16 bytes and up to a third of the source buffer size, so we set the destination buffer initial size to be half the source buffer size + level.compressed_data = std::vector(std::max((unsigned int)16, level.w * level.h * 2), 0); int compressed_size = 0; rygCompress(level.compressed_data.data(), level.src_data.data(), level.w, level.h, 1, compressed_size); level.compressed_data.resize(compressed_size); @@ -455,8 +455,7 @@ bool GLTexture::load_from_png(const std::string& filename, bool use_mipmaps, ECo int lod_w = m_width; int lod_h = m_height; GLint level = 0; - // we do not need to generate all levels down to 1x1 - while ((lod_w > 16) || (lod_h > 16)) + while ((lod_w > 1) || (lod_h > 1)) { ++level; @@ -600,8 +599,7 @@ bool GLTexture::load_from_svg(const std::string& filename, bool use_mipmaps, boo int lod_w = m_width; int lod_h = m_height; GLint level = 0; - // we do not need to generate all levels down to 1x1 - while ((lod_w > 16) || (lod_h > 16)) + while ((lod_w > 1) || (lod_h > 1)) { ++level; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 0835a0d40..9906d3751 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -267,7 +267,8 @@ void ObjectList::create_objects_ctrl() wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE); // column Extruder of the view control: - AppendColumn(create_objects_list_extruder_column(4)); + AppendColumn(new wxDataViewColumn(_(L("Extruder")), new BitmapChoiceRenderer(), + colExtruder, 8*em, wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE)); // column ItemEditing of the view control: AppendBitmapColumn(_(L("Editing")), colEditing, wxDATAVIEW_CELL_INERT, 3*em, @@ -434,19 +435,6 @@ DynamicPrintConfig& ObjectList::get_item_config(const wxDataViewItem& item) cons (*m_objects)[obj_idx]->config; } -wxDataViewColumn* ObjectList::create_objects_list_extruder_column(size_t extruders_count) -{ - wxArrayString choices; - choices.Add(_(L("default"))); - for (int i = 1; i <= extruders_count; ++i) - choices.Add(wxString::Format("%d", i)); - wxDataViewChoiceRenderer *c = - new wxDataViewChoiceRenderer(choices, wxDATAVIEW_CELL_EDITABLE, wxALIGN_CENTER_HORIZONTAL); - wxDataViewColumn* column = new wxDataViewColumn(_(L("Extruder")), c, colExtruder, - 8*wxGetApp().em_unit()/*80*/, wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE); - return column; -} - void ObjectList::update_extruder_values_for_items(const size_t max_extruder) { for (size_t i = 0; i < m_objects->size(); ++i) @@ -462,7 +450,7 @@ void ObjectList::update_extruder_values_for_items(const size_t max_extruder) else extruder = wxString::Format("%d", object->config.option("extruder")->value); - m_objects_model->SetValue(extruder, item, colExtruder); + m_objects_model->SetExtruder(extruder, item); if (object->volumes.size() > 1) { for (size_t id = 0; id < object->volumes.size(); id++) { @@ -474,7 +462,7 @@ void ObjectList::update_extruder_values_for_items(const size_t max_extruder) else extruder = wxString::Format("%d", object->volumes[id]->config.option("extruder")->value); - m_objects_model->SetValue(extruder, item, colExtruder); + m_objects_model->SetExtruder(extruder, item); } } } @@ -486,19 +474,13 @@ void ObjectList::update_objects_list_extruder_column(size_t extruders_count) if (printer_technology() == ptSLA) extruders_count = 1; - wxDataViewChoiceRenderer* ch_render = dynamic_cast(GetColumn(colExtruder)->GetRenderer()); - if (ch_render->GetChoices().GetCount() - 1 == extruders_count) - return; - m_prevent_update_extruder_in_config = true; if (m_objects && extruders_count > 1) update_extruder_values_for_items(extruders_count); - // delete old extruder column - DeleteColumn(GetColumn(colExtruder)); - // insert new created extruder column - InsertColumn(colExtruder, create_objects_list_extruder_column(extruders_count)); + update_extruder_colors(); + // set show/hide for this column set_extruder_column_hidden(extruders_count <= 1); //a workaround for a wrong last column width updating under OSX @@ -507,6 +489,11 @@ void ObjectList::update_objects_list_extruder_column(size_t extruders_count) m_prevent_update_extruder_in_config = false; } +void ObjectList::update_extruder_colors() +{ + m_objects_model->UpdateColumValues(colExtruder); +} + void ObjectList::set_extruder_column_hidden(const bool hide) const { GetColumn(colExtruder)->SetHidden(hide); @@ -535,14 +522,10 @@ void ObjectList::update_extruder_in_config(const wxDataViewItem& item) m_config = &get_item_config(item); } - wxVariant variant; - m_objects_model->GetValue(variant, item, colExtruder); - const wxString selection = variant.GetString(); - - if (!m_config || selection.empty()) + if (!m_config) return; - const int extruder = /*selection.size() > 1 ? 0 : */atoi(selection.c_str()); + const int extruder = m_objects_model->GetExtruderNumber(item); m_config->set_key_value("extruder", new ConfigOptionInt(extruder)); // update scene @@ -805,6 +788,9 @@ void ObjectList::list_manipulation(bool evt_context_menu/* = false*/) const wxPoint pt = get_mouse_position_in_control(); HitTest(pt, item, col); + if (m_extruder_editor) + m_extruder_editor->Hide(); + /* Note: Under OSX right click doesn't send "selection changed" event. * It means that Selection() will be return still previously selected item. * Thus under OSX we should force UnselectAll(), when item and col are nullptr, @@ -853,6 +839,9 @@ void ObjectList::list_manipulation(bool evt_context_menu/* = false*/) fix_through_netfabb(); } } + // workaround for extruder editing under OSX + else if (wxOSX && evt_context_menu && title == _("Extruder")) + extruder_editing(); #ifndef __WXMSW__ GetMainWindow()->SetToolTip(""); // hide tooltip @@ -894,6 +883,74 @@ void ObjectList::show_context_menu(const bool evt_context_menu) wxGetApp().plater()->PopupMenu(menu); } +void ObjectList::extruder_editing() +{ + wxDataViewItem item = GetSelection(); + if (!item || !(m_objects_model->GetItemType(item) & (itVolume | itObject))) + return; + + std::vector icons = get_extruder_color_icons(); + if (icons.empty()) + return; + + const int column_width = GetColumn(colExtruder)->GetWidth() + wxSystemSettings::GetMetric(wxSYS_VSCROLL_X) + 5; + + wxPoint pos = get_mouse_position_in_control(); + wxSize size = wxSize(column_width, -1); + pos.x = GetColumn(colName)->GetWidth() + GetColumn(colPrint)->GetWidth() + 5; + pos.y -= GetTextExtent("m").y; + + if (!m_extruder_editor) + m_extruder_editor = new wxBitmapComboBox(this, wxID_ANY, wxEmptyString, pos, size, + 0, nullptr, wxCB_READONLY); + else + { + m_extruder_editor->SetPosition(pos); + m_extruder_editor->SetMinSize(size); + m_extruder_editor->SetSize(size); + m_extruder_editor->Clear(); + m_extruder_editor->Show(); + } + + int i = 0; + for (wxBitmap* bmp : icons) { + if (i == 0) { + m_extruder_editor->Append(_(L("default")), *bmp); + ++i; + } + + m_extruder_editor->Append(wxString::Format("%d", i), *bmp); + ++i; + } + m_extruder_editor->SetSelection(m_objects_model->GetExtruderNumber(item)); + + auto set_extruder = [this]() + { + wxDataViewItem item = GetSelection(); + if (!item) return; + + const int selection = m_extruder_editor->GetSelection(); + if (selection >= 0) + m_objects_model->SetExtruder(m_extruder_editor->GetString(selection), item); + + m_extruder_editor->Hide(); + }; + + // to avoid event propagation to other sidebar items + m_extruder_editor->Bind(wxEVT_COMBOBOX, [set_extruder](wxCommandEvent& evt) + { + set_extruder(); + evt.StopPropagation(); + }); + /* + m_extruder_editor->Bind(wxEVT_KILL_FOCUS, [set_extruder](wxFocusEvent& evt) + { + set_extruder(); + evt.Skip(); + });*/ + +} + void ObjectList::copy() { // if (m_selection_mode & smLayer) @@ -2341,7 +2398,8 @@ void ObjectList::part_selection_changed() wxGetApp().obj_manipul()->get_og()->set_name(" " + og_name + " "); if (item) { - wxGetApp().obj_manipul()->get_og()->set_value("object_name", m_objects_model->GetName(item)); + // wxGetApp().obj_manipul()->get_og()->set_value("object_name", m_objects_model->GetName(item)); + wxGetApp().obj_manipul()->update_item_name(m_objects_model->GetName(item)); wxGetApp().obj_manipul()->update_warning_icon_state(get_mesh_errors_list(obj_idx, volume_id)); } } @@ -2547,7 +2605,7 @@ void ObjectList::delete_from_model_and_list(const std::vector& it (*m_objects)[item->obj_idx]->config.has("extruder")) { const wxString extruder = wxString::Format("%d", (*m_objects)[item->obj_idx]->config.option("extruder")->value); - m_objects_model->SetValue(extruder, m_objects_model->GetItemById(item->obj_idx), colExtruder); + m_objects_model->SetExtruder(extruder, m_objects_model->GetItemById(item->obj_idx)); } wxGetApp().plater()->canvas3D()->ensure_on_bed(item->obj_idx); } @@ -2881,6 +2939,7 @@ int ObjectList::get_selected_layers_range_idx() const void ObjectList::update_selections() { + if (m_extruder_editor) m_extruder_editor->Hide(); const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); wxDataViewItemArray sels; @@ -3822,7 +3881,7 @@ void ObjectList::set_extruder_for_selected_items(const int extruder) const /* We can change extruder for Object/Volume only. * So, if Instance is selected, get its Object item and change it */ - m_objects_model->SetValue(extruder_str, type & itInstance ? m_objects_model->GetTopParent(item) : item, colExtruder); + m_objects_model->SetExtruder(extruder_str, type & itInstance ? m_objects_model->GetTopParent(item) : item); const int obj_idx = type & itObject ? m_objects_model->GetIdByItem(item) : m_objects_model->GetIdByItem(m_objects_model->GetTopParent(item)); diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index 4dd618a90..87be25ed9 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -12,6 +12,7 @@ #include "wxExtensions.hpp" class wxBoxSizer; +class wxBitmapComboBox; class wxMenuItem; class ObjectDataViewModel; class MenuWithSeparators; @@ -140,6 +141,8 @@ private: DynamicPrintConfig *m_config {nullptr}; std::vector *m_objects{ nullptr }; + wxBitmapComboBox *m_extruder_editor { nullptr }; + std::vector m_bmp_vector; t_layer_config_ranges m_layer_config_ranges_cache; @@ -183,8 +186,8 @@ public: void create_objects_ctrl(); void create_popup_menus(); - wxDataViewColumn* create_objects_list_extruder_column(size_t extruders_count); void update_objects_list_extruder_column(size_t extruders_count); + void update_extruder_colors(); // show/hide "Extruder" column for Objects List void set_extruder_column_hidden(const bool hide) const; // update extruder in current config @@ -210,6 +213,7 @@ public: void selection_changed(); void show_context_menu(const bool evt_context_menu); + void extruder_editing(); #ifndef __WXOSX__ void key_event(wxKeyEvent& event); #endif /* __WXOSX__ */ diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 2295ac0b6..747cec0f9 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -112,7 +112,310 @@ void msw_rescale_word_local_combo(wxBitmapComboBox* combo) combo->SetValue(selection); } +static void set_font_and_background_style(wxWindow* win, const wxFont& font) +{ + win->SetFont(font); + win->SetBackgroundStyle(wxBG_STYLE_PAINT); +} +ObjectManipulation::ObjectManipulation(wxWindow* parent) : + OG_Settings(parent, true) +#ifndef __APPLE__ + , m_focused_option("") +#endif // __APPLE__ +{ + m_manifold_warning_bmp = ScalableBitmap(parent, "exclamation"); + + // Load bitmaps to be used for the mirroring buttons: + m_mirror_bitmap_on = ScalableBitmap(parent, "mirroring_on"); + m_mirror_bitmap_off = ScalableBitmap(parent, "mirroring_off"); + m_mirror_bitmap_hidden = ScalableBitmap(parent, "mirroring_transparent.png"); + + const int border = wxOSX ? 0 : 4; + const int em = wxGetApp().em_unit(); + m_main_grid_sizer = new wxFlexGridSizer(2, 3, 3); // "Name/label", "String name / Editors" + m_main_grid_sizer->SetFlexibleDirection(wxBOTH); + + // Add "Name" label with warning icon + auto sizer = new wxBoxSizer(wxHORIZONTAL); + + m_fix_throught_netfab_bitmap = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap); + if (is_windows10()) + m_fix_throught_netfab_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()) + return; + + wxGetApp().obj_list()->fix_through_netfabb(); + update_warning_icon_state(wxGetApp().obj_list()->get_mesh_errors_list()); + }); + + sizer->Add(m_fix_throught_netfab_bitmap); + + auto name_label = new wxStaticText(m_parent, wxID_ANY, _(L("Name"))+":"); + set_font_and_background_style(name_label, wxGetApp().normal_font()); + name_label->SetToolTip(_(L("Object name"))); + sizer->Add(name_label); + + m_main_grid_sizer->Add(sizer); + + // Add name of the item + const wxSize name_size = wxSize(20 * em, wxDefaultCoord); + m_item_name = new wxStaticText(m_parent, wxID_ANY, "", wxDefaultPosition, name_size, wxST_ELLIPSIZE_MIDDLE); + set_font_and_background_style(m_item_name, wxGetApp().bold_font()); + + m_main_grid_sizer->Add(m_item_name, 0, wxEXPAND); + + // Add labels grid sizer + m_labels_grid_sizer = new wxFlexGridSizer(1, 3, 3); // "Name/label", "String name / Editors" + m_labels_grid_sizer->SetFlexibleDirection(wxBOTH); + + // Add world local combobox + m_word_local_combo = create_word_local_combo(parent); + m_word_local_combo->Bind(wxEVT_COMBOBOX, ([this](wxCommandEvent& evt) { + this->set_world_coordinates(evt.GetSelection() != 1); + }), m_word_local_combo->GetId()); + + // Small trick to correct layouting in different view_mode : + // Show empty string of a same height as a m_word_local_combo, when m_word_local_combo is hidden + m_word_local_combo_sizer = new wxBoxSizer(wxHORIZONTAL); + m_empty_str = new wxStaticText(parent, wxID_ANY, ""); + m_word_local_combo_sizer->Add(m_word_local_combo); + m_word_local_combo_sizer->Add(m_empty_str); + m_word_local_combo_sizer->SetMinSize(wxSize(-1, m_word_local_combo->GetBestHeight(-1))); + m_labels_grid_sizer->Add(m_word_local_combo_sizer); + + // Text trick to grid sizer layout: + // Height of labels should be equivalent to the edit boxes + int height = wxTextCtrl(parent, wxID_ANY, "Br").GetBestHeight(-1); +#ifdef __WXGTK__ + // On Linux button with bitmap has bigger height then regular button or regular TextCtrl + // It can cause a wrong alignment on show/hide of a reset buttons + const int bmp_btn_height = ScalableButton(parent, wxID_ANY, "undo") .GetBestHeight(-1); + if (bmp_btn_height > height) + height = bmp_btn_height; +#endif //__WXGTK__ + + auto add_label = [this, height](wxStaticText** label, const std::string& name, wxSizer* reciver = nullptr) + { + *label = new wxStaticText(m_parent, wxID_ANY, _(name) + ":"); + set_font_and_background_style(m_move_Label, wxGetApp().normal_font()); + + wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->SetMinSize(wxSize(-1, height)); + sizer->Add(*label, 0, wxALIGN_CENTER_VERTICAL); + + if (reciver) + reciver->Add(sizer); + else + m_labels_grid_sizer->Add(sizer); + + m_rescalable_sizers.push_back(sizer); + }; + + // Add labels + add_label(&m_move_Label, L("Position")); + add_label(&m_rotate_Label, L("Rotation")); + + // additional sizer for lock and labels "Scale" & "Size" + sizer = new wxBoxSizer(wxHORIZONTAL); + + m_lock_bnt = new LockButton(parent, wxID_ANY); + m_lock_bnt->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { + event.Skip(); + wxTheApp->CallAfter([this]() { set_uniform_scaling(m_lock_bnt->IsLocked()); }); + }); + sizer->Add(m_lock_bnt, 0, wxALIGN_CENTER_VERTICAL); + + auto v_sizer = new wxGridSizer(1, 3, 3); + + add_label(&m_scale_Label, L("Scale"), v_sizer); + wxStaticText* size_Label {nullptr}; + add_label(&size_Label, L("Size"), v_sizer); + if (wxOSX) set_font_and_background_style(size_Label, wxGetApp().normal_font()); + + sizer->Add(v_sizer, 0, wxLEFT, border); + m_labels_grid_sizer->Add(sizer); + m_main_grid_sizer->Add(m_labels_grid_sizer, 0, wxEXPAND); + + + // Add editors grid sizer + wxFlexGridSizer* editors_grid_sizer = new wxFlexGridSizer(5, 3, 3); // "Name/label", "String name / Editors" + editors_grid_sizer->SetFlexibleDirection(wxBOTH); + + // Add Axes labels with icons + static const char axes[] = { 'X', 'Y', 'Z' }; + for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) { + const char label = axes[axis_idx]; + + wxStaticText* axis_name = new wxStaticText(m_parent, wxID_ANY, wxString(label)); + set_font_and_background_style(axis_name, wxGetApp().bold_font()); + + sizer = new wxBoxSizer(wxHORIZONTAL); + // Under OSX we use font, smaller than default font, so + // there is a next trick for an equivalent layout of coordinates combobox and axes labels in they own sizers + if (wxOSX) + sizer->SetMinSize(-1, m_word_local_combo->GetBestHeight(-1)); + sizer->Add(axis_name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, border); + + // We will add a button to toggle mirroring to each axis: + auto btn = new ScalableButton(parent, wxID_ANY, "mirroring_off", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER | wxTRANSPARENT_WINDOW); + btn->SetToolTip(wxString::Format(_(L("Toggle %c axis mirroring")), (int)label)); + btn->SetBitmapDisabled_(m_mirror_bitmap_hidden); + + m_mirror_buttons[axis_idx].first = btn; + m_mirror_buttons[axis_idx].second = mbShown; + + sizer->AddStretchSpacer(2); + sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL); + + btn->Bind(wxEVT_BUTTON, [this, axis_idx](wxCommandEvent&) { + Axis axis = (Axis)(axis_idx + X); + if (m_mirror_buttons[axis_idx].second == mbHidden) + return; + + GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); + Selection& selection = canvas->get_selection(); + + if (selection.is_single_volume() || selection.is_single_modifier()) { + GLVolume* volume = const_cast(selection.get_volume(*selection.get_volume_idxs().begin())); + volume->set_volume_mirror(axis, -volume->get_volume_mirror(axis)); + } + else if (selection.is_single_full_instance()) { + for (unsigned int idx : selection.get_volume_idxs()) { + GLVolume* volume = const_cast(selection.get_volume(idx)); + volume->set_instance_mirror(axis, -volume->get_instance_mirror(axis)); + } + } + else + return; + + // Update mirroring at the GLVolumes. + selection.synchronize_unselected_instances(Selection::SYNC_ROTATION_GENERAL); + selection.synchronize_unselected_volumes(); + // Copy mirroring values from GLVolumes into Model (ModelInstance / ModelVolume), trigger background processing. + canvas->do_mirror(L("Set Mirror")); + UpdateAndShow(true); + }); + + editors_grid_sizer->Add(sizer, 0, wxALIGN_CENTER_HORIZONTAL); + } + + editors_grid_sizer->AddStretchSpacer(1); + editors_grid_sizer->AddStretchSpacer(1); + + // add EditBoxes + auto add_edit_boxes = [this, editors_grid_sizer](const std::string& opt_key, int axis) + { + ManipulationEditor* editor = new ManipulationEditor(this, opt_key, axis); + m_editors.push_back(editor); + + editors_grid_sizer->Add(editor, 0, wxALIGN_CENTER_VERTICAL); + }; + + // add Units + auto add_unit_text = [this, parent, editors_grid_sizer, height](std::string unit) + { + wxStaticText* unit_text = new wxStaticText(parent, wxID_ANY, _(unit)); + set_font_and_background_style(unit_text, wxGetApp().normal_font()); + + // Unit text should be the same height as labels + wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->SetMinSize(wxSize(-1, height)); + sizer->Add(unit_text, 0, wxALIGN_CENTER_VERTICAL); + + editors_grid_sizer->Add(sizer); + m_rescalable_sizers.push_back(sizer); + }; + + for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) + add_edit_boxes("position", axis_idx); + add_unit_text(L("mm")); + + // Add drop to bed button + m_drop_to_bed_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "drop_to_bed")); + m_drop_to_bed_button->SetToolTip(_(L("Drop to bed"))); + m_drop_to_bed_button->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) { + // ??? + GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); + Selection& selection = canvas->get_selection(); + + if (selection.is_single_volume() || selection.is_single_modifier()) { + const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + + const Geometry::Transformation& instance_trafo = volume->get_instance_transformation(); + Vec3d diff = m_cache.position - instance_trafo.get_matrix(true).inverse() * Vec3d(0., 0., get_volume_min_z(volume)); + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Drop to bed"))); + change_position_value(0, diff.x()); + change_position_value(1, diff.y()); + change_position_value(2, diff.z()); + } + }); + editors_grid_sizer->Add(m_drop_to_bed_button); + + for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) + add_edit_boxes("rotation", axis_idx); + add_unit_text("°"); + + // Add reset rotation button + m_reset_rotation_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "undo")); + m_reset_rotation_button->SetToolTip(_(L("Reset rotation"))); + m_reset_rotation_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); + Selection& selection = canvas->get_selection(); + + if (selection.is_single_volume() || selection.is_single_modifier()) { + GLVolume* volume = const_cast(selection.get_volume(*selection.get_volume_idxs().begin())); + volume->set_volume_rotation(Vec3d::Zero()); + } + else if (selection.is_single_full_instance()) { + for (unsigned int idx : selection.get_volume_idxs()) { + GLVolume* volume = const_cast(selection.get_volume(idx)); + volume->set_instance_rotation(Vec3d::Zero()); + } + } + else + return; + + // Update rotation at the GLVolumes. + selection.synchronize_unselected_instances(Selection::SYNC_ROTATION_GENERAL); + selection.synchronize_unselected_volumes(); + // Copy rotation values from GLVolumes into Model (ModelInstance / ModelVolume), trigger background processing. + canvas->do_rotate(L("Reset Rotation")); + + UpdateAndShow(true); + }); + editors_grid_sizer->Add(m_reset_rotation_button); + + for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) + add_edit_boxes("scale", axis_idx); + add_unit_text("%"); + + // Add reset scale button + m_reset_scale_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "undo")); + m_reset_scale_button->SetToolTip(_(L("Reset scale"))); + m_reset_scale_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Reset scale"))); + change_scale_value(0, 100.); + change_scale_value(1, 100.); + change_scale_value(2, 100.); + }); + editors_grid_sizer->Add(m_reset_scale_button); + + for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) + add_edit_boxes("size", axis_idx); + add_unit_text("mm"); + editors_grid_sizer->AddStretchSpacer(1); + + m_main_grid_sizer->Add(editors_grid_sizer, 1, wxEXPAND); + + m_og->sizer->Clear(true); + m_og->sizer->Add(m_main_grid_sizer, 1, wxEXPAND | wxALL, border); +} + +/* ObjectManipulation::ObjectManipulation(wxWindow* parent) : OG_Settings(parent, true) #ifndef __APPLE__ @@ -180,7 +483,7 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : line = Line{ "", "" }; def.label = ""; def.type = coString; - def.width = field_width - mirror_btn_width;//field_width/*50*/; + def.width = field_width - mirror_btn_width; // Load bitmaps to be used for the mirroring buttons: m_mirror_bitmap_on = ScalableBitmap(parent, "mirroring_on"); @@ -254,7 +557,7 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : ConfigOptionDef def; def.type = coFloat; def.set_default_value(new ConfigOptionFloat(0.0)); - def.width = field_width/*50*/; + def.width = field_width; if (option_name == "Scale") { // Add "uniform scaling" button in front of "Scale" option @@ -395,7 +698,7 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : win->SetMinSize(create_scaled_bitmap(m_parent, "one_layer_lock_on.png").GetSize()); }; } - +*/ void ObjectManipulation::Show(const bool show) @@ -407,9 +710,9 @@ void ObjectManipulation::Show(const bool show) if (show && wxGetApp().get_mode() != comSimple) { // Show the label and the name of the STL in simple mode only. // Label "Name: " - m_og->get_grid_sizer()->Show(size_t(0), false); + /*m_og->get_grid_sizer()*/m_main_grid_sizer->Show(size_t(0), false); // The actual name of the STL. - m_og->get_grid_sizer()->Show(size_t(1), false); + /*m_og->get_grid_sizer()*/m_main_grid_sizer->Show(size_t(1), false); } } @@ -417,6 +720,7 @@ void ObjectManipulation::Show(const bool show) // Show the "World Coordinates" / "Local Coordintes" Combo in Advanced / Expert mode only. bool show_world_local_combo = wxGetApp().plater()->canvas3D()->get_selection().is_single_full_instance() && wxGetApp().get_mode() != comSimple; m_word_local_combo->Show(show_world_local_combo); + m_empty_str->Show(!show_world_local_combo); } } @@ -522,12 +826,14 @@ void ObjectManipulation::update_if_dirty() if (label_cache != new_label_localized) { label_cache = new_label_localized; widget->SetLabel(new_label_localized); + if (wxOSX) set_font_and_background_style(widget, wxGetApp().normal_font()); } }; update_label(m_cache.move_label_string, m_new_move_label_string, m_move_Label); update_label(m_cache.rotate_label_string, m_new_rotate_label_string, m_rotate_Label); update_label(m_cache.scale_label_string, m_new_scale_label_string, m_scale_Label); + /* char axis[2] = "x"; for (int i = 0; i < 3; ++ i, ++ axis[0]) { auto update = [this, i, &axis](Vec3d &cached, Vec3d &cached_rounded, const char *key, const Vec3d &new_value) { @@ -545,6 +851,34 @@ void ObjectManipulation::update_if_dirty() update(m_cache.size, m_cache.size_rounded, "size_", m_new_size); update(m_cache.rotation, m_cache.rotation_rounded, "rotation_", m_new_rotation); } + */ + + enum ManipulationEditorKey + { + mePosition = 0, + meRotation, + meScale, + meSize + }; + + for (int i = 0; i < 3; ++ i) { + auto update = [this, i](Vec3d &cached, Vec3d &cached_rounded, ManipulationEditorKey key_id, const Vec3d &new_value) { + wxString new_text = double_to_string(new_value(i), 2); + double new_rounded; + new_text.ToDouble(&new_rounded); + if (std::abs(cached_rounded(i) - new_rounded) > EPSILON) { + cached_rounded(i) = new_rounded; + const int id = key_id*3+i; + if (id >= 0) m_editors[id]->set_value(new_text); + } + cached(i) = new_value(i); + }; + update(m_cache.position, m_cache.position_rounded, mePosition, m_new_position); + update(m_cache.scale, m_cache.scale_rounded, meScale, m_new_scale); + update(m_cache.size, m_cache.size_rounded, meSize, m_new_size); + update(m_cache.rotation, m_cache.rotation_rounded, meRotation, m_new_rotation); + } + if (selection.requires_uniform_scale()) { m_lock_bnt->SetLock(true); @@ -687,6 +1021,11 @@ void ObjectManipulation::emulate_kill_focus() } #endif // __APPLE__ +void ObjectManipulation::update_item_name(const wxString& item_name) +{ + m_item_name->SetLabel(item_name); +} + void ObjectManipulation::update_warning_icon_state(const wxString& tooltip) { m_fix_throught_netfab_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp()); @@ -851,6 +1190,21 @@ void ObjectManipulation::on_change(t_config_option_key opt_key, const boost::any change_size_value(axis, new_value); } +void ObjectManipulation::on_change(const std::string& opt_key, int axis, double new_value) +{ + if (!m_cache.is_valid()) + return; + + if (opt_key == "position") + change_position_value(axis, new_value); + else if (opt_key == "rotation") + change_rotation_value(axis, new_value); + else if (opt_key == "scale") + change_scale_value(axis, new_value); + else if (opt_key == "size") + change_size_value(axis, new_value); +} + void ObjectManipulation::on_fill_empty_value(const std::string& opt_key) { // needed to hide the visual hints in 3D scene @@ -923,7 +1277,10 @@ void ObjectManipulation::set_uniform_scaling(const bool new_value) void ObjectManipulation::msw_rescale() { + const int em = wxGetApp().em_unit(); + m_item_name->SetMinSize(wxSize(20*em, wxDefaultCoord)); msw_rescale_word_local_combo(m_word_local_combo); + m_word_local_combo_sizer->SetMinSize(wxSize(-1, m_word_local_combo->GetBestHeight(-1))); m_manifold_warning_bmp.msw_rescale(); const wxString& tooltip = m_fix_throught_netfab_bitmap->GetToolTipText(); @@ -936,12 +1293,129 @@ void ObjectManipulation::msw_rescale() m_reset_scale_button->msw_rescale(); m_reset_rotation_button->msw_rescale(); m_drop_to_bed_button->msw_rescale(); + m_lock_bnt->msw_rescale(); for (int id = 0; id < 3; ++id) m_mirror_buttons[id].first->msw_rescale(); + // rescale label-heights + // Text trick to grid sizer layout: + // Height of labels should be equivalent to the edit boxes + const int height = wxTextCtrl(parent(), wxID_ANY, "Br").GetBestHeight(-1); + for (wxBoxSizer* sizer : m_rescalable_sizers) + sizer->SetMinSize(wxSize(-1, height)); + + // rescale edit-boxes + for (ManipulationEditor* editor : m_editors) + editor->msw_rescale(); + get_og()->msw_rescale(); } +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(5*int(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__ + + // A name used to call handle_sidebar_focus_event() + m_full_opt_name = m_opt_key+"_"+axes[axis]; + + // Reset m_enter_pressed flag to _false_, when value is editing + this->Bind(wxEVT_TEXT, [this](wxEvent&) { m_enter_pressed = false; }, this->GetId()); + + this->Bind(wxEVT_TEXT_ENTER, [this, parent](wxEvent&) + { + m_enter_pressed = true; + parent->on_change(m_opt_key, m_axis, get_value()); + }, this->GetId()); + + this->Bind(wxEVT_KILL_FOCUS, [this, parent/*, edit_fn*/](wxFocusEvent& e) + { + if (!m_enter_pressed) { + parent->on_change(m_opt_key, m_axis, get_value()); + + // if the change does not come from the user pressing the ENTER key + // we need to hide the visual hints in 3D scene + wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(m_full_opt_name, false); +// #ifndef __WXGTK__ +// /* Update data for next editor selection. +// * But under GTK it looks like there is no information about selected control at e.GetWindow(), +// * so we'll take it from wxEVT_LEFT_DOWN event +// * */ +// LayerRangeEditor* new_editor = dynamic_cast(e.GetWindow()); +// if (new_editor) +// new_editor->set_focus_data(); +// #endif // not __WXGTK__ + } + + e.Skip(); + }, this->GetId()); + + this->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& e) + { + // 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()); + +// #ifdef __WXGTK__ // Workaround! To take information about selectable range +// this->Bind(wxEVT_LEFT_DOWN, [this](wxEvent& e) +// { +// set_focus_data(); +// e.Skip(); +// }, this->GetId()); +// #endif //__WXGTK__ + + this->Bind(wxEVT_CHAR, ([this](wxKeyEvent& event) + { + // select all text using Ctrl+A + if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL)) + this->SetSelection(-1, -1); //select all + event.Skip(); + })); +} + +void ManipulationEditor::msw_rescale() +{ + const int em = wxGetApp().em_unit(); + SetMinSize(wxSize(5 * em, wxDefaultCoord)); +} + +double ManipulationEditor::get_value() +{ + wxString str = GetValue(); + + double value; + // Replace the first occurence of comma in decimal number. + str.Replace(",", ".", false); + if (str == ".") + value = 0.0; + + if ((str.IsEmpty() || !str.ToCDouble(&value)) && !m_valid_value.IsEmpty()) { + str = m_valid_value; + SetValue(str); + str.ToCDouble(&value); + } + + return value; +} + +void ManipulationEditor::set_value(const wxString& new_value) +{ + if (new_value.IsEmpty()) + return; + m_valid_value = new_value; + SetValue(m_valid_value); +} + } //namespace GUI } //namespace Slic3r diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.hpp b/src/slic3r/GUI/GUI_ObjectManipulation.hpp index e4e190b5b..e25aab678 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.hpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.hpp @@ -16,6 +16,28 @@ namespace GUI { class Selection; +class ObjectManipulation; +class ManipulationEditor : public wxTextCtrl +{ + std::string m_opt_key; + int m_axis; + bool m_enter_pressed { false }; + wxString m_valid_value {wxEmptyString}; + + std::string m_full_opt_name; + +public: + ManipulationEditor(ObjectManipulation* parent, const std::string& opt_key, int axis); + ~ManipulationEditor() {} + + void msw_rescale(); + void set_value(const wxString& new_value); + +private: + double get_value(); +}; + + class ObjectManipulation : public OG_Settings { struct Cache @@ -53,6 +75,9 @@ class ObjectManipulation : public OG_Settings wxStaticText* m_scale_Label = nullptr; wxStaticText* m_rotate_Label = nullptr; + wxStaticText* m_item_name = nullptr; + wxStaticText* m_empty_str = nullptr; + // Non-owning pointers to the reset buttons, so we can hide and show them. ScalableButton* m_reset_scale_button = nullptr; ScalableButton* m_reset_rotation_button = nullptr; @@ -81,7 +106,7 @@ class ObjectManipulation : public OG_Settings Vec3d m_new_rotation; Vec3d m_new_scale; Vec3d m_new_size; - bool m_new_enabled; + bool m_new_enabled {true}; bool m_uniform_scale {true}; // Does the object manipulation panel work in World or Local coordinates? bool m_world_coordinates = true; @@ -96,6 +121,15 @@ class ObjectManipulation : public OG_Settings std::string m_focused_option; #endif // __APPLE__ + wxFlexGridSizer* m_main_grid_sizer; + wxFlexGridSizer* m_labels_grid_sizer; + + // sizers, used for msw_rescale + wxBoxSizer* m_word_local_combo_sizer; + std::vector m_rescalable_sizers; + + std::vector m_editors; + public: ObjectManipulation(wxWindow* parent); ~ObjectManipulation() {} @@ -122,8 +156,10 @@ public: void emulate_kill_focus(); #endif // __APPLE__ + void update_item_name(const wxString &item_name); void update_warning_icon_state(const wxString& tooltip); void msw_rescale(); + void on_change(const std::string& opt_key, int axis, double new_value); private: void reset_settings_value(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7c36f3665..6f39db86d 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -261,7 +261,7 @@ bool MainFrame::can_export_supports() const const PrintObjects& objects = m_plater->sla_print().objects(); for (const SLAPrintObject* object : objects) { - if (object->has_mesh(slaposBasePool) || object->has_mesh(slaposSupportTree)) + if (object->has_mesh(slaposPad) || object->has_mesh(slaposSupportTree)) { can_export = true; break; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fc5ca1921..2fb92502d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -528,12 +528,7 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : const std::vector &init_matrix = (project_config.option("wiping_volumes_matrix"))->values; const std::vector &init_extruders = (project_config.option("wiping_volumes_extruders"))->values; - const DynamicPrintConfig* config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; - std::vector extruder_colours = (config->option("extruder_colour"))->values; - const std::vector& filament_colours = (wxGetApp().plater()->get_plater_config()->option("filament_colour"))->values; - for (size_t i=0; i extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); WipingDialog dlg(parent, cast(init_matrix), cast(init_extruders), extruder_colours); @@ -4474,10 +4469,10 @@ void Plater::export_stl(bool extended, bool selection_only) bool is_left_handed = object->is_left_handed(); TriangleMesh pad_mesh; - bool has_pad_mesh = object->has_mesh(slaposBasePool); + bool has_pad_mesh = object->has_mesh(slaposPad); if (has_pad_mesh) { - pad_mesh = object->get_mesh(slaposBasePool); + pad_mesh = object->get_mesh(slaposPad); pad_mesh.transform(mesh_trafo_inv); } @@ -4653,7 +4648,7 @@ void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error // Otherwise calculate everything, but start with the provided object. if (!this->p->background_processing_enabled()) { task.single_model_instance_only = true; - task.to_object_step = slaposBasePool; + task.to_object_step = slaposPad; } this->p->background_process.set_task(task); // and let the background processing start. @@ -4797,6 +4792,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) filament_colors.push_back(filaments.find_preset(filament_preset, true)->config.opt_string("filament_colour", (unsigned)0)); p->config->option(opt_key)->values = filament_colors; + p->sidebar->obj_list()->update_extruder_colors(); continue; } } @@ -4822,6 +4818,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) else if(opt_key == "extruder_colour") { update_scheduled = true; p->preview->set_number_extruders(p->config->option(opt_key)->values.size()); + p->sidebar->obj_list()->update_extruder_colors(); } else if(opt_key == "max_print_height") { update_scheduled = true; } @@ -4870,8 +4867,10 @@ void Plater::force_filament_colors_update() } } - if (update_scheduled) + if (update_scheduled) { update(); + p->sidebar->obj_list()->update_extruder_colors(); + } if (p->main_frame->is_loaded()) this->p->schedule_background_process(); @@ -4898,6 +4897,22 @@ const DynamicPrintConfig* Plater::get_plater_config() const return p->config; } +std::vector Plater::get_extruder_colors_from_plater_config() const +{ + const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + std::vector extruder_colors; + if (!config->has("extruder_colour")) // in case of a SLA print + return extruder_colors; + + extruder_colors = (config->option("extruder_colour"))->values; + const std::vector& filament_colours = (p->config->option("filament_colour"))->values; + for (size_t i = 0; i < extruder_colors.size(); ++i) + if (extruder_colors[i] == "" && i < filament_colours.size()) + extruder_colors[i] = filament_colours[i]; + + return extruder_colors; +} + wxString Plater::get_project_filename(const wxString& extension) const { return p->get_project_filename(extension); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 744f2eae3..de99b278d 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -219,6 +219,7 @@ public: // On activating the parent window. void on_activate(); const DynamicPrintConfig* get_plater_config() const; + std::vector get_extruder_colors_from_plater_config() const; void update_object_menu(); diff --git a/src/slic3r/GUI/Preset.cpp b/src/slic3r/GUI/Preset.cpp index 9b16d9480..ae995aa44 100644 --- a/src/slic3r/GUI/Preset.cpp +++ b/src/slic3r/GUI/Preset.cpp @@ -508,11 +508,13 @@ const std::vector& Preset::sla_print_options() "pad_enable", "pad_wall_thickness", "pad_wall_height", + "pad_brim_size", "pad_max_merge_distance", // "pad_edge_radius", "pad_wall_slope", "pad_object_gap", "pad_around_object", + "pad_around_object_everywhere", "pad_object_connector_stride", "pad_object_connector_width", "pad_object_connector_penetration", @@ -853,6 +855,21 @@ bool PresetCollection::delete_current_preset() return true; } +bool PresetCollection::delete_preset(const std::string& name) +{ + auto it = this->find_preset_internal(name); + + const Preset& preset = *it; + if (preset.is_default) + return false; + if (!preset.is_external && !preset.is_system) { + // Erase the preset file. + boost::nowide::remove(preset.file.c_str()); + } + m_presets.erase(it); + return true; +} + void PresetCollection::load_bitmap_default(wxWindow *window, const std::string &file_name) { // XXX: See note in PresetBundle::load_compatible_bitmaps() diff --git a/src/slic3r/GUI/Preset.hpp b/src/slic3r/GUI/Preset.hpp index 8be1388f0..6ed818719 100644 --- a/src/slic3r/GUI/Preset.hpp +++ b/src/slic3r/GUI/Preset.hpp @@ -289,6 +289,9 @@ public: // Delete the current preset, activate the first visible preset. // returns true if the preset was deleted successfully. bool delete_current_preset(); + // Delete the current preset, activate the first visible preset. + // returns true if the preset was deleted successfully. + bool delete_preset(const std::string& name); // Load default bitmap to be placed at the wxBitmapComboBox of a MainFrame. void load_bitmap_default(wxWindow *window, const std::string &file_name); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index d727c980e..c2a258e69 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1808,7 +1808,10 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("single_extruder_multi_material"); optgroup->m_on_change = [this, optgroup](t_config_option_key opt_key, boost::any value) { - size_t extruders_count = boost::any_cast(optgroup->get_value("extruders_count")); + // optgroup->get_value() return int for def.type == coInt, + // Thus, there should be boost::any_cast ! + // Otherwise, boost::any_cast causes an "unhandled unknown exception" + size_t extruders_count = size_t(boost::any_cast(optgroup->get_value("extruders_count"))); wxTheApp->CallAfter([this, opt_key, value, extruders_count]() { if (opt_key == "extruders_count" || opt_key == "single_extruder_multi_material") { extruders_count_changed(extruders_count); @@ -3016,6 +3019,18 @@ void Tab::save_preset(std::string name /*= ""*/) show_error(this, _(L("Cannot overwrite an external profile."))); return; } + if (existing && name != preset.name) + { + wxString msg_text = GUI::from_u8((boost::format(_utf8(L("Preset with name \"%1%\" already exist."))) % name).str()); + msg_text += "\n" + _(L("Replace?")); + wxMessageDialog dialog(nullptr, msg_text, _(L("Warning")), wxICON_WARNING | wxYES | wxNO); + + if (dialog.ShowModal() == wxID_NO) + return; + + // Remove the preset from the list. + m_presets->delete_preset(name); + } } // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini @@ -3536,12 +3551,14 @@ void TabSLAPrint::build() optgroup->append_single_option_line("pad_enable"); optgroup->append_single_option_line("pad_wall_thickness"); optgroup->append_single_option_line("pad_wall_height"); + optgroup->append_single_option_line("pad_brim_size"); optgroup->append_single_option_line("pad_max_merge_distance"); // TODO: Disabling this parameter for the beta release // optgroup->append_single_option_line("pad_edge_radius"); optgroup->append_single_option_line("pad_wall_slope"); optgroup->append_single_option_line("pad_around_object"); + optgroup->append_single_option_line("pad_around_object_everywhere"); optgroup->append_single_option_line("pad_object_gap"); optgroup->append_single_option_line("pad_object_connector_stride"); optgroup->append_single_option_line("pad_object_connector_width"); diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 59406b6e9..0109f6a29 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -7,6 +7,7 @@ #include "libslic3r/Model.hpp" #include +#include #include #include #include @@ -20,6 +21,7 @@ #include "libslic3r/GCode/PreviewData.hpp" #include "I18N.hpp" #include "GUI_Utils.hpp" +#include "PresetBundle.hpp" #include "../Utils/MacDarkMode.hpp" using Slic3r::GUI::from_u8; @@ -445,6 +447,52 @@ wxBitmap create_scaled_bitmap(wxWindow *win, const std::string& bmp_name_in, return *bmp; } + +Slic3r::GUI::BitmapCache* m_bitmap_cache = nullptr; +/*static*/ std::vector get_extruder_color_icons() +{ + // Create the bitmap with color bars. + std::vector bmps; + std::vector colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); + + if (bmps.empty()) + return bmps; + + unsigned char rgb[3]; + + /* It's supposed that standard size of an icon is 36px*16px for 100% scaled display. + * So set sizes for solid_colored icons used for filament preset + * and scale them in respect to em_unit value + */ + const double em = Slic3r::GUI::wxGetApp().em_unit(); + const int icon_width = lround(3.2 * em); + const int icon_height = lround(1.6 * em); + + for (const std::string& color : colors) + { + wxBitmap* bitmap = m_bitmap_cache->find(color); + if (bitmap == nullptr) { + // Paint the color icon. + Slic3r::PresetBundle::parse_color(color, rgb); + bitmap = m_bitmap_cache->insert(color, m_bitmap_cache->mksolid(icon_width, icon_height, rgb)); + } + bmps.emplace_back(bitmap); + } + + return bmps; +} + + +static wxBitmap get_extruder_color_icon(size_t extruder_idx) +{ + // Create the bitmap with color bars. + std::vector bmps = get_extruder_color_icons(); + if (bmps.empty()) + return wxNullBitmap; + + return *bmps[extruder_idx >= bmps.size() ? 0 : extruder_idx]; +} + // ***************************************************************************** // ---------------------------------------------------------------------------- // ObjectDataViewModelNode @@ -477,7 +525,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent m_idx = parent->GetChildCount(); m_name = wxString::Format(_(L("Instance %d")), m_idx + 1); - set_action_icon(); + set_action_and_extruder_icons(); } else if (type == itLayerRoot) { @@ -512,7 +560,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent m_name = _(L("Range")) + label_range + "(" + _(L("mm")) + ")"; m_bmp = create_scaled_bitmap(nullptr, LAYER_ICON); // FIXME: pass window ptr - set_action_icon(); + set_action_and_extruder_icons(); init_container(); } @@ -525,11 +573,16 @@ bool ObjectDataViewModelNode::valid() } #endif /* NDEBUG */ -void ObjectDataViewModelNode::set_action_icon() +void ObjectDataViewModelNode::set_action_and_extruder_icons() { m_action_icon_name = m_type & itObject ? "advanced_plus" : m_type & (itVolume | itLayer) ? "cog" : /*m_type & itInstance*/ "set_separate_obj"; m_action_icon = create_scaled_bitmap(nullptr, m_action_icon_name); // FIXME: pass window ptr + + // set extruder bitmap + int extruder_idx = atoi(m_extruder.c_str()); + if (extruder_idx > 0) --extruder_idx; + m_extruder_bmp = get_extruder_color_icon(extruder_idx); } void ObjectDataViewModelNode::set_printable_icon(PrintIndicator printable) @@ -539,7 +592,6 @@ void ObjectDataViewModelNode::set_printable_icon(PrintIndicator printable) create_scaled_bitmap(nullptr, m_printable == piPrintable ? "eye_open.png" : "eye_closed.png"); } -Slic3r::GUI::BitmapCache *m_bitmap_cache = nullptr; void ObjectDataViewModelNode::update_settings_digest_bitmaps() { m_bmp = m_empty_bmp; @@ -605,8 +657,10 @@ bool ObjectDataViewModelNode::SetValue(const wxVariant& variant, unsigned col) m_name = data.GetText(); return true; } case colExtruder: { - const wxString & val = variant.GetString(); - m_extruder = val == "0" ? _(L("default")) : val; + DataViewBitmapText data; + data << variant; + m_extruder_bmp = data.GetBitmap(); + m_extruder = data.GetText() == "0" ? _(L("default")) : data.GetText(); return true; } case colEditing: m_action_icon << variant; @@ -1379,6 +1433,51 @@ t_layer_height_range ObjectDataViewModel::GetLayerRangeByItem(const wxDataViewIt return node->GetLayerRange(); } +bool ObjectDataViewModel::UpdateColumValues(unsigned col) +{ + switch (col) + { + case colPrint: + case colName: + case colEditing: + return true; + case colExtruder: + { + wxDataViewItemArray items; + GetAllChildren(wxDataViewItem(nullptr), items); + + if (items.IsEmpty()) return false; + + for (auto item : items) + UpdateExtruderBitmap(item); + + return true; + } + default: + printf("MyObjectTreeModel::SetValue: wrong column"); + } + return false; +} + + +void ObjectDataViewModel::UpdateExtruderBitmap(wxDataViewItem item) +{ + wxString extruder = GetExtruder(item); + if (extruder.IsEmpty()) + return; + + // set extruder bitmap + int extruder_idx = atoi(extruder.c_str()); + if (extruder_idx > 0) --extruder_idx; + + const DataViewBitmapText extruder_val(extruder, get_extruder_color_icon(extruder_idx)); + + wxVariant value; + value << extruder_val; + + SetValue(value, item, colExtruder); +} + void ObjectDataViewModel::GetItemInfo(const wxDataViewItem& item, ItemType& type, int& obj_idx, int& idx) { wxASSERT(item.IsOk()); @@ -1475,6 +1574,24 @@ wxBitmap& ObjectDataViewModel::GetBitmap(const wxDataViewItem &item) const return node->m_bmp; } +wxString ObjectDataViewModel::GetExtruder(const wxDataViewItem& item) const +{ + ObjectDataViewModelNode *node = (ObjectDataViewModelNode*)item.GetID(); + if (!node) // happens if item.IsOk()==false + return wxEmptyString; + + return node->m_extruder; +} + +int ObjectDataViewModel::GetExtruderNumber(const wxDataViewItem& item) const +{ + ObjectDataViewModelNode *node = (ObjectDataViewModelNode*)item.GetID(); + if (!node) // happens if item.IsOk()==false + return 0; + + return atoi(node->m_extruder.c_str()); +} + void ObjectDataViewModel::GetValue(wxVariant &variant, const wxDataViewItem &item, unsigned int col) const { wxASSERT(item.IsOk()); @@ -1489,7 +1606,7 @@ void ObjectDataViewModel::GetValue(wxVariant &variant, const wxDataViewItem &ite variant << DataViewBitmapText(node->m_name, node->m_bmp); break; case colExtruder: - variant = node->m_extruder; + variant << DataViewBitmapText(node->m_extruder, node->m_extruder_bmp); break; case colEditing: variant << node->m_action_icon; @@ -1515,6 +1632,22 @@ bool ObjectDataViewModel::SetValue(const wxVariant &variant, const int item_idx, return m_objects[item_idx]->SetValue(variant, col); } +void ObjectDataViewModel::SetExtruder(const wxString& extruder, wxDataViewItem item) +{ + DataViewBitmapText extruder_val; + extruder_val.SetText(extruder); + + // set extruder bitmap + int extruder_idx = atoi(extruder.c_str()); + if (extruder_idx > 0) --extruder_idx; + extruder_val.SetBitmap(get_extruder_color_icon(extruder_idx)); + + wxVariant value; + value << extruder_val; + + SetValue(value, item, colExtruder); +} + wxDataViewItem ObjectDataViewModel::ReorganizeChildren( const int current_volume_id, const int new_volume_id, const wxDataViewItem &parent) @@ -1840,7 +1973,7 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo } //----------------------------------------------------------------------------- -// PrusaDataViewBitmapText +// DataViewBitmapText //----------------------------------------------------------------------------- wxIMPLEMENT_DYNAMIC_CLASS(DataViewBitmapText, wxObject) @@ -1966,6 +2099,104 @@ bool BitmapTextRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value return true; } +// ---------------------------------------------------------------------------- +// BitmapChoiceRenderer +// ---------------------------------------------------------------------------- + +bool BitmapChoiceRenderer::SetValue(const wxVariant& value) +{ + m_value << value; + return true; +} + +bool BitmapChoiceRenderer::GetValue(wxVariant& value) const +{ + value << m_value; + return true; +} + +bool BitmapChoiceRenderer::Render(wxRect rect, wxDC* dc, int state) +{ + int xoffset = 0; + + const wxBitmap& icon = m_value.GetBitmap(); + if (icon.IsOk()) + { + dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon.GetHeight()) / 2); + xoffset = icon.GetWidth() + 4; + } + + if (rect.height==0) + rect.height= icon.GetHeight(); + RenderText(m_value.GetText(), xoffset, rect, dc, state); + + return true; +} + +wxSize BitmapChoiceRenderer::GetSize() const +{ + wxSize sz = GetTextExtent(m_value.GetText()); + + if (m_value.GetBitmap().IsOk()) + sz.x += m_value.GetBitmap().GetWidth() + 4; + + return sz; +} + + +wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelRect, const wxVariant& value) +{ + wxDataViewCtrl* const dv_ctrl = GetOwner()->GetOwner(); + ObjectDataViewModel* const model = dynamic_cast(dv_ctrl->GetModel()); + + if (!(model->GetItemType(dv_ctrl->GetSelection()) & (itVolume | itObject))) + return nullptr; + + std::vector icons = get_extruder_color_icons(); + if (icons.empty()) + return nullptr; + + DataViewBitmapText data; + data << value; + + auto c_editor = new wxBitmapComboBox(parent, wxID_ANY, wxEmptyString, + labelRect.GetTopLeft(), wxSize(labelRect.GetWidth(), -1), + 0, nullptr , wxCB_READONLY); + + int i=0; + for (wxBitmap* bmp : icons) { + if (i==0) { + c_editor->Append(_(L("default")), *bmp); + ++i; + } + + c_editor->Append(wxString::Format("%d", i), *bmp); + ++i; + } + c_editor->SetSelection(atoi(data.GetText().c_str())); + + // to avoid event propagation to other sidebar items + c_editor->Bind(wxEVT_COMBOBOX, [](wxCommandEvent& evt) { evt.StopPropagation(); }); + + return c_editor; +} + +bool BitmapChoiceRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) +{ + wxBitmapComboBox* c = (wxBitmapComboBox*)ctrl; + int selection = c->GetSelection(); + if (selection < 0) + return false; + + DataViewBitmapText bmpText; + + bmpText.SetText(c->GetString(selection)); + bmpText.SetBitmap(c->GetItemBitmap(selection)); + + value << bmpText; + return true; +} + // ---------------------------------------------------------------------------- // DoubleSlider // ---------------------------------------------------------------------------- @@ -2987,6 +3218,8 @@ void LockButton::msw_rescale() m_bmp_lock_closed_f.msw_rescale(); m_bmp_lock_open.msw_rescale(); m_bmp_lock_open_f.msw_rescale(); + + update_button_bitmaps(); } void LockButton::update_button_bitmaps() diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 54d1bf7cb..d4d5e7998 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -55,6 +55,8 @@ int em_unit(wxWindow* win); wxBitmap create_scaled_bitmap(wxWindow *win, const std::string& bmp_name, const int px_cnt = 16, const bool is_horizontal = false, const bool grayscale = false); +std::vector get_extruder_color_icons(); + class wxCheckListBoxComboPopup : public wxCheckListBox, public wxComboPopup { static const unsigned int DefaultWidth; @@ -209,6 +211,7 @@ class ObjectDataViewModelNode int m_idx = -1; bool m_container = false; wxString m_extruder = "default"; + wxBitmap m_extruder_bmp; wxBitmap m_action_icon; PrintIndicator m_printable {piUndef}; wxBitmap m_printable_icon; @@ -224,7 +227,7 @@ public: m_type(itObject), m_extruder(extruder) { - set_action_icon(); + set_action_and_extruder_icons(); init_container(); } @@ -240,7 +243,7 @@ public: m_extruder (extruder) { m_bmp = bmp; - set_action_icon(); + set_action_and_extruder_icons(); init_container(); } @@ -356,7 +359,7 @@ public: } // Set action icons for node - void set_action_icon(); + void set_action_and_extruder_icons(); // Set printable icon for node void set_printable_icon(PrintIndicator printable); @@ -438,6 +441,8 @@ public: wxString GetName(const wxDataViewItem &item) const; wxBitmap& GetBitmap(const wxDataViewItem &item) const; + wxString GetExtruder(const wxDataViewItem &item) const; + int GetExtruderNumber(const wxDataViewItem &item) const; // helper methods to change the model @@ -454,6 +459,8 @@ public: const int item_idx, unsigned int col); + void SetExtruder(const wxString& extruder, wxDataViewItem item); + // For parent move child from cur_volume_id place to new_volume_id // Remaining items will moved up/down accordingly wxDataViewItem ReorganizeChildren( const int cur_volume_id, @@ -504,6 +511,9 @@ public: void DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object = false); t_layer_height_range GetLayerRangeByItem(const wxDataViewItem& item) const; + bool UpdateColumValues(unsigned col); + void UpdateExtruderBitmap(wxDataViewItem item); + private: wxDataViewItem AddRoot(const wxDataViewItem& parent_item, const ItemType root_type); wxDataViewItem AddInstanceRoot(const wxDataViewItem& parent_item); @@ -563,6 +573,40 @@ private: }; +// ---------------------------------------------------------------------------- +// BitmapChoiceRenderer +// ---------------------------------------------------------------------------- + +class BitmapChoiceRenderer : public wxDataViewCustomRenderer +{ +public: + BitmapChoiceRenderer(wxDataViewCellMode mode = +#ifdef __WXOSX__ + wxDATAVIEW_CELL_INERT +#else + wxDATAVIEW_CELL_EDITABLE +#endif + ,int align = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL + ) : wxDataViewCustomRenderer(wxT("DataViewBitmapText"), mode, align) {} + + bool SetValue(const wxVariant& value); + bool GetValue(wxVariant& value) const; + + virtual bool Render(wxRect cell, wxDC* dc, int state); + virtual wxSize GetSize() const; + + bool HasEditorCtrl() const override { return true; } + wxWindow* CreateEditorCtrl(wxWindow* parent, + wxRect labelRect, + const wxVariant& value) override; + bool GetValueFromEditorCtrl( wxWindow* ctrl, + wxVariant& value) override; + +private: + DataViewBitmapText m_value; +}; + + // ---------------------------------------------------------------------------- // MyCustomRenderer // ---------------------------------------------------------------------------- diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 11bdc4b3d..cc3fd1de7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,33 @@ # TODO Add individual tests as executables in separate directories +# add_subirectory() + +set(TEST_DATA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data) +file(TO_NATIVE_PATH "${TEST_DATA_DIR}" TEST_DATA_DIR) + +add_library(Catch2 INTERFACE) +list (APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/modules/Catch2) +target_include_directories(Catch2 INTERFACE ${CMAKE_CURRENT_LIST_DIR}) +add_library(Catch2::Catch2 ALIAS Catch2) +include(Catch) + +add_library(test_catch2_common INTERFACE) +target_compile_definitions(test_catch2_common INTERFACE TEST_DATA_DIR=R"\(${TEST_DATA_DIR}\)") +target_link_libraries(test_catch2_common INTERFACE Catch2::Catch2) + +add_library(test_common INTERFACE) +if (APPLE) + target_link_libraries(test_common INTERFACE "-liconv -framework IOKit" "-framework CoreFoundation" -lc++) +endif() + +target_link_libraries(test_common INTERFACE test_catch2_common) + +# DEPRECATED: +#find_package(GTest REQUIRED) +#add_library(test_gtest_common INTERFACE) +#target_compile_definitions(test_gtest_common INTERFACE TEST_DATA_DIR=R"\(${TEST_DATA_DIR}\)") +#target_link_libraries(test_gtest_common INTERFACE GTest::GTest GTest::Main) + +add_subdirectory(libnest2d) +add_subdirectory(timeutils) +add_subdirectory(sla_print) -# add_subirectory() \ No newline at end of file diff --git a/tests/catch2/LICENSE.txt b/tests/catch2/LICENSE.txt new file mode 100644 index 000000000..36b7cd93c --- /dev/null +++ b/tests/catch2/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/tests/catch2/VERSION.txt b/tests/catch2/VERSION.txt new file mode 100644 index 000000000..587a8125d --- /dev/null +++ b/tests/catch2/VERSION.txt @@ -0,0 +1,2 @@ +2.9.2 g2c869e1 + diff --git a/tests/catch2/catch.hpp b/tests/catch2/catch.hpp new file mode 100644 index 000000000..5feb2a4be --- /dev/null +++ b/tests/catch2/catch.hpp @@ -0,0 +1,17075 @@ +/* + * Catch v2.9.2 + * Generated: 2019-08-08 13:35:12.279703 + * ---------------------------------------------------------- + * This file has been merged from multiple headers. Please don't edit it directly + * Copyright (c) 2019 Two Blue Cubes Ltd. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED +#define TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED +// start catch.hpp + + +#define CATCH_VERSION_MAJOR 2 +#define CATCH_VERSION_MINOR 9 +#define CATCH_VERSION_PATCH 2 + +#ifdef __clang__ +# pragma clang system_header +#elif defined __GNUC__ +# pragma GCC system_header +#endif + +// start catch_suppress_warnings.h + +#ifdef __clang__ +# ifdef __ICC // icpc defines the __clang__ macro +# pragma warning(push) +# pragma warning(disable: 161 1682) +# else // __ICC +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wpadded" +# pragma clang diagnostic ignored "-Wswitch-enum" +# pragma clang diagnostic ignored "-Wcovered-switch-default" +# endif +#elif defined __GNUC__ + // Because REQUIREs trigger GCC's -Wparentheses, and because still + // supported version of g++ have only buggy support for _Pragmas, + // Wparentheses have to be suppressed globally. +# pragma GCC diagnostic ignored "-Wparentheses" // See #674 for details + +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-variable" +# pragma GCC diagnostic ignored "-Wpadded" +#endif +// end catch_suppress_warnings.h +#if defined(CATCH_CONFIG_MAIN) || defined(CATCH_CONFIG_RUNNER) +# define CATCH_IMPL +# define CATCH_CONFIG_ALL_PARTS +#endif + +// In the impl file, we want to have access to all parts of the headers +// Can also be used to sanely support PCHs +#if defined(CATCH_CONFIG_ALL_PARTS) +# define CATCH_CONFIG_EXTERNAL_INTERFACES +# if defined(CATCH_CONFIG_DISABLE_MATCHERS) +# undef CATCH_CONFIG_DISABLE_MATCHERS +# endif +# if !defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER) +# define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER +# endif +#endif + +#if !defined(CATCH_CONFIG_IMPL_ONLY) +// start catch_platform.h + +#ifdef __APPLE__ +# include +# if TARGET_OS_OSX == 1 +# define CATCH_PLATFORM_MAC +# elif TARGET_OS_IPHONE == 1 +# define CATCH_PLATFORM_IPHONE +# endif + +#elif defined(linux) || defined(__linux) || defined(__linux__) +# define CATCH_PLATFORM_LINUX + +#elif defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) || defined(__MINGW32__) +# define CATCH_PLATFORM_WINDOWS +#endif + +// end catch_platform.h + +#ifdef CATCH_IMPL +# ifndef CLARA_CONFIG_MAIN +# define CLARA_CONFIG_MAIN_NOT_DEFINED +# define CLARA_CONFIG_MAIN +# endif +#endif + +// start catch_user_interfaces.h + +namespace Catch { + unsigned int rngSeed(); +} + +// end catch_user_interfaces.h +// start catch_tag_alias_autoregistrar.h + +// start catch_common.h + +// start catch_compiler_capabilities.h + +// Detect a number of compiler features - by compiler +// The following features are defined: +// +// CATCH_CONFIG_COUNTER : is the __COUNTER__ macro supported? +// CATCH_CONFIG_WINDOWS_SEH : is Windows SEH supported? +// CATCH_CONFIG_POSIX_SIGNALS : are POSIX signals supported? +// CATCH_CONFIG_DISABLE_EXCEPTIONS : Are exceptions enabled? +// **************** +// Note to maintainers: if new toggles are added please document them +// in configuration.md, too +// **************** + +// In general each macro has a _NO_ form +// (e.g. CATCH_CONFIG_NO_POSIX_SIGNALS) which disables the feature. +// Many features, at point of detection, define an _INTERNAL_ macro, so they +// can be combined, en-mass, with the _NO_ forms later. + +#ifdef __cplusplus + +# if (__cplusplus >= 201402L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L) +# define CATCH_CPP14_OR_GREATER +# endif + +# if (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +# define CATCH_CPP17_OR_GREATER +# endif + +#endif + +#if defined(CATCH_CPP17_OR_GREATER) +# define CATCH_INTERNAL_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS +#endif + +#ifdef __clang__ + +# define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ + _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wexit-time-destructors\"" ) \ + _Pragma( "clang diagnostic ignored \"-Wglobal-constructors\"") +# define CATCH_INTERNAL_UNSUPPRESS_GLOBALS_WARNINGS \ + _Pragma( "clang diagnostic pop" ) + +# define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS \ + _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wparentheses\"" ) +# define CATCH_INTERNAL_UNSUPPRESS_PARENTHESES_WARNINGS \ + _Pragma( "clang diagnostic pop" ) + +# define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS \ + _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wunused-variable\"" ) +# define CATCH_INTERNAL_UNSUPPRESS_UNUSED_WARNINGS \ + _Pragma( "clang diagnostic pop" ) + +# define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS \ + _Pragma( "clang diagnostic push" ) \ + _Pragma( "clang diagnostic ignored \"-Wgnu-zero-variadic-macro-arguments\"" ) +# define CATCH_INTERNAL_UNSUPPRESS_ZERO_VARIADIC_WARNINGS \ + _Pragma( "clang diagnostic pop" ) + +#endif // __clang__ + +//////////////////////////////////////////////////////////////////////////////// +// Assume that non-Windows platforms support posix signals by default +#if !defined(CATCH_PLATFORM_WINDOWS) + #define CATCH_INTERNAL_CONFIG_POSIX_SIGNALS +#endif + +//////////////////////////////////////////////////////////////////////////////// +// We know some environments not to support full POSIX signals +#if defined(__CYGWIN__) || defined(__QNX__) || defined(__EMSCRIPTEN__) || defined(__DJGPP__) + #define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS +#endif + +#ifdef __OS400__ +# define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS +# define CATCH_CONFIG_COLOUR_NONE +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Android somehow still does not support std::to_string +#if defined(__ANDROID__) +# define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Not all Windows environments support SEH properly +#if defined(__MINGW32__) +# define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH +#endif + +//////////////////////////////////////////////////////////////////////////////// +// PS4 +#if defined(__ORBIS__) +# define CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Cygwin +#ifdef __CYGWIN__ + +// Required for some versions of Cygwin to declare gettimeofday +// see: http://stackoverflow.com/questions/36901803/gettimeofday-not-declared-in-this-scope-cygwin +# define _BSD_SOURCE +// some versions of cygwin (most) do not support std::to_string. Use the libstd check. +// https://gcc.gnu.org/onlinedocs/gcc-4.8.2/libstdc++/api/a01053_source.html line 2812-2813 +# if !((__cplusplus >= 201103L) && defined(_GLIBCXX_USE_C99) \ + && !defined(_GLIBCXX_HAVE_BROKEN_VSWPRINTF)) + +# define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING + +# endif +#endif // __CYGWIN__ + +//////////////////////////////////////////////////////////////////////////////// +// Visual C++ +#ifdef _MSC_VER + +# if _MSC_VER >= 1900 // Visual Studio 2015 or newer +# define CATCH_INTERNAL_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS +# endif + +// Universal Windows platform does not support SEH +// Or console colours (or console at all...) +# if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +# define CATCH_CONFIG_COLOUR_NONE +# else +# define CATCH_INTERNAL_CONFIG_WINDOWS_SEH +# endif + +// MSVC traditional preprocessor needs some workaround for __VA_ARGS__ +// _MSVC_TRADITIONAL == 0 means new conformant preprocessor +// _MSVC_TRADITIONAL == 1 means old traditional non-conformant preprocessor +# if !defined(_MSVC_TRADITIONAL) || (defined(_MSVC_TRADITIONAL) && _MSVC_TRADITIONAL) +# define CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR +# endif +#endif // _MSC_VER + +#if defined(_REENTRANT) || defined(_MSC_VER) +// Enable async processing, as -pthread is specified or no additional linking is required +# define CATCH_INTERNAL_CONFIG_USE_ASYNC +#endif // _MSC_VER + +//////////////////////////////////////////////////////////////////////////////// +// Check if we are compiled with -fno-exceptions or equivalent +#if defined(__EXCEPTIONS) || defined(__cpp_exceptions) || defined(_CPPUNWIND) +# define CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED +#endif + +//////////////////////////////////////////////////////////////////////////////// +// DJGPP +#ifdef __DJGPP__ +# define CATCH_INTERNAL_CONFIG_NO_WCHAR +#endif // __DJGPP__ + +//////////////////////////////////////////////////////////////////////////////// +// Embarcadero C++Build +#if defined(__BORLANDC__) + #define CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN +#endif + +//////////////////////////////////////////////////////////////////////////////// + +// Use of __COUNTER__ is suppressed during code analysis in +// CLion/AppCode 2017.2.x and former, because __COUNTER__ is not properly +// handled by it. +// Otherwise all supported compilers support COUNTER macro, +// but user still might want to turn it off +#if ( !defined(__JETBRAINS_IDE__) || __JETBRAINS_IDE__ >= 20170300L ) + #define CATCH_INTERNAL_CONFIG_COUNTER +#endif + +//////////////////////////////////////////////////////////////////////////////// + +// RTX is a special version of Windows that is real time. +// This means that it is detected as Windows, but does not provide +// the same set of capabilities as real Windows does. +#if defined(UNDER_RTSS) || defined(RTX64_BUILD) + #define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH + #define CATCH_INTERNAL_CONFIG_NO_ASYNC + #define CATCH_CONFIG_COLOUR_NONE +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Check if string_view is available and usable +// The check is split apart to work around v140 (VS2015) preprocessor issue... +#if defined(__has_include) +#if __has_include() && defined(CATCH_CPP17_OR_GREATER) +# define CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW +#endif +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Check if optional is available and usable +#if defined(__has_include) +# if __has_include() && defined(CATCH_CPP17_OR_GREATER) +# define CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL +# endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) +#endif // __has_include + +//////////////////////////////////////////////////////////////////////////////// +// Check if byte is available and usable +#if defined(__has_include) +# if __has_include() && defined(CATCH_CPP17_OR_GREATER) +# define CATCH_INTERNAL_CONFIG_CPP17_BYTE +# endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) +#endif // __has_include + +//////////////////////////////////////////////////////////////////////////////// +// Check if variant is available and usable +#if defined(__has_include) +# if __has_include() && defined(CATCH_CPP17_OR_GREATER) +# if defined(__clang__) && (__clang_major__ < 8) + // work around clang bug with libstdc++ https://bugs.llvm.org/show_bug.cgi?id=31852 + // fix should be in clang 8, workaround in libstdc++ 8.2 +# include +# if defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) +# define CATCH_CONFIG_NO_CPP17_VARIANT +# else +# define CATCH_INTERNAL_CONFIG_CPP17_VARIANT +# endif // defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) +# else +# define CATCH_INTERNAL_CONFIG_CPP17_VARIANT +# endif // defined(__clang__) && (__clang_major__ < 8) +# endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) +#endif // __has_include + +#if defined(CATCH_INTERNAL_CONFIG_COUNTER) && !defined(CATCH_CONFIG_NO_COUNTER) && !defined(CATCH_CONFIG_COUNTER) +# define CATCH_CONFIG_COUNTER +#endif +#if defined(CATCH_INTERNAL_CONFIG_WINDOWS_SEH) && !defined(CATCH_CONFIG_NO_WINDOWS_SEH) && !defined(CATCH_CONFIG_WINDOWS_SEH) && !defined(CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH) +# define CATCH_CONFIG_WINDOWS_SEH +#endif +// This is set by default, because we assume that unix compilers are posix-signal-compatible by default. +#if defined(CATCH_INTERNAL_CONFIG_POSIX_SIGNALS) && !defined(CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_POSIX_SIGNALS) +# define CATCH_CONFIG_POSIX_SIGNALS +#endif +// This is set by default, because we assume that compilers with no wchar_t support are just rare exceptions. +#if !defined(CATCH_INTERNAL_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_WCHAR) +# define CATCH_CONFIG_WCHAR +#endif + +#if !defined(CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_CPP11_TO_STRING) +# define CATCH_CONFIG_CPP11_TO_STRING +#endif + +#if defined(CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_NO_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_CPP17_OPTIONAL) +# define CATCH_CONFIG_CPP17_OPTIONAL +#endif + +#if defined(CATCH_INTERNAL_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS) && !defined(CATCH_CONFIG_NO_CPP17_UNCAUGHT_EXCEPTIONS) && !defined(CATCH_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS) +# define CATCH_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS +#endif + +#if defined(CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_NO_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_CPP17_STRING_VIEW) +# define CATCH_CONFIG_CPP17_STRING_VIEW +#endif + +#if defined(CATCH_INTERNAL_CONFIG_CPP17_VARIANT) && !defined(CATCH_CONFIG_NO_CPP17_VARIANT) && !defined(CATCH_CONFIG_CPP17_VARIANT) +# define CATCH_CONFIG_CPP17_VARIANT +#endif + +#if defined(CATCH_INTERNAL_CONFIG_CPP17_BYTE) && !defined(CATCH_CONFIG_NO_CPP17_BYTE) && !defined(CATCH_CONFIG_CPP17_BYTE) +# define CATCH_CONFIG_CPP17_BYTE +#endif + +#if defined(CATCH_CONFIG_EXPERIMENTAL_REDIRECT) +# define CATCH_INTERNAL_CONFIG_NEW_CAPTURE +#endif + +#if defined(CATCH_INTERNAL_CONFIG_NEW_CAPTURE) && !defined(CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NEW_CAPTURE) +# define CATCH_CONFIG_NEW_CAPTURE +#endif + +#if !defined(CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED) && !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) +# define CATCH_CONFIG_DISABLE_EXCEPTIONS +#endif + +#if defined(CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_NO_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_POLYFILL_ISNAN) +# define CATCH_CONFIG_POLYFILL_ISNAN +#endif + +#if defined(CATCH_INTERNAL_CONFIG_USE_ASYNC) && !defined(CATCH_INTERNAL_CONFIG_NO_ASYNC) && !defined(CATCH_CONFIG_NO_USE_ASYNC) && !defined(CATCH_CONFIG_USE_ASYNC) +# define CATCH_CONFIG_USE_ASYNC +#endif + +#if !defined(CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS) +# define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS +# define CATCH_INTERNAL_UNSUPPRESS_PARENTHESES_WARNINGS +#endif +#if !defined(CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS) +# define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS +# define CATCH_INTERNAL_UNSUPPRESS_GLOBALS_WARNINGS +#endif +#if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS) +# define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS +# define CATCH_INTERNAL_UNSUPPRESS_UNUSED_WARNINGS +#endif +#if !defined(CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS) +# define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS +# define CATCH_INTERNAL_UNSUPPRESS_ZERO_VARIADIC_WARNINGS +#endif + +#if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) +#define CATCH_TRY if ((true)) +#define CATCH_CATCH_ALL if ((false)) +#define CATCH_CATCH_ANON(type) if ((false)) +#else +#define CATCH_TRY try +#define CATCH_CATCH_ALL catch (...) +#define CATCH_CATCH_ANON(type) catch (type) +#endif + +#if defined(CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) && !defined(CATCH_CONFIG_NO_TRADITIONAL_MSVC_PREPROCESSOR) && !defined(CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) +#define CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR +#endif + +// end catch_compiler_capabilities.h +#define INTERNAL_CATCH_UNIQUE_NAME_LINE2( name, line ) name##line +#define INTERNAL_CATCH_UNIQUE_NAME_LINE( name, line ) INTERNAL_CATCH_UNIQUE_NAME_LINE2( name, line ) +#ifdef CATCH_CONFIG_COUNTER +# define INTERNAL_CATCH_UNIQUE_NAME( name ) INTERNAL_CATCH_UNIQUE_NAME_LINE( name, __COUNTER__ ) +#else +# define INTERNAL_CATCH_UNIQUE_NAME( name ) INTERNAL_CATCH_UNIQUE_NAME_LINE( name, __LINE__ ) +#endif + +#include +#include +#include + +// We need a dummy global operator<< so we can bring it into Catch namespace later +struct Catch_global_namespace_dummy {}; +std::ostream& operator<<(std::ostream&, Catch_global_namespace_dummy); + +namespace Catch { + + struct CaseSensitive { enum Choice { + Yes, + No + }; }; + + class NonCopyable { + NonCopyable( NonCopyable const& ) = delete; + NonCopyable( NonCopyable && ) = delete; + NonCopyable& operator = ( NonCopyable const& ) = delete; + NonCopyable& operator = ( NonCopyable && ) = delete; + + protected: + NonCopyable(); + virtual ~NonCopyable(); + }; + + struct SourceLineInfo { + + SourceLineInfo() = delete; + SourceLineInfo( char const* _file, std::size_t _line ) noexcept + : file( _file ), + line( _line ) + {} + + SourceLineInfo( SourceLineInfo const& other ) = default; + SourceLineInfo& operator = ( SourceLineInfo const& ) = default; + SourceLineInfo( SourceLineInfo&& ) noexcept = default; + SourceLineInfo& operator = ( SourceLineInfo&& ) noexcept = default; + + bool empty() const noexcept; + bool operator == ( SourceLineInfo const& other ) const noexcept; + bool operator < ( SourceLineInfo const& other ) const noexcept; + + char const* file; + std::size_t line; + }; + + std::ostream& operator << ( std::ostream& os, SourceLineInfo const& info ); + + // Bring in operator<< from global namespace into Catch namespace + // This is necessary because the overload of operator<< above makes + // lookup stop at namespace Catch + using ::operator<<; + + // Use this in variadic streaming macros to allow + // >> +StreamEndStop + // as well as + // >> stuff +StreamEndStop + struct StreamEndStop { + std::string operator+() const; + }; + template + T const& operator + ( T const& value, StreamEndStop ) { + return value; + } +} + +#define CATCH_INTERNAL_LINEINFO \ + ::Catch::SourceLineInfo( __FILE__, static_cast( __LINE__ ) ) + +// end catch_common.h +namespace Catch { + + struct RegistrarForTagAliases { + RegistrarForTagAliases( char const* alias, char const* tag, SourceLineInfo const& lineInfo ); + }; + +} // end namespace Catch + +#define CATCH_REGISTER_TAG_ALIAS( alias, spec ) \ + CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ + namespace{ Catch::RegistrarForTagAliases INTERNAL_CATCH_UNIQUE_NAME( AutoRegisterTagAlias )( alias, spec, CATCH_INTERNAL_LINEINFO ); } \ + CATCH_INTERNAL_UNSUPPRESS_GLOBALS_WARNINGS + +// end catch_tag_alias_autoregistrar.h +// start catch_test_registry.h + +// start catch_interfaces_testcase.h + +#include + +namespace Catch { + + class TestSpec; + + struct ITestInvoker { + virtual void invoke () const = 0; + virtual ~ITestInvoker(); + }; + + class TestCase; + struct IConfig; + + struct ITestCaseRegistry { + virtual ~ITestCaseRegistry(); + virtual std::vector const& getAllTests() const = 0; + virtual std::vector const& getAllTestsSorted( IConfig const& config ) const = 0; + }; + + bool isThrowSafe( TestCase const& testCase, IConfig const& config ); + bool matchTest( TestCase const& testCase, TestSpec const& testSpec, IConfig const& config ); + std::vector filterTests( std::vector const& testCases, TestSpec const& testSpec, IConfig const& config ); + std::vector const& getAllTestCasesSorted( IConfig const& config ); + +} + +// end catch_interfaces_testcase.h +// start catch_stringref.h + +#include +#include +#include + +namespace Catch { + + /// A non-owning string class (similar to the forthcoming std::string_view) + /// Note that, because a StringRef may be a substring of another string, + /// it may not be null terminated. c_str() must return a null terminated + /// string, however, and so the StringRef will internally take ownership + /// (taking a copy), if necessary. In theory this ownership is not externally + /// visible - but it does mean (substring) StringRefs should not be shared between + /// threads. + class StringRef { + public: + using size_type = std::size_t; + + private: + friend struct StringRefTestAccess; + + char const* m_start; + size_type m_size; + + char* m_data = nullptr; + + void takeOwnership(); + + static constexpr char const* const s_empty = ""; + + public: // construction/ assignment + StringRef() noexcept + : StringRef( s_empty, 0 ) + {} + + StringRef( StringRef const& other ) noexcept + : m_start( other.m_start ), + m_size( other.m_size ) + {} + + StringRef( StringRef&& other ) noexcept + : m_start( other.m_start ), + m_size( other.m_size ), + m_data( other.m_data ) + { + other.m_data = nullptr; + } + + StringRef( char const* rawChars ) noexcept; + + StringRef( char const* rawChars, size_type size ) noexcept + : m_start( rawChars ), + m_size( size ) + {} + + StringRef( std::string const& stdString ) noexcept + : m_start( stdString.c_str() ), + m_size( stdString.size() ) + {} + + ~StringRef() noexcept { + delete[] m_data; + } + + auto operator = ( StringRef const &other ) noexcept -> StringRef& { + delete[] m_data; + m_data = nullptr; + m_start = other.m_start; + m_size = other.m_size; + return *this; + } + + operator std::string() const; + + void swap( StringRef& other ) noexcept; + + public: // operators + auto operator == ( StringRef const& other ) const noexcept -> bool; + auto operator != ( StringRef const& other ) const noexcept -> bool; + + auto operator[] ( size_type index ) const noexcept -> char; + + public: // named queries + auto empty() const noexcept -> bool { + return m_size == 0; + } + auto size() const noexcept -> size_type { + return m_size; + } + + auto numberOfCharacters() const noexcept -> size_type; + auto c_str() const -> char const*; + + public: // substrings and searches + auto substr( size_type start, size_type size ) const noexcept -> StringRef; + + // Returns the current start pointer. + // Note that the pointer can change when if the StringRef is a substring + auto currentData() const noexcept -> char const*; + + private: // ownership queries - may not be consistent between calls + auto isOwned() const noexcept -> bool; + auto isSubstring() const noexcept -> bool; + }; + + auto operator + ( StringRef const& lhs, StringRef const& rhs ) -> std::string; + auto operator + ( StringRef const& lhs, char const* rhs ) -> std::string; + auto operator + ( char const* lhs, StringRef const& rhs ) -> std::string; + + auto operator += ( std::string& lhs, StringRef const& sr ) -> std::string&; + auto operator << ( std::ostream& os, StringRef const& sr ) -> std::ostream&; + + inline auto operator "" _sr( char const* rawChars, std::size_t size ) noexcept -> StringRef { + return StringRef( rawChars, size ); + } + +} // namespace Catch + +inline auto operator "" _catch_sr( char const* rawChars, std::size_t size ) noexcept -> Catch::StringRef { + return Catch::StringRef( rawChars, size ); +} + +// end catch_stringref.h +// start catch_type_traits.hpp + + +#include + +namespace Catch{ + +#ifdef CATCH_CPP17_OR_GREATER + template + inline constexpr auto is_unique = std::true_type{}; + + template + inline constexpr auto is_unique = std::bool_constant< + (!std::is_same_v && ...) && is_unique + >{}; +#else + +template +struct is_unique : std::true_type{}; + +template +struct is_unique : std::integral_constant +::value + && is_unique::value + && is_unique::value +>{}; + +#endif +} + +// end catch_type_traits.hpp +// start catch_preprocessor.hpp + + +#define CATCH_RECURSION_LEVEL0(...) __VA_ARGS__ +#define CATCH_RECURSION_LEVEL1(...) CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(__VA_ARGS__))) +#define CATCH_RECURSION_LEVEL2(...) CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(__VA_ARGS__))) +#define CATCH_RECURSION_LEVEL3(...) CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(__VA_ARGS__))) +#define CATCH_RECURSION_LEVEL4(...) CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(__VA_ARGS__))) +#define CATCH_RECURSION_LEVEL5(...) CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(__VA_ARGS__))) + +#ifdef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR +#define INTERNAL_CATCH_EXPAND_VARGS(...) __VA_ARGS__ +// MSVC needs more evaluations +#define CATCH_RECURSION_LEVEL6(...) CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(__VA_ARGS__))) +#define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL6(CATCH_RECURSION_LEVEL6(__VA_ARGS__)) +#else +#define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL5(__VA_ARGS__) +#endif + +#define CATCH_REC_END(...) +#define CATCH_REC_OUT + +#define CATCH_EMPTY() +#define CATCH_DEFER(id) id CATCH_EMPTY() + +#define CATCH_REC_GET_END2() 0, CATCH_REC_END +#define CATCH_REC_GET_END1(...) CATCH_REC_GET_END2 +#define CATCH_REC_GET_END(...) CATCH_REC_GET_END1 +#define CATCH_REC_NEXT0(test, next, ...) next CATCH_REC_OUT +#define CATCH_REC_NEXT1(test, next) CATCH_DEFER ( CATCH_REC_NEXT0 ) ( test, next, 0) +#define CATCH_REC_NEXT(test, next) CATCH_REC_NEXT1(CATCH_REC_GET_END test, next) + +#define CATCH_REC_LIST0(f, x, peek, ...) , f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1) ) ( f, peek, __VA_ARGS__ ) +#define CATCH_REC_LIST1(f, x, peek, ...) , f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST0) ) ( f, peek, __VA_ARGS__ ) +#define CATCH_REC_LIST2(f, x, peek, ...) f(x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1) ) ( f, peek, __VA_ARGS__ ) + +#define CATCH_REC_LIST0_UD(f, userdata, x, peek, ...) , f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD) ) ( f, userdata, peek, __VA_ARGS__ ) +#define CATCH_REC_LIST1_UD(f, userdata, x, peek, ...) , f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST0_UD) ) ( f, userdata, peek, __VA_ARGS__ ) +#define CATCH_REC_LIST2_UD(f, userdata, x, peek, ...) f(userdata, x) CATCH_DEFER ( CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD) ) ( f, userdata, peek, __VA_ARGS__ ) + +// Applies the function macro `f` to each of the remaining parameters, inserts commas between the results, +// and passes userdata as the first parameter to each invocation, +// e.g. CATCH_REC_LIST_UD(f, x, a, b, c) evaluates to f(x, a), f(x, b), f(x, c) +#define CATCH_REC_LIST_UD(f, userdata, ...) CATCH_RECURSE(CATCH_REC_LIST2_UD(f, userdata, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) + +#define CATCH_REC_LIST(f, ...) CATCH_RECURSE(CATCH_REC_LIST2(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) + +#define INTERNAL_CATCH_EXPAND1(param) INTERNAL_CATCH_EXPAND2(param) +#define INTERNAL_CATCH_EXPAND2(...) INTERNAL_CATCH_NO## __VA_ARGS__ +#define INTERNAL_CATCH_DEF(...) INTERNAL_CATCH_DEF __VA_ARGS__ +#define INTERNAL_CATCH_NOINTERNAL_CATCH_DEF +#define INTERNAL_CATCH_STRINGIZE(...) INTERNAL_CATCH_STRINGIZE2(__VA_ARGS__) +#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR +#define INTERNAL_CATCH_STRINGIZE2(...) #__VA_ARGS__ +#define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) +#else +// MSVC is adding extra space and needs another indirection to expand INTERNAL_CATCH_NOINTERNAL_CATCH_DEF +#define INTERNAL_CATCH_STRINGIZE2(...) INTERNAL_CATCH_STRINGIZE3(__VA_ARGS__) +#define INTERNAL_CATCH_STRINGIZE3(...) #__VA_ARGS__ +#define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) (INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) + 1) +#endif + +#define INTERNAL_CATCH_MAKE_NAMESPACE2(...) ns_##__VA_ARGS__ +#define INTERNAL_CATCH_MAKE_NAMESPACE(name) INTERNAL_CATCH_MAKE_NAMESPACE2(name) + +#define INTERNAL_CATCH_REMOVE_PARENS(...) INTERNAL_CATCH_EXPAND1(INTERNAL_CATCH_DEF __VA_ARGS__) + +#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR +#define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) decltype(get_wrapper()) +#define INTERNAL_CATCH_MAKE_TYPE_LIST(...) INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__)) +#else +#define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) INTERNAL_CATCH_EXPAND_VARGS(decltype(get_wrapper())) +#define INTERNAL_CATCH_MAKE_TYPE_LIST(...) INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__))) +#endif + +#define INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(...)\ + CATCH_REC_LIST(INTERNAL_CATCH_MAKE_TYPE_LIST,__VA_ARGS__) + +#define INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_0) INTERNAL_CATCH_REMOVE_PARENS(_0) +#define INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_0, _1) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_1) +#define INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_0, _1, _2) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_1, _2) +#define INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_0, _1, _2, _3) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_1, _2, _3) +#define INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_0, _1, _2, _3, _4) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_1, _2, _3, _4) +#define INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_0, _1, _2, _3, _4, _5) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_1, _2, _3, _4, _5) +#define INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_0, _1, _2, _3, _4, _5, _6) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_1, _2, _4, _5, _6) +#define INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_0, _1, _2, _3, _4, _5, _6, _7) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_1, _2, _3, _4, _5, _6, _7) +#define INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_1, _2, _3, _4, _5, _6, _7, _8) +#define INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9) +#define INTERNAL_CATCH_REMOVE_PARENS_11_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10) + +#define INTERNAL_CATCH_VA_NARGS_IMPL(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N + +#define INTERNAL_CATCH_TYPE_GEN\ + template struct TypeList {};\ + template\ + constexpr auto get_wrapper() noexcept -> TypeList { return {}; }\ + \ + template class L1, typename...E1, template class L2, typename...E2> \ + constexpr auto append(L1, L2) noexcept -> L1 { return {}; }\ + template< template class L1, typename...E1, template class L2, typename...E2, typename...Rest>\ + constexpr auto append(L1, L2, Rest...) noexcept -> decltype(append(L1{}, Rest{}...)) { return {}; }\ + template< template class L1, typename...E1, typename...Rest>\ + constexpr auto append(L1, TypeList, Rest...) noexcept -> L1 { return {}; }\ + \ + template< template class Container, template class List, typename...elems>\ + constexpr auto rewrap(List) noexcept -> TypeList> { return {}; }\ + template< template class Container, template class List, class...Elems, typename...Elements>\ + constexpr auto rewrap(List,Elements...) noexcept -> decltype(append(TypeList>{}, rewrap(Elements{}...))) { return {}; }\ + \ + template