This commit is contained in:
bubnikv 2019-10-04 16:50:18 +02:00
commit d815a518bd
41 changed files with 23664 additions and 194 deletions

View file

@ -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)

View file

@ -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 ``<target>_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 ``<target>_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=$<TARGET_FILE:${TARGET}>"
-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
)

View file

@ -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}")

View file

@ -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 "\\\$<TARGET_OBJECTS:.+>")
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} $<TARGET_FILE:${TestTarget}> ${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()

View file

@ -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()

View file

@ -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<bool(const ConfigOptionDef &)> filter) const
{
// prepare a function for wrapping text

View file

@ -1444,7 +1444,7 @@ public:
std::vector<std::string> 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.

View file

@ -114,7 +114,7 @@ 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;
}

View file

@ -3,116 +3,232 @@
#include <iomanip>
#include <sstream>
#include <chrono>
#include <cassert>
#include <ctime>
#include <cstdio>
//#include <boost/date_time/local_time/local_time.hpp>
//#include <boost/chrono.hpp>
#ifdef _MSC_VER
#include <map>
#endif
#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#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<std::string, std::string> 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<class Ttm>
struct GetPutTimeReturnT {
Ttm *tms;
const char *fmt;
GetPutTimeReturnT(Ttm *_tms, const char *_fmt): tms(_tms), fmt(_fmt) {}
};
using GetTimeReturnT = GetPutTimeReturnT<std::tm>;
using PutTimeReturnT = GetPutTimeReturnT<const std::tm>;
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 &&gt)
{
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

View file

@ -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::gcode);
}
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_ */

View file

@ -5,6 +5,7 @@
#include <utility>
#include <functional>
#include <type_traits>
#include <system_error>
#include "libslic3r.h"

View file

@ -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()

View file

@ -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;

View file

@ -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 += "<td>";
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("<font size=\"5\"><b>") + (snapshot_active ? _(L("Active")) + ": " : "") +
Utils::format_local_date_time(snapshot.time_captured) + ": " + format_reason(snapshot.reason);
text += wxString("<font size=\"5\"><b>") + (snapshot_active ? _(L("Active")) + ": " : "") +
datetime + ": " + format_reason(snapshot.reason);
if (! snapshot.comment.empty())
text += " (" + wxString::FromUTF8(snapshot.comment.data()) + ")";
text += "</b></font><br>";

View file

@ -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<unsigned char>(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<unsigned char>(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;

View file

@ -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<ConfigOptionInt>("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<ConfigOptionInt>("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<wxDataViewChoiceRenderer*>(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<wxBitmap*> 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<ItemForDelete>& it
(*m_objects)[item->obj_idx]->config.has("extruder"))
{
const wxString extruder = wxString::Format("%d", (*m_objects)[item->obj_idx]->config.option<ConfigOptionInt>("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));

View file

@ -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<ModelObject*> *m_objects{ nullptr };
wxBitmapComboBox *m_extruder_editor { nullptr };
std::vector<wxBitmap*> 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__ */

View file

@ -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<GLVolume*>(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<GLVolume*>(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<GLVolume*>(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<GLVolume*>(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<LayerRangeEditor*>(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

View file

@ -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<wxBoxSizer*> m_rescalable_sizers;
std::vector<ManipulationEditor*> 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();

View file

@ -521,12 +521,7 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) :
const std::vector<double> &init_matrix = (project_config.option<ConfigOptionFloats>("wiping_volumes_matrix"))->values;
const std::vector<double> &init_extruders = (project_config.option<ConfigOptionFloats>("wiping_volumes_extruders"))->values;
const DynamicPrintConfig* config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
std::vector<std::string> extruder_colours = (config->option<ConfigOptionStrings>("extruder_colour"))->values;
const std::vector<std::string>& filament_colours = (wxGetApp().plater()->get_plater_config()->option<ConfigOptionStrings>("filament_colour"))->values;
for (size_t i=0; i<extruder_colours.size(); ++i)
if (extruder_colours[i] == "" && i < filament_colours.size())
extruder_colours[i] = filament_colours[i];
const std::vector<std::string> extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config();
WipingDialog dlg(parent, cast<float>(init_matrix), cast<float>(init_extruders), extruder_colours);
@ -4790,6 +4785,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<ConfigOptionStrings>(opt_key)->values = filament_colors;
p->sidebar->obj_list()->update_extruder_colors();
continue;
}
}
@ -4815,6 +4811,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<ConfigOptionStrings>(opt_key)->values.size());
p->sidebar->obj_list()->update_extruder_colors();
} else if(opt_key == "max_print_height") {
update_scheduled = true;
}
@ -4863,8 +4860,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();
@ -4891,6 +4890,18 @@ const DynamicPrintConfig* Plater::get_plater_config() const
return p->config;
}
std::vector<std::string> Plater::get_extruder_colors_from_plater_config() const
{
const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
std::vector<std::string> extruder_colors = (config->option<ConfigOptionStrings>("extruder_colour"))->values;
const std::vector<std::string>& filament_colours = (p->config->option<ConfigOptionStrings>("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);

View file

@ -215,6 +215,7 @@ public:
// On activating the parent window.
void on_activate();
const DynamicPrintConfig* get_plater_config() const;
std::vector<std::string> get_extruder_colors_from_plater_config() const;
void update_object_menu();

View file

@ -819,6 +819,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()

View file

@ -276,6 +276,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);

View file

@ -3019,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

View file

@ -7,6 +7,7 @@
#include "libslic3r/Model.hpp"
#include <wx/sizer.h>
#include <wx/bmpcbox.h>
#include <wx/statline.h>
#include <wx/dcclient.h>
#include <wx/numformatter.h>
@ -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,47 @@ wxBitmap create_scaled_bitmap(wxWindow *win, const std::string& bmp_name_in,
return *bmp;
}
Slic3r::GUI::BitmapCache* m_bitmap_cache = nullptr;
/*static*/ std::vector<wxBitmap*> get_extruder_color_icons()
{
// Create the bitmap with color bars.
std::vector<wxBitmap*> bmps;
std::vector<std::string> colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config();
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<wxBitmap*> bmps = get_extruder_color_icons();
return *bmps[extruder_idx >= bmps.size() ? 0 : extruder_idx];
}
// *****************************************************************************
// ----------------------------------------------------------------------------
// ObjectDataViewModelNode
@ -477,7 +520,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 +555,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 +568,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 +587,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 +652,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 +1428,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 +1569,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 +1601,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 +1627,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 +1968,7 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo
}
//-----------------------------------------------------------------------------
// PrusaDataViewBitmapText
// DataViewBitmapText
//-----------------------------------------------------------------------------
wxIMPLEMENT_DYNAMIC_CLASS(DataViewBitmapText, wxObject)
@ -1966,6 +2094,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<ObjectDataViewModel*>(dv_ctrl->GetModel());
if (!(model->GetItemType(dv_ctrl->GetSelection()) & (itVolume | itObject)))
return nullptr;
std::vector<wxBitmap*> 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 +3213,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()

View file

@ -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<wxBitmap*> 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
// ----------------------------------------------------------------------------

View file

@ -1,3 +1,32 @@
# TODO Add individual tests as executables in separate directories
# add_subirectory(<testcase>)
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_subirectory(<testcase>)

23
tests/catch2/LICENSE.txt Normal file
View file

@ -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.

2
tests/catch2/VERSION.txt Normal file
View file

@ -0,0 +1,2 @@
2.9.2 g2c869e1

17075
tests/catch2/catch.hpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
/*
* Created by Justin R. Wilson on 2/19/2017.
* Copyright 2017 Justin R. Wilson. 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_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED
#define TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED
// Don't #include any Catch headers here - we can assume they are already
// included before this header.
// This is not good practice in general but is necessary in this case so this
// file can be distributed as a single header that works with the main
// Catch single header.
namespace Catch {
struct AutomakeReporter : StreamingReporterBase<AutomakeReporter> {
AutomakeReporter( ReporterConfig const& _config )
: StreamingReporterBase( _config )
{}
~AutomakeReporter() override;
static std::string getDescription() {
return "Reports test results in the format of Automake .trs files";
}
void assertionStarting( AssertionInfo const& ) override {}
bool assertionEnded( AssertionStats const& /*_assertionStats*/ ) override { return true; }
void testCaseEnded( TestCaseStats const& _testCaseStats ) override {
// Possible values to emit are PASS, XFAIL, SKIP, FAIL, XPASS and ERROR.
stream << ":test-result: ";
if (_testCaseStats.totals.assertions.allPassed()) {
stream << "PASS";
} else if (_testCaseStats.totals.assertions.allOk()) {
stream << "XFAIL";
} else {
stream << "FAIL";
}
stream << ' ' << _testCaseStats.testInfo.name << '\n';
StreamingReporterBase::testCaseEnded( _testCaseStats );
}
void skipTest( TestCaseInfo const& testInfo ) override {
stream << ":test-result: SKIP " << testInfo.name << '\n';
}
};
#ifdef CATCH_IMPL
AutomakeReporter::~AutomakeReporter() {}
#endif
CATCH_REGISTER_REPORTER( "automake", AutomakeReporter)
} // end namespace Catch
#endif // TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED

View file

@ -0,0 +1,253 @@
/*
* Created by Colton Wolkins on 2015-08-15.
* Copyright 2015 Martin Moene. 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_CATCH_REPORTER_TAP_HPP_INCLUDED
#define TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED
// Don't #include any Catch headers here - we can assume they are already
// included before this header.
// This is not good practice in general but is necessary in this case so this
// file can be distributed as a single header that works with the main
// Catch single header.
#include <algorithm>
namespace Catch {
struct TAPReporter : StreamingReporterBase<TAPReporter> {
using StreamingReporterBase::StreamingReporterBase;
~TAPReporter() override;
static std::string getDescription() {
return "Reports test results in TAP format, suitable for test harnesses";
}
ReporterPreferences getPreferences() const override {
return m_reporterPrefs;
}
void noMatchingTestCases( std::string const& spec ) override {
stream << "# No test cases matched '" << spec << "'" << std::endl;
}
void assertionStarting( AssertionInfo const& ) override {}
bool assertionEnded( AssertionStats const& _assertionStats ) override {
++counter;
stream << "# " << currentTestCaseInfo->name << std::endl;
AssertionPrinter printer( stream, _assertionStats, counter );
printer.print();
stream << std::endl;
return true;
}
void testRunEnded( TestRunStats const& _testRunStats ) override {
printTotals( _testRunStats.totals );
stream << "\n" << std::endl;
StreamingReporterBase::testRunEnded( _testRunStats );
}
private:
std::size_t counter = 0;
class AssertionPrinter {
public:
AssertionPrinter& operator= ( AssertionPrinter const& ) = delete;
AssertionPrinter( AssertionPrinter const& ) = delete;
AssertionPrinter( std::ostream& _stream, AssertionStats const& _stats, std::size_t _counter )
: stream( _stream )
, result( _stats.assertionResult )
, messages( _stats.infoMessages )
, itMessage( _stats.infoMessages.begin() )
, printInfoMessages( true )
, counter(_counter)
{}
void print() {
itMessage = messages.begin();
switch( result.getResultType() ) {
case ResultWas::Ok:
printResultType( passedString() );
printOriginalExpression();
printReconstructedExpression();
if ( ! result.hasExpression() )
printRemainingMessages( Colour::None );
else
printRemainingMessages();
break;
case ResultWas::ExpressionFailed:
if (result.isOk()) {
printResultType(passedString());
} else {
printResultType(failedString());
}
printOriginalExpression();
printReconstructedExpression();
if (result.isOk()) {
printIssue(" # TODO");
}
printRemainingMessages();
break;
case ResultWas::ThrewException:
printResultType( failedString() );
printIssue( "unexpected exception with message:" );
printMessage();
printExpressionWas();
printRemainingMessages();
break;
case ResultWas::FatalErrorCondition:
printResultType( failedString() );
printIssue( "fatal error condition with message:" );
printMessage();
printExpressionWas();
printRemainingMessages();
break;
case ResultWas::DidntThrowException:
printResultType( failedString() );
printIssue( "expected exception, got none" );
printExpressionWas();
printRemainingMessages();
break;
case ResultWas::Info:
printResultType( "info" );
printMessage();
printRemainingMessages();
break;
case ResultWas::Warning:
printResultType( "warning" );
printMessage();
printRemainingMessages();
break;
case ResultWas::ExplicitFailure:
printResultType( failedString() );
printIssue( "explicitly" );
printRemainingMessages( Colour::None );
break;
// These cases are here to prevent compiler warnings
case ResultWas::Unknown:
case ResultWas::FailureBit:
case ResultWas::Exception:
printResultType( "** internal error **" );
break;
}
}
private:
static Colour::Code dimColour() { return Colour::FileName; }
static const char* failedString() { return "not ok"; }
static const char* passedString() { return "ok"; }
void printSourceInfo() const {
Colour colourGuard( dimColour() );
stream << result.getSourceInfo() << ":";
}
void printResultType( std::string const& passOrFail ) const {
if( !passOrFail.empty() ) {
stream << passOrFail << ' ' << counter << " -";
}
}
void printIssue( std::string const& issue ) const {
stream << " " << issue;
}
void printExpressionWas() {
if( result.hasExpression() ) {
stream << ";";
{
Colour colour( dimColour() );
stream << " expression was:";
}
printOriginalExpression();
}
}
void printOriginalExpression() const {
if( result.hasExpression() ) {
stream << " " << result.getExpression();
}
}
void printReconstructedExpression() const {
if( result.hasExpandedExpression() ) {
{
Colour colour( dimColour() );
stream << " for: ";
}
std::string expr = result.getExpandedExpression();
std::replace( expr.begin(), expr.end(), '\n', ' ');
stream << expr;
}
}
void printMessage() {
if ( itMessage != messages.end() ) {
stream << " '" << itMessage->message << "'";
++itMessage;
}
}
void printRemainingMessages( Colour::Code colour = dimColour() ) {
if (itMessage == messages.end()) {
return;
}
// using messages.end() directly (or auto) yields compilation error:
std::vector<MessageInfo>::const_iterator itEnd = messages.end();
const std::size_t N = static_cast<std::size_t>( std::distance( itMessage, itEnd ) );
{
Colour colourGuard( colour );
stream << " with " << pluralise( N, "message" ) << ":";
}
for(; itMessage != itEnd; ) {
// If this assertion is a warning ignore any INFO messages
if( printInfoMessages || itMessage->type != ResultWas::Info ) {
stream << " '" << itMessage->message << "'";
if ( ++itMessage != itEnd ) {
Colour colourGuard( dimColour() );
stream << " and";
}
}
}
}
private:
std::ostream& stream;
AssertionResult const& result;
std::vector<MessageInfo> messages;
std::vector<MessageInfo>::const_iterator itMessage;
bool printInfoMessages;
std::size_t counter;
};
void printTotals( const Totals& totals ) const {
if( totals.testCases.total() == 0 ) {
stream << "1..0 # Skipped: No tests ran.";
} else {
stream << "1.." << counter;
}
}
};
#ifdef CATCH_IMPL
TAPReporter::~TAPReporter() {}
#endif
CATCH_REGISTER_REPORTER( "tap", TAPReporter )
} // end namespace Catch
#endif // TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED

View file

@ -0,0 +1,220 @@
/*
* Created by Phil Nash on 19th December 2014
* Copyright 2014 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_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED
#define TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED
// Don't #include any Catch headers here - we can assume they are already
// included before this header.
// This is not good practice in general but is necessary in this case so this
// file can be distributed as a single header that works with the main
// Catch single header.
#include <cstring>
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wpadded"
#endif
namespace Catch {
struct TeamCityReporter : StreamingReporterBase<TeamCityReporter> {
TeamCityReporter( ReporterConfig const& _config )
: StreamingReporterBase( _config )
{
m_reporterPrefs.shouldRedirectStdOut = true;
}
static std::string escape( std::string const& str ) {
std::string escaped = str;
replaceInPlace( escaped, "|", "||" );
replaceInPlace( escaped, "'", "|'" );
replaceInPlace( escaped, "\n", "|n" );
replaceInPlace( escaped, "\r", "|r" );
replaceInPlace( escaped, "[", "|[" );
replaceInPlace( escaped, "]", "|]" );
return escaped;
}
~TeamCityReporter() override;
static std::string getDescription() {
return "Reports test results as TeamCity service messages";
}
void skipTest( TestCaseInfo const& /* testInfo */ ) override {
}
void noMatchingTestCases( std::string const& /* spec */ ) override {}
void testGroupStarting( GroupInfo const& groupInfo ) override {
StreamingReporterBase::testGroupStarting( groupInfo );
stream << "##teamcity[testSuiteStarted name='"
<< escape( groupInfo.name ) << "']\n";
}
void testGroupEnded( TestGroupStats const& testGroupStats ) override {
StreamingReporterBase::testGroupEnded( testGroupStats );
stream << "##teamcity[testSuiteFinished name='"
<< escape( testGroupStats.groupInfo.name ) << "']\n";
}
void assertionStarting( AssertionInfo const& ) override {}
bool assertionEnded( AssertionStats const& assertionStats ) override {
AssertionResult const& result = assertionStats.assertionResult;
if( !result.isOk() ) {
ReusableStringStream msg;
if( !m_headerPrintedForThisSection )
printSectionHeader( msg.get() );
m_headerPrintedForThisSection = true;
msg << result.getSourceInfo() << "\n";
switch( result.getResultType() ) {
case ResultWas::ExpressionFailed:
msg << "expression failed";
break;
case ResultWas::ThrewException:
msg << "unexpected exception";
break;
case ResultWas::FatalErrorCondition:
msg << "fatal error condition";
break;
case ResultWas::DidntThrowException:
msg << "no exception was thrown where one was expected";
break;
case ResultWas::ExplicitFailure:
msg << "explicit failure";
break;
// We shouldn't get here because of the isOk() test
case ResultWas::Ok:
case ResultWas::Info:
case ResultWas::Warning:
CATCH_ERROR( "Internal error in TeamCity reporter" );
// These cases are here to prevent compiler warnings
case ResultWas::Unknown:
case ResultWas::FailureBit:
case ResultWas::Exception:
CATCH_ERROR( "Not implemented" );
}
if( assertionStats.infoMessages.size() == 1 )
msg << " with message:";
if( assertionStats.infoMessages.size() > 1 )
msg << " with messages:";
for( auto const& messageInfo : assertionStats.infoMessages )
msg << "\n \"" << messageInfo.message << "\"";
if( result.hasExpression() ) {
msg <<
"\n " << result.getExpressionInMacro() << "\n"
"with expansion:\n" <<
" " << result.getExpandedExpression() << "\n";
}
if( currentTestCaseInfo->okToFail() ) {
msg << "- failure ignore as test marked as 'ok to fail'\n";
stream << "##teamcity[testIgnored"
<< " name='" << escape( currentTestCaseInfo->name )<< "'"
<< " message='" << escape( msg.str() ) << "'"
<< "]\n";
}
else {
stream << "##teamcity[testFailed"
<< " name='" << escape( currentTestCaseInfo->name )<< "'"
<< " message='" << escape( msg.str() ) << "'"
<< "]\n";
}
}
stream.flush();
return true;
}
void sectionStarting( SectionInfo const& sectionInfo ) override {
m_headerPrintedForThisSection = false;
StreamingReporterBase::sectionStarting( sectionInfo );
}
void testCaseStarting( TestCaseInfo const& testInfo ) override {
m_testTimer.start();
StreamingReporterBase::testCaseStarting( testInfo );
stream << "##teamcity[testStarted name='"
<< escape( testInfo.name ) << "']\n";
stream.flush();
}
void testCaseEnded( TestCaseStats const& testCaseStats ) override {
StreamingReporterBase::testCaseEnded( testCaseStats );
if( !testCaseStats.stdOut.empty() )
stream << "##teamcity[testStdOut name='"
<< escape( testCaseStats.testInfo.name )
<< "' out='" << escape( testCaseStats.stdOut ) << "']\n";
if( !testCaseStats.stdErr.empty() )
stream << "##teamcity[testStdErr name='"
<< escape( testCaseStats.testInfo.name )
<< "' out='" << escape( testCaseStats.stdErr ) << "']\n";
stream << "##teamcity[testFinished name='"
<< escape( testCaseStats.testInfo.name ) << "' duration='"
<< m_testTimer.getElapsedMilliseconds() << "']\n";
stream.flush();
}
private:
void printSectionHeader( std::ostream& os ) {
assert( !m_sectionStack.empty() );
if( m_sectionStack.size() > 1 ) {
os << getLineOfChars<'-'>() << "\n";
std::vector<SectionInfo>::const_iterator
it = m_sectionStack.begin()+1, // Skip first section (test case)
itEnd = m_sectionStack.end();
for( ; it != itEnd; ++it )
printHeaderString( os, it->name );
os << getLineOfChars<'-'>() << "\n";
}
SourceLineInfo lineInfo = m_sectionStack.front().lineInfo;
if( !lineInfo.empty() )
os << lineInfo << "\n";
os << getLineOfChars<'.'>() << "\n\n";
}
// if string has a : in first line will set indent to follow it on
// subsequent lines
static void printHeaderString( std::ostream& os, std::string const& _string, std::size_t indent = 0 ) {
std::size_t i = _string.find( ": " );
if( i != std::string::npos )
i+=2;
else
i = 0;
os << Column( _string )
.indent( indent+i)
.initialIndent( indent ) << "\n";
}
private:
bool m_headerPrintedForThisSection = false;
Timer m_testTimer;
};
#ifdef CATCH_IMPL
TeamCityReporter::~TeamCityReporter() {}
#endif
CATCH_REGISTER_REPORTER( "teamcity", TeamCityReporter )
} // end namespace Catch
#ifdef __clang__
# pragma clang diagnostic pop
#endif
#endif // TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED

View file

@ -0,0 +1,5 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r ${Boost_LIBRARIES} ${TBB_LIBRARIES} ${Boost_LIBRARIES})
catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")

View file

@ -0,0 +1,6 @@
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
TEST_CASE("Is example succesful", "[example]") {
REQUIRE(true);
}

View file

@ -0,0 +1,5 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp printer_parts.cpp printer_parts.hpp)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r ${Boost_LIBRARIES} ${TBB_LIBRARIES} ${Boost_LIBRARIES})
catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
#ifndef PRINTER_PARTS_H
#define PRINTER_PARTS_H
#include <vector>
#include <libnest2d/backends/clipper/clipper_polygon.hpp>
using TestData = std::vector<ClipperLib::Path>;
using TestDataEx = std::vector<ClipperLib::Polygon>;
extern const TestData PRINTER_PART_POLYGONS;
extern const TestData STEGOSAUR_POLYGONS;
extern const TestDataEx PRINTER_PART_POLYGONS_EX;
#endif // PRINTER_PARTS_H

View file

@ -0,0 +1,5 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r ${Boost_LIBRARIES} ${TBB_LIBRARIES} ${Boost_LIBRARIES})
catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")

View file

@ -0,0 +1,38 @@
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
#include "libslic3r/Time.hpp"
#include <sstream>
#include <iomanip>
#include <locale>
namespace {
void test_time_fmt(Slic3r::Utils::TimeFormat fmt) {
using namespace Slic3r::Utils;
time_t t = get_current_time_utc();
std::string tstr = time2str(t, TimeZone::local, fmt);
time_t parsedtime = str2time(tstr, TimeZone::local, fmt);
REQUIRE(t == parsedtime);
tstr = time2str(t, TimeZone::utc, fmt);
parsedtime = str2time(tstr, TimeZone::utc, fmt);
REQUIRE(t == parsedtime);
parsedtime = str2time("not valid string", TimeZone::local, fmt);
REQUIRE(parsedtime == time_t(-1));
parsedtime = str2time("not valid string", TimeZone::utc, fmt);
REQUIRE(parsedtime == time_t(-1));
}
}
TEST_CASE("ISO8601Z", "[Timeutils]") {
test_time_fmt(Slic3r::Utils::TimeFormat::iso8601Z);
}
TEST_CASE("Slic3r_UTC_Time_Format", "[Timeutils]") {
test_time_fmt(Slic3r::Utils::TimeFormat::gcode);
}