QIDISlicer1.0.0

This commit is contained in:
sunsets
2023-06-10 10:14:12 +08:00
parent f2e20e1a90
commit b4cd486f2d
3475 changed files with 1973675 additions and 0 deletions

36
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,36 @@
# 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)
if (APPLE)
# OSX builds targeting OSX 10.9 do not support new std::uncought_exception()
# see https://github.com/catchorg/Catch2/issues/1218
target_compile_definitions(Catch2 INTERFACE -DCATCH_CONFIG_NO_CPP17_UNCAUGHT_EXCEPTIONS)
endif()
include(Catch)
set(CATCH_EXTRA_ARGS "" CACHE STRING "Extra arguments for catch2 test suites.")
add_library(test_common INTERFACE)
target_compile_definitions(test_common INTERFACE TEST_DATA_DIR=R"\(${TEST_DATA_DIR}\)" CATCH_CONFIG_FAST_COMPILE)
target_link_libraries(test_common INTERFACE Catch2::Catch2)
if (APPLE)
target_link_libraries(test_common INTERFACE "-liconv -framework IOKit" "-framework CoreFoundation" -lc++)
endif()
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
add_subdirectory(libnest2d)
add_subdirectory(libslic3r)
add_subdirectory(slic3rutils)
add_subdirectory(fff_print)
add_subdirectory(sla_print)
add_subdirectory(cpp17 EXCLUDE_FROM_ALL) # does not have to be built all the time
# add_subdirectory(example)

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

17937
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

54
tests/catch_main.hpp Normal file
View File

@@ -0,0 +1,54 @@
#ifndef CATCH_MAIN
#define CATCH_MAIN
#define CATCH_CONFIG_EXTERNAL_INTERFACES
#define CATCH_CONFIG_MAIN
// #define CATCH_CONFIG_DEFAULT_REPORTER "verboseconsole"
#include <catch2/catch.hpp>
namespace Catch {
struct VerboseConsoleReporter : public ConsoleReporter {
double duration = 0.;
using ConsoleReporter::ConsoleReporter;
void testCaseStarting(TestCaseInfo const& _testInfo) override
{
Colour::use(Colour::Cyan);
stream << "Testing ";
Colour::use(Colour::None);
stream << _testInfo.name << std::endl;
ConsoleReporter::testCaseStarting(_testInfo);
}
void sectionStarting(const SectionInfo &_sectionInfo) override
{
if (_sectionInfo.name != currentTestCaseInfo->name)
stream << _sectionInfo.name << std::endl;
ConsoleReporter::sectionStarting(_sectionInfo);
}
void sectionEnded(const SectionStats &_sectionStats) override {
duration += _sectionStats.durationInSeconds;
ConsoleReporter::sectionEnded(_sectionStats);
}
void testCaseEnded(TestCaseStats const& stats) override
{
if (stats.totals.assertions.allOk()) {
Colour::use(Colour::BrightGreen);
stream << "Passed";
Colour::use(Colour::None);
stream << " in " << duration << " [seconds]\n" << std::endl;
}
duration = 0.;
ConsoleReporter::testCaseEnded(stats);
}
};
CATCH_REGISTER_REPORTER( "verboseconsole", VerboseConsoleReporter )
} // namespace Catch
#endif // CATCH_MAIN

View File

@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.1)
project(Cpp17Test)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(cpp17test main.cpp)

80
tests/cpp17/main.cpp Normal file
View File

@@ -0,0 +1,80 @@
#include <cstdlib>
#include <iostream>
#include <tuple>
#include <string>
// Test new headers in cpp17
#include <variant>
#include <optional>
#include <any>
#include <string_view>
// Test for nested namespace definition
namespace QIDISlicer::Cpp17 {
template<class T> class Foo
{
T m_arg;
public:
explicit Foo(T &&arg): m_arg{arg} {}
};
} // namespace QIDISlicer::Cpp17
template<class T> std::string get_type(const T &v);
template<> std::string get_type(const int &) { return "int"; }
template<> std::string get_type(const double &) { return "double"; }
template<> std::string get_type(const float &) { return "double"; }
int main()
{
// /////////////////////////////////////////////////////////////////////////
// Template argument deduction for class templates
// /////////////////////////////////////////////////////////////////////////
auto foo = QIDISlicer::Cpp17::Foo{1.f};
// /////////////////////////////////////////////////////////////////////////
// Structured bindings:
// /////////////////////////////////////////////////////////////////////////
auto my_tuple = std::make_tuple(0.2, 10);
auto [a, b] = my_tuple;
std::cout << "a is " << get_type(a) << std::endl;
std::cout << "b is " << get_type(b) << std::endl;
// /////////////////////////////////////////////////////////////////////////
// Test for std::apply()
// /////////////////////////////////////////////////////////////////////////
auto fun = [] (auto a, auto b) {
std::cout << "a (" << get_type(a) << ") = " << a << std::endl;
std::cout << "b (" << get_type(b) << ") = " << b << std::endl;
};
std::apply(fun, my_tuple);
// /////////////////////////////////////////////////////////////////////////
// constexpr lambda and if
// /////////////////////////////////////////////////////////////////////////
auto isIntegral = [](auto v) constexpr -> bool {
if constexpr (std::is_integral_v<decltype(v)>) {
return true;
} else {
return false;
}
};
static_assert (isIntegral(10), "" );
// would fail to compile: static_assert (isIntegral(10.0), "" );
std::cout << "Integer is integral: " << isIntegral(0) << std::endl;
std::cout << "Floating point is not integral: " << isIntegral(0.0) << std::endl;
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg">
<path fill="#808080" d="M 437 868 425 886 417 866 405 884 400 874 388 880 402 896 381 899 395 915 374 918 388 934 367 936 381 952 360 955 374 971 360 982 371 973 353 961 373 953 355 941 375 933 357 921 377 913 359 901 379 894 361 882 372 869 365 857 350 873 346 852 331 867 326 856 313 859 323 878 301 875 311 894 290 892 300 911 279 909 289 927 267 925 278 944 264 947 276 941 262 925 283 922 269 906 290 903 275 887 297 884 282 869 303 866 289 850 300 847 296 834 278 846 278 824 260 836 254 822 240 822 246 843 226 836 231 856 211 849 217 870 197 863 202 884 182 877 188 898 180 892 193 889 183 870 204 872 193 853 215 855 204 836 225 838 214 819 232 809 230 795 210 804 214 783 195 792 188 774 175 771 175 792 157 780 158 802 140 790 140 812 122 800 123 822 110 818 124 818 118 797 138 804 132 783 153 790 147 769 167 776 161 756 172 758 173 744 152 749 152 734 154 720 133 723 134 708 122 702 118 723 103 707 98 729 83 713 79 734 60 730 73 733 72 712 91 723 90 701 108 713 107 691 124 692 129 679 107 678 120 661 99 659 112 643 91 641 96 630 86 621 76 640 66 621 56 641 46 622 36 641 30 632 42 638 47 617 61 633 66 612 81 627 85 606 92 614 100 603 79 596 96 583 76 576 93 563 72 556 81 550 81 542 73 531 60 548 53 527 40 544 25 532 36 541 44 521 56 539 64 520 81 528 92 520 75 507 95 500 77 487 98 480 80 467 92 454 87 441 70 455 68 433 52 447 42 432 50 443 63 426 70 446 83 428 96 440 108 434 94 418 115 415 101 399 122 396 108 380 122 369 120 355 100 364 104 343 85 352 82 338 87 351 104 338 105 359 122 346 126 358 139 354 128 336 149 337 138 319 160 320 148 302 170 303 159 284 168 287 168 273 148 278 142 258 145 272 164 262 172 279 185 276 175 257 197 259 187 241 208 243 198 224 219 226 209 207 224 202 220 192 226 199 231 196 245 193 235 174 256 177 246 158 260 155 274 155 267 134 288 141 282 120 302 126 296 106 313 102 327 101 319 81 338 73 352 73 346 52 366 60 361 39 380 34 393 38 394 17 411 29 412 8 429 21 430 -1 440 5 448 17 462 0 467 21 481 5 496 18 499 32 518 22 515 43 534 34 543 52 544 66 564 58 570 76 569 90 590 86 582 105 603 101 594 121 615 117 614 130 614 144 635 137 628 158 649 152 655 171 653 185 674 182 665 201 686 198 684 211 698 209 690 190 700 192 687 197 700 214 688 218 686 232 707 229 697 248 718 246 708 265 730 262 720 281 741 279 736 288 750 290 747 269 766 278 763 257 778 258 764 260 773 279 752 275 760 295 744 299 741 312 762 311 751 330 772 329 761 347 783 346 777 355 780 364 793 369 794 347 811 361 813 339 830 353 831 331 839 338 825 336 829 358 809 348 813 370 793 361 797 382 786 378 780 391 802 394 787 409 809 412 794 428 815 431 810 444 821 452 828 431 841 448 848 428 861 445 867 424 879 432 866 427 865 449 848 436 846 457 829 444 828 465 816 460 808 471 828 479 811 491 831 498 813 511 833 518 825 527 834 537 846 519 854 539 866 520 874 540 886 522 896 532 884 525 878 545 865 528 859 549 845 532 839 553 824 546 813 555 831 567 811 575 828 587 808 594 826 607 816 610 823 622 838 607 842 628 857 612 861 633 876 618 880 622 870 640 861 621 850 639 841 620 830 638 821 619 810 637 810 628 797 634 811 650 790 652 803 669 782 671 796 687 786 686 790 699 808 688 807 709 825 698 824 719 843 708 842 730 860 718 861 730 854 718 839 734 835 713 820 728 816 707 801 723 796 702 781 717 774 705 760 707 770 727 749 724 758 743 741 750 741 764 762 757 756 778 776 771 770 792 790 785 784 806 805 799 810 818 806 805 788 816 789 795 771 806 771 785 753 797 754 775 736 787 736 765 718 777 722 768 708 768 713 788 693 781 699 802 684 802 681 816 703 814 692 832 713 831 703 849 724 848 713 866 735 865 724 883 746 881 741 892 740 878 720 885 726 865 705 872 711 851 691 858 696 837 676 844 682 824 662 831 662 818 649 814 649 836 631 823 631 845 619 840 613 853 635 856 620 871 641 875 626 890 648 894 633 909 654 913 639 928 661 931 656 947 658 933 637 936 647 917 626 919 636 900 615 903 625 884 603 886 614 867 592 869 592 852 580 846 576 867 561 851 556 872 548 865 540 876 560 884 542 896 562 904 545 916 565 923 547 936 567 943 549 955 569 963 562 982 567 969 546 967 560 951 539 948 553 932 532 929 546 913 525 911 539 895 517 892 520 872 509 864 501 884 489 867 482 887 476 878 465 887 483 899 462 906 480 919 460 926 477 939 457 946 474 959 454 966 472 978 460 994 468 983 448 975 466 963 446 955 463 943 443 936 461 923 441 916 459 904 439 896 446 878 z" />
<path fill="#404040" d="M 391 214 383 234 371 216 363 236 354 228 342 222 338 243 323 228 319 249 304 235 300 256 296 247 283 242 282 264 265 251 263 272 246 259 245 280 240 272 226 272 232 293 212 286 218 307 197 300 203 320 188 322 175 326 186 344 165 343 176 361 155 361 166 379 156 378 144 384 158 400 136 403 151 419 129 421 143 437 130 446 120 455 138 467 118 475 136 487 116 495 135 507 124 520 135 529 143 509 155 527 163 508 175 526 183 506 195 524 203 505 204 514 217 519 219 498 235 511 237 490 254 504 256 482 270 487 283 491 283 469 301 481 301 460 316 462 331 450 345 451 340 430 360 438 355 417 375 425 370 404 380 408 393 404 382 386 403 387 392 369 404 366 416 359 401 344 422 340 407 325 421 311 432 303 416 289 437 284 420 270 432 259 433 245 412 249 420 230 399 234 402 222 z" />
<path fill="#404040" d="M 467 232 486 242 476 254 469 266 489 272 473 285 494 291 477 305 498 311 481 324 502 330 496 347 491 360 512 362 508 377 507 391 528 386 521 406 542 401 534 421 555 416 551 426 553 440 572 431 568 452 588 444 596 463 598 477 618 467 622 482 628 495 644 481 646 502 663 488 669 501 676 513 691 497 696 517 710 501 721 514 730 525 742 507 750 527 762 510 769 530 782 512 784 522 788 520 796 509 776 501 794 489 774 481 782 464 788 452 767 448 782 433 761 429 776 413 755 410 770 394 749 391 756 382 758 368 737 371 747 352 725 355 735 336 714 338 718 311 697 314 706 295 685 298 692 292 689 279 670 289 672 268 653 279 655 257 636 268 638 247 619 257 622 248 615 236 600 252 596 231 581 246 576 225 562 241 558 230 549 220 537 237 529 218 517 236 509 216 497 234 489 214 477 232 477 222 z" />
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg">
<path fill="#808080" d="M 815000 -18000 831750 -8750 846000 7000 871000 46000 877000 60000 884000 74000 886000 79000 892000 89000 896000 100000 897250 104000 897000 106000 895500 105500 893000 102000 887000 93000 881000 83000 879000 78000 878500 77000 881999 81999 881000 80000 880000 79000 878000 76000 877000 74000 876000 73000 874000 69000 872000 67000 871000 65000 870000 64000 859250 51750 847000 45000 849000 45000 835500 40750 823000 42000 811250 48250 800000 57000 782000 82000 768000 110000 749500 152750 734000 197000 708000 288000 686000 368000 679000 391000 671000 414000 667250 418250 662000 421000 651000 425000 631250 426250 612000 422000 590500 410250 572000 393000 556750 373000 543000 351000 510750 290500 484000 225000 467898 169648 465000 192000 448000 257000 438000 288250 426000 318000 413000 344500 398000 370000 381750 393000 363000 412000 344000 423000 334500 427000 324000 429000 313000 429000 303000 428000 291000 426000 279000 422000 265625 415563 253500 407250 242625 397063 233000 385000 215750 358250 200000 330000 185250 297250 173000 263000 162750 228250 154000 192000 145000 140000 139624 105054 138000 115000 133000 165000 125000 225000 115000 284000 101000 337000 91500 362250 79000 386000 63750 405750 46000 421000 26250 431750 5000 438000 -9000 439000 -20000 439000 -24000 435000 -27000 430000 -33000 421000 -38000 409000 -43000 398000 -48000 388000 -48000 386500 -50000 382000 -51000 378250 -50000 377000 -27000 376500 -4000 371000 17500 360000 36000 343000 51250 322750 63000 300000 80000 250000 92250 194500 99000 138000 113000 25000 117000 5000 121000 8000 124000 13000 127000 19000 136750 37000 144000 55000 152500 80250 158000 106000 168000 158000 178000 218000 190000 278000 191000 282000 193000 288000 194000 292000 196000 297000 202000 318000 203000 320999 203001 320999 203001 321001 204000 322750 203801 322601 203800 322601 204000 323000 206000 325000 209000 329000 210000 331000 216000 337000 228250 348500 243000 359000 242000 357000 264250 365250 287000 368000 306250 365500 326000 356000 346500 339500 364000 318000 378750 294250 391000 269000 410000 223750 425000 176000 436000 127500 443000 78000 443000 79000 444000 52000 444000 27000 444250 24500 445000 25000 447000 28000 457000 46000 468000 69000 471000 77000 473000 85000 476000 130000 485000 176000 487000 189000 491000 204000 492000 206000 492500 208250 492000 207000 494000 215000 496000 222000 498000 230000 502000 245000 507000 259000 508500 263750 509000 266000 510000 270000 513000 277000 515000 284000 515353 284999 515354 284999 515354 285000 515500 285000 517000 288999 517001 288999 517001 289001 517000 289001 518000 292000 520000 295000 520000 295500 520250 295938 520000 295250 521000 297000 522000 298000 524000 303000 525750 306000 526999 307922 527000 307922 527000 307750 528999 310999 529001 310999 529001 311001 532000 316000 534000 319000 537000 322000 541000 328000 547000 333000 552000 340000 563000 350000 577000 359000 575000 357000 594000 364000 603500 365000 614000 364000 624000 362750 634000 359000 637500 358000 640000 355000 643750 348750 647000 340000 651000 327000 656000 314000 678000 228000 703000 144000 717000 99000 735000 56000 751000 23000 772000 -5000 792000 -19000 803250 -20750 z M 202999 320999 202430 320287 202429 320287 203000 321999 203001 321999 203001 322001 203443 322332 203445 322332 203445 322334 203799 322599 203000 321001 202999 321001 z M 203000 321999 202200 320001 202199 320001 202199 319999 202000 319750 z M 202000 319500 202200 319999 202201 319999 202201 320001 202428 320285 202429 320285 202000 319000 z M 527000 307924 527000 308000 528999 310999 527001 307924 z M 516500 288250 516999 288999 516200 287401 z M 515000 285000 516199 287399 516200 287399 515353 285001 515352 285001 515352 285000 z M 881000 81500 881000 81878 881063 82000 882250 84000 882563 84000 882000 82001 881999 82001 881999 81999 880333 80333 z " />
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,30 @@
before_layer_gcode =
between_objects_gcode =
end_filament_gcode = "; Filament-specific end gcode \n;END gcode for filament\n"
end_gcode = M104 S0 ; turn off temperature\nG28 X0 ; home X axis\nM84 ; disable motors\n
extrusion_axis = E
extrusion_multiplier = 1
filament_cost = 0
filament_density = 0
filament_diameter = 3
filament_max_volumetric_speed = 0
gcode_comments = 0
gcode_flavor = reprap
layer_gcode =
max_print_speed = 80
max_volumetric_speed = 0
retract_length = 2
retract_length_toolchange = 10
retract_lift = 1.5
retract_lift_above = 0
retract_lift_below = 0
retract_restart_extra = 0
retract_restart_extra_toolchange = 0
retract_speed = 40
start_filament_gcode = "; Filament gcode\n"
start_gcode = G28 ; home all axes\nG1 Z5 F5000 ; lift nozzle\n
toolchange_gcode =
travel_speed = 130
use_firmware_retraction = 0
use_relative_e_distances = 0
use_volumetric_e = 0

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 100" width="50px" height="100px">
<path fill="#808080" d="M 10,10 11,90 20,80 11,70 25,50 V 22 L 10,20 40,15 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
filament_colour = #ABCD

View File

@@ -0,0 +1 @@
solid STL generated by MeshLab

View File

@@ -0,0 +1,86 @@
solid STL generated by MeshLab
facet normal 0.000000e+00 -0.000000e+00 -1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal -0.000000e+00 0.000000e+00 -1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 0.000000e+00 1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 0.000000e+00 0.000000e+00 1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 2.000000e+01 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 1.000000e+00 0.000000e+00 -0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 2.000000e+01 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 0.000000e+00 2.000000e+01
vertex 2.000000e+01 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 -1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 2.000000e+01 0.000000e+00 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 0.000000e+00 -1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 0.000000e+00 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal -1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 0.000000e+00 2.000000e+01 2.000000e+01
endloop
endfacet
facet normal -1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 0.000000e+00
vertex 0.000000e+00 2.000000e+01 2.000000e+01
endloop
endfacet
endsolid vcg

View File

@@ -0,0 +1,86 @@
solid STL generated by MeshLab
facet normal 0.000000e+00 -0.000000e+00 -1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal -0.000000e+00 0.000000e+00 -1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 0.000000e+00 1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 0.000000e+00 0.000000e+00 1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 2.000000e+01 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 1.000000e+00 0.000000e+00 -0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 2.000000e+01 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 0.000000e+00 2.000000e+01
vertex 2.000000e+01 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 -1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 2.000000e+01 0.000000e+00 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 0.000000e+00 -1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 0.000000e+00 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal -1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 0.000000e+00 2.000000e+01 2.000000e+01
endloop
endfacet
facet normal -1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 0.000000e+00
vertex 0.000000e+00 2.000000e+01 2.000000e+01
endloop
endfacet
endsolid

View File

@@ -0,0 +1,86 @@
solid STL generated by MeshLab
facet normal 0.000000e+00 -0.000000e+00 -1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal -0.000000e+00 0.000000e+00 -1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 0.000000e+00 1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 0.000000e+00 0.000000e+00 1.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 2.000000e+01 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 1.000000e+00 0.000000e+00 -0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 2.000000e+01 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 0.000000e+00 2.000000e+01
vertex 2.000000e+01 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal 0.000000e+00 -1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 2.000000e+01 0.000000e+00 2.000000e+01
vertex 0.000000e+00 0.000000e+00 2.000000e+01
endloop
endfacet
facet normal 0.000000e+00 -1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 0.000000e+00 0.000000e+00 0.000000e+00
endloop
endfacet
facet normal +inf -inf weirdvalue
outer loop
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 0.000000e+00 2.000000e+01
vertex 0.000000e+00 2.000000e+01 2.000000e+01
endloop
endfacet
facet normal -1.000000e+00 0.000000e+00 0.000000e+00
outer loop
vertex 0.000000e+00 0.000000e+00 0.000000e+00
vertex 0.000000e+00 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop
endfacet blah
facet normal 0.000000e+00 1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 0.000000e+00 2.000000e+01 0.000000e+00
endloop foo
endfacet bar
facet normal 0.000000e+00 1.000000e+00 0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 0.000000e+00 2.000000e+01 0.000000e+00
vertex 0.000000e+00 2.000000e+01 2.000000e+01
endloop foo
endfacet bar
endsolid some blah blah

Binary file not shown.

View File

@@ -0,0 +1,6 @@
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)
# catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${CATCH_EXTRA_ARGS})

View File

@@ -0,0 +1,5 @@
#include <catch_main.hpp>
TEST_CASE("Is example succesful", "[example]") {
REQUIRE(true);
}

View File

@@ -0,0 +1,38 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_avoid_crossing_perimeters.cpp
test_bridges.cpp
test_cooling.cpp
test_clipper.cpp
test_custom_gcode.cpp
test_data.cpp
test_data.hpp
test_extrusion_entity.cpp
test_fill.cpp
test_flow.cpp
test_gaps.cpp
test_gcode.cpp
test_gcodefindreplace.cpp
test_gcodewriter.cpp
test_model.cpp
test_multi.cpp
test_perimeters.cpp
test_print.cpp
test_printgcode.cpp
test_printobject.cpp
test_shells.cpp
test_skirt_brim.cpp
test_support_material.cpp
test_thin_walls.cpp
test_trianglemesh.cpp
)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
if (WIN32)
qidislicer_copy_dlls(${_TEST_NAME}_tests)
endif()
# catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${CATCH_EXTRA_ARGS})

View File

@@ -0,0 +1,18 @@
#include <catch_main.hpp>
#include "libslic3r/libslic3r.h"
// __has_feature() is used later for Clang, this is for compatibility with other compilers (such as GCC and MSVC)
#ifndef __has_feature
# define __has_feature(x) 0
#endif
// Print reports about memory leaks but exit with zero exit code when any memory leaks is found to make unit tests pass.
// After merging the stable branch (2.4.1) with the master branch, this should be deleted.
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
extern "C" {
const char *__lsan_default_options() {
return "exitcode=0";
}
}
#endif

View File

@@ -0,0 +1,16 @@
#include <catch2/catch.hpp>
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Avoid crossing perimeters", "[AvoidCrossingPerimeters]") {
WHEN("Two 20mm cubes sliced") {
std::string gcode = Slic3r::Test::slice(
{ Slic3r::Test::TestMesh::cube_20x20x20, Slic3r::Test::TestMesh::cube_20x20x20 },
{ { "avoid_crossing_perimeters", true } });
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
}
}

View File

@@ -0,0 +1,133 @@
#include <catch2/catch.hpp>
#include <libslic3r/BridgeDetector.hpp>
#include <libslic3r/Geometry.hpp>
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Bridge detector", "[Bridging]")
{
auto check_angle = [](const ExPolygons &lower, const ExPolygon &bridge, double expected, double tolerance = -1, double expected_coverage = -1)
{
if (expected_coverage < 0)
expected_coverage = bridge.area();
BridgeDetector bridge_detector(bridge, lower, scaled<coord_t>(0.5)); // extrusion width
if (tolerance < 0)
tolerance = Geometry::rad2deg(bridge_detector.resolution) + EPSILON;
bridge_detector.detect_angle();
double result = bridge_detector.angle;
Polygons coverage = bridge_detector.coverage();
THEN("correct coverage area") {
REQUIRE(is_approx(area(coverage), expected_coverage));
}
// our epsilon is equal to the steps used by the bridge detection algorithm
//##use XXX; YYY [ rad2deg($result), $expected ];
// returned value must be non-negative, check for that too
double delta = Geometry::rad2deg(result) - expected;
if (delta >= 180. - EPSILON)
delta -= 180;
return result >= 0. && std::abs(delta) < tolerance;
};
GIVEN("O-shaped overhang") {
auto test = [&check_angle](const Point &size, double rotate, double expected_angle, double tolerance = -1) {
ExPolygon lower{
Polygon::new_scale({ {-2,-2}, {size.x()+2,-2}, {size.x()+2,size.y()+2}, {-2,size.y()+2} }),
Polygon::new_scale({ {0,0}, {0,size.y()}, {size.x(),size.y()}, {size.x(),0} } )
};
lower.rotate(Geometry::deg2rad(rotate), size / 2);
ExPolygon bridge_expoly(lower.holes.front());
bridge_expoly.contour.reverse();
return check_angle({ lower }, bridge_expoly, expected_angle, tolerance);
};
WHEN("Bridge size 20x10") {
bool valid = test({20,10}, 0., 90.);
THEN("bridging angle is 90 degrees") {
REQUIRE(valid);
}
}
WHEN("Bridge size 10x20") {
bool valid = test({10,20}, 0., 0.);
THEN("bridging angle is 0 degrees") {
REQUIRE(valid);
}
}
WHEN("Bridge size 20x10, rotated by 45 degrees") {
bool valid = test({20,10}, 45., 135., 20.);
THEN("bridging angle is 135 degrees") {
REQUIRE(valid);
}
}
WHEN("Bridge size 20x10, rotated by 135 degrees") {
bool valid = test({20,10}, 135., 45., 20.);
THEN("bridging angle is 45 degrees") {
REQUIRE(valid);
}
}
}
GIVEN("two-sided bridge") {
ExPolygon bridge{ Polygon::new_scale({ {0,0}, {20,0}, {20,10}, {0,10} }) };
ExPolygons lower { ExPolygon{ Polygon::new_scale({ {-2,0}, {0,0}, {0,10}, {-2,10} }) } };
lower.emplace_back(lower.front());
lower.back().translate(Point::new_scale(22, 0));
THEN("Bridging angle 0 degrees") {
REQUIRE(check_angle(lower, bridge, 0));
}
}
GIVEN("for C-shaped overhang") {
ExPolygon bridge{ Polygon::new_scale({ {0,0}, {20,0}, {10,10}, {0,10} }) };
ExPolygon lower{ Polygon::new_scale({ {0,0}, {0,10}, {10,10}, {10,12}, {-2,12}, {-2,-2}, {22,-2}, {22,0} }) };
bool valid = check_angle({ lower }, bridge, 135);
THEN("Bridging angle is 135 degrees") {
REQUIRE(valid);
}
}
GIVEN("square overhang with L-shaped anchors") {
ExPolygon bridge{ Polygon::new_scale({ {10,10}, {20,10}, {20,20}, {10,20} }) };
ExPolygon lower{ Polygon::new_scale({ {10,10}, {10,20}, {20,20}, {30,30}, {0,30}, {0,0} }) };
bool valid = check_angle({ lower }, bridge, 45., -1., bridge.area() / 2.);
THEN("Bridging angle is 45 degrees") {
REQUIRE(valid);
}
}
}
SCENARIO("Bridging integration", "[Bridging]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "top_solid_layers", 0 },
// to prevent bridging on sparse infill
{ "bridge_speed", 99 }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::bridge }, config);
GCodeReader parser;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
// angle => length
std::map<coord_t, double> extrusions;
parser.parse_buffer(gcode, [&extrusions, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (line.cmd() == "G1" && is_approx<double>(bridge_speed, line.new_F(self))) {
// Accumulate lengths of bridging extrusions according to bridging angle.
Line l{ self.xy_scaled(), line.new_XY_scaled(self) };
size_t angle = scaled<coord_t>(l.direction());
auto it = extrusions.find(angle);
if (it == extrusions.end())
it = extrusions.insert(std::make_pair(angle, 0.)).first;
it->second += l.length();
}
});
THEN("bridge is generated") {
REQUIRE(! extrusions.empty());
}
THEN("bridge has the expected direction 0 degrees") {
// Bridging with the longest extrusion.
auto it_longest_extrusion = std::max_element(extrusions.begin(), extrusions.end(),
[](const auto &e1, const auto &e2){ return e1.second < e2.second; });
REQUIRE(it_longest_extrusion->first == 0);
}
}

View File

@@ -0,0 +1,203 @@
#include <catch2/catch.hpp>
#include "test_data.hpp"
#include "libslic3r/ClipperZUtils.hpp"
#include "libslic3r/clipper.hpp"
using namespace Slic3r;
// tests for ExPolygon::overlaps(const ExPolygon &other)
SCENARIO("Clipper intersection with polyline", "[Clipper]")
{
struct TestData {
ClipperLib::Path subject;
ClipperLib::Path clip;
ClipperLib::Paths result;
};
auto run_test = [](const TestData &data) {
ClipperLib::Clipper clipper;
clipper.AddPath(data.subject, ClipperLib::ptSubject, false);
clipper.AddPath(data.clip, ClipperLib::ptClip, true);
ClipperLib::PolyTree polytree;
ClipperLib::Paths paths;
clipper.Execute(ClipperLib::ctIntersection, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero);
ClipperLib::PolyTreeToPaths(polytree, paths);
REQUIRE(paths == data.result);
};
WHEN("Open polyline completely inside stays inside") {
run_test({
{ { 10, 0 }, { 20, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 20, 0 }, { 10, 0 } } }
});
};
WHEN("Closed polyline completely inside stays inside") {
run_test({
{ { 10, 0 }, { 20, 0 }, { 20, 20 }, { 10, 20 }, { 10, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 10, 0 }, { 20, 0 }, { 20, 20 }, { 10, 20 }, { 10, 0 } } }
});
};
WHEN("Polyline which crosses right rectangle boundary is trimmed") {
run_test({
{ { 10, 0 }, { 2000, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 1000, 0 }, { 10, 0 } } }
});
};
WHEN("Polyline which is outside clipping region is removed") {
run_test({
{ { 1500, 0 }, { 2000, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
};
WHEN("Polyline on left vertical boundary is kept") {
run_test({
{ { -1000, -1000 }, { -1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { -1000, -1000 }, { -1000, 1000 } } }
});
run_test({
{ { -1000, 1000 }, { -1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { -1000, 1000 }, { -1000, -1000 } } }
});
};
WHEN("Polyline on right vertical boundary is kept") {
run_test({
{ { 1000, -1000 }, { 1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 1000, -1000 }, { 1000, 1000 } } }
});
run_test({
{ { 1000, 1000 }, { 1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 1000, 1000 }, { 1000, -1000 } } }
});
};
WHEN("Polyline on bottom horizontal boundary is removed") {
run_test({
{ { -1000, -1000 }, { 1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
run_test({
{ { 1000, -1000 }, { -1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
};
WHEN("Polyline on top horizontal boundary is removed") {
run_test({
{ { -1000, 1000 }, { 1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
run_test({
{ { 1000, 1000 }, { -1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
};
}
SCENARIO("Clipper Z", "[ClipperZ]")
{
ClipperLib_Z::Path subject { { -2000, -1000, 10 }, { -2000, 1000, 10 }, { 2000, 1000, 10 }, { 2000, -1000, 10 } };
ClipperLib_Z::Path clip{ { -1000, -2000, -5 }, { -1000, 2000, -5 }, { 1000, 2000, -5 }, { 1000, -2000, -5 } };
ClipperLib_Z::Clipper clipper;
clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot,
const ClipperLib_Z::IntPoint &e2top, ClipperLib_Z::IntPoint &pt) {
pt.z() = 1;
});
clipper.AddPath(subject, ClipperLib_Z::ptSubject, false);
clipper.AddPath(clip, ClipperLib_Z::ptClip, true);
ClipperLib_Z::PolyTree polytree;
ClipperLib_Z::Paths paths;
clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
ClipperLib_Z::PolyTreeToPaths(polytree, paths);
REQUIRE(paths.size() == 1);
REQUIRE(paths.front().size() == 2);
for (const ClipperLib_Z::IntPoint &pt : paths.front())
REQUIRE(pt.z() == 1);
}
SCENARIO("Intersection with multiple polylines", "[ClipperZ]")
{
// 1000x1000 CCQ square
ClipperLib_Z::Path clip { { 0, 0, 1 }, { 1000, 0, 1 }, { 1000, 1000, 1 }, { 0, 1000, 1 } };
// Two lines interseting inside the square above, crossing the bottom edge of the square.
ClipperLib_Z::Path line1 { { +100, -100, 2 }, { +900, +900, 2 } };
ClipperLib_Z::Path line2 { { +100, +900, 3 }, { +900, -100, 3 } };
ClipperLib_Z::Clipper clipper;
ClipperZUtils::ClipperZIntersectionVisitor::Intersections intersections;
ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections);
clipper.ZFillFunction(visitor.clipper_callback());
clipper.AddPath(line1, ClipperLib_Z::ptSubject, false);
clipper.AddPath(line2, ClipperLib_Z::ptSubject, false);
clipper.AddPath(clip, ClipperLib_Z::ptClip, true);
ClipperLib_Z::PolyTree polytree;
ClipperLib_Z::Paths paths;
clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
ClipperLib_Z::PolyTreeToPaths(polytree, paths);
REQUIRE(paths.size() == 2);
THEN("First output polyline is a trimmed 2nd line") {
// Intermediate point (intersection) was removed)
REQUIRE(paths.front().size() == 2);
REQUIRE(paths.front().front().z() == 3);
REQUIRE(paths.front().back().z() < 0);
REQUIRE(intersections[- paths.front().back().z() - 1] == std::pair<coord_t, coord_t>(1, 3));
}
THEN("Second output polyline is a trimmed 1st line") {
// Intermediate point (intersection) was removed)
REQUIRE(paths[1].size() == 2);
REQUIRE(paths[1].front().z() < 0);
REQUIRE(paths[1].back().z() == 2);
REQUIRE(intersections[- paths[1].front().z() - 1] == std::pair<coord_t, coord_t>(1, 2));
}
}
SCENARIO("Interseting a closed loop as an open polyline", "[ClipperZ]")
{
// 1000x1000 CCQ square
ClipperLib_Z::Path clip{ { 0, 0, 1 }, { 1000, 0, 1 }, { 1000, 1000, 1 }, { 0, 1000, 1 } };
// Two lines interseting inside the square above, crossing the bottom edge of the square.
ClipperLib_Z::Path rect{ { 500, 500, 2}, { 500, 1500, 2 }, { 1500, 1500, 2}, { 500, 1500, 2}, { 500, 500, 2 } };
ClipperLib_Z::Clipper clipper;
clipper.AddPath(rect, ClipperLib_Z::ptSubject, false);
clipper.AddPath(clip, ClipperLib_Z::ptClip, true);
ClipperLib_Z::PolyTree polytree;
ClipperLib_Z::Paths paths;
ClipperZUtils::ClipperZIntersectionVisitor::Intersections intersections;
ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections);
clipper.ZFillFunction(visitor.clipper_callback());
clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
ClipperLib_Z::PolyTreeToPaths(std::move(polytree), paths);
THEN("Open polyline is clipped into two pieces") {
REQUIRE(paths.size() == 2);
REQUIRE(paths.front().size() == 2);
REQUIRE(paths.back().size() == 2);
REQUIRE(paths.front().front().z() == 2);
REQUIRE(paths.back().back().z() == 2);
REQUIRE(paths.front().front().x() == paths.back().back().x());
REQUIRE(paths.front().front().y() == paths.back().back().y());
}
}

View File

@@ -0,0 +1,275 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "test_data.hpp" // get access to init_print, etc
#include "libslic3r/Config.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/GCode/CoolingBuffer.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
std::unique_ptr<CoolingBuffer> make_cooling_buffer(
GCode &gcode,
const DynamicPrintConfig &config = DynamicPrintConfig{},
const std::vector<unsigned int> &extruder_ids = { 0 })
{
PrintConfig print_config;
print_config.apply(config, true); // ignore_nonexistent
gcode.apply_print_config(print_config);
gcode.set_layer_count(10);
gcode.writer().set_extruders(extruder_ids);
gcode.writer().set_extruder(0);
return std::make_unique<CoolingBuffer>(gcode);
}
SCENARIO("Cooling unit tests", "[Cooling]") {
const std::string gcode1 = "G1 X100 E1 F3000\n";
// 2 sec
const double print_time1 = 100. / (3000. / 60.);
const std::string gcode2 = gcode1 + "G1 X0 E1 F3000\n";
// 4 sec
const double print_time2 = 2. * print_time1;
auto config = DynamicPrintConfig::full_print_config_with({
// Default cooling settings.
{ "bridge_fan_speed", "100" },
{ "cooling", "1" },
{ "fan_always_on", "0" },
{ "fan_below_layer_time", "60" },
{ "max_fan_speed", "100" },
{ "min_print_speed", "10" },
{ "slowdown_below_layer_time", "5" },
// Default print speeds.
{ "bridge_speed", 60 },
{ "external_perimeter_speed", "50%" },
{ "first_layer_speed", 30 },
{ "gap_fill_speed", 20 },
{ "infill_speed", 80 },
{ "perimeter_speed", 60 },
{ "small_perimeter_speed", 15 },
{ "solid_infill_speed", 20 },
{ "top_solid_infill_speed", 15 },
{ "max_print_speed", 80 },
// Override for tests.
{ "disable_fan_first_layers", "0" }
});
WHEN("G-code block 3") {
THEN("speed is not altered when elapsed time is greater than slowdown threshold") {
// Print time of gcode.
const double print_time = 100. / (3000. / 60.);
//FIXME slowdown_below_layer_time is rounded down significantly from 1.8s to 1s.
config.set_deserialize_strict({ { "slowdown_below_layer_time", { int(print_time * 0.999) } } });
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer("G1 F3000;_EXTRUDE_SET_SPEED\nG1 X100 E1", 0, true);
bool speed_not_altered = gcode.find("F3000") != gcode.npos;
REQUIRE(speed_not_altered);
}
}
WHEN("G-code block 4") {
const std::string gcode_src =
"G1 X50 F2500\n"
"G1 F3000;_EXTRUDE_SET_SPEED\n"
"G1 X100 E1\n"
";_EXTRUDE_END\n"
"G1 E4 F400";
// Print time of gcode.
const double print_time = 50. / (2500. / 60.) + 100. / (3000. / 60.) + 4. / (400. / 60.);
config.set_deserialize_strict({ { "slowdown_below_layer_time", { int(print_time * 1.001) } } });
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer(gcode_src, 0, true);
THEN("speed is altered when elapsed time is lower than slowdown threshold") {
bool speed_is_altered = gcode.find("F3000") == gcode.npos;
REQUIRE(speed_is_altered);
}
THEN("speed is not altered for travel moves") {
bool speed_not_altered = gcode.find("F2500") != gcode.npos;
REQUIRE(speed_not_altered);
}
THEN("speed is not altered for extruder-only moves") {
bool speed_not_altered = gcode.find("F400") != gcode.npos;
REQUIRE(speed_not_altered);
}
}
WHEN("G-code block 1") {
THEN("fan is not activated when elapsed time is greater than fan threshold") {
config.set_deserialize_strict({
{ "fan_below_layer_time" , int(print_time1 * 0.88) },
{ "slowdown_below_layer_time" , int(print_time1 * 0.99) }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer(gcode1, 0, true);
bool fan_not_activated = gcode.find("M106") == gcode.npos;
REQUIRE(fan_not_activated);
}
}
WHEN("G-code block 1 with two extruders") {
config.set_deserialize_strict({
{ "cooling", "1, 0" },
{ "fan_below_layer_time", { int(print_time2 + 1.), int(print_time2 + 1.) } },
{ "slowdown_below_layer_time", { int(print_time2 + 2.), int(print_time2 + 2.) } }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config, { 0, 1 });
std::string gcode = buffer->process_layer(gcode1 + "T1\nG1 X0 E1 F3000\n", 0, true);
THEN("fan is activated for the 1st tool") {
bool ok = gcode.find("M106") == 0;
REQUIRE(ok);
}
THEN("fan is disabled for the 2nd tool") {
bool ok = gcode.find("\nM107") > 0;
REQUIRE(ok);
}
}
WHEN("G-code block 2") {
THEN("slowdown is computed on all objects printing at the same Z") {
config.set_deserialize_strict({ { "slowdown_below_layer_time", int(print_time2 * 0.99) } });
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer(gcode2, 0, true);
bool ok = gcode.find("F3000") != gcode.npos;
REQUIRE(ok);
}
THEN("fan is not activated on all objects printing at different Z") {
config.set_deserialize_strict({
{ "fan_below_layer_time", int(print_time2 * 0.65) },
{ "slowdown_below_layer_time", int(print_time2 * 0.7) }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
// use an elapsed time which is < the threshold but greater than it when summed twice
std::string gcode = buffer->process_layer(gcode2, 0, true) + buffer->process_layer(gcode2, 1, true);
bool fan_not_activated = gcode.find("M106") == gcode.npos;
REQUIRE(fan_not_activated);
}
THEN("fan is activated on all objects printing at different Z") {
// use an elapsed time which is < the threshold even when summed twice
config.set_deserialize_strict({
{ "fan_below_layer_time", int(print_time2 + 1) },
{ "slowdown_below_layer_time", int(print_time2 + 1) }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
// use an elapsed time which is < the threshold but greater than it when summed twice
std::string gcode = buffer->process_layer(gcode2, 0, true) + buffer->process_layer(gcode2, 1, true);
bool fan_activated = gcode.find("M106") != gcode.npos;
REQUIRE(fan_activated);
}
}
}
SCENARIO("Cooling integration tests", "[Cooling]") {
GIVEN("overhang") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "cooling", { 1 } },
{ "bridge_fan_speed", { 100 } },
{ "fan_below_layer_time", { 0 } },
{ "slowdown_below_layer_time", { 0 } },
{ "bridge_speed", 99 },
{ "enable_dynamic_overhang_speeds", false },
// internal bridges use solid_infil speed
{ "bottom_solid_layers", 1 },
// internal bridges use solid_infil speed
});
GCodeReader parser;
int fan = 0;
int fan_with_incorrect_speeds = 0;
int fan_with_incorrect_print_speeds = 0;
int bridge_with_no_fan = 0;
const double bridge_speed = config.opt_float("bridge_speed") * 60;
parser.parse_buffer(
Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config),
[&fan, &fan_with_incorrect_speeds, &fan_with_incorrect_print_speeds, &bridge_with_no_fan, bridge_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("M106")) {
line.has_value('S', fan);
if (fan != 255)
++ fan_with_incorrect_speeds;
} else if (line.cmd_is("M107")) {
fan = 0;
} else if (line.extruding(self) && line.dist_XY(self) > 0) {
if (is_approx<double>(line.new_F(self), bridge_speed)) {
if (fan != 255)
++ bridge_with_no_fan;
} else {
if (fan != 0)
++ fan_with_incorrect_print_speeds;
}
}
});
THEN("bridge fan speed is applied correctly") {
REQUIRE(fan_with_incorrect_speeds == 0);
}
THEN("bridge fan is only turned on for bridges") {
REQUIRE(fan_with_incorrect_print_speeds == 0);
}
THEN("bridge fan is turned on for all bridges") {
REQUIRE(bridge_with_no_fan == 0);
}
}
GIVEN("20mm cube") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "cooling", { 1 } },
{ "fan_below_layer_time", { 0 } },
{ "slowdown_below_layer_time", { 10 } },
{ "min_print_speed", { 0 } },
{ "start_gcode", "" },
{ "first_layer_speed", "100%" },
{ "external_perimeter_speed", 99 }
});
GCodeReader parser;
const double external_perimeter_speed = config.opt<ConfigOptionFloatOrPercent>("external_perimeter_speed")->value * 60;
std::vector<double> layer_times;
// z => 1
std::map<coord_t, int> layer_external;
parser.parse_buffer(
Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
[&layer_times, &layer_external, external_perimeter_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("G1")) {
if (line.dist_Z(self) != 0) {
layer_times.emplace_back(0.);
layer_external[scaled<coord_t>(line.new_Z(self))] = 0;
}
double l = line.dist_XY(self);
if (l == 0)
l = line.dist_E(self);
if (l == 0)
l = line.dist_Z(self);
if (l > 0.) {
if (layer_times.empty())
layer_times.emplace_back(0.);
layer_times.back() += 60. * std::abs(l) / line.new_F(self);
}
if (line.has('F') && line.f() == external_perimeter_speed)
++ layer_external[scaled<coord_t>(self.z())];
}
});
THEN("slowdown_below_layer_time is honored") {
// Account for some inaccuracies.
const double slowdown_below_layer_time = config.opt<ConfigOptionInts>("slowdown_below_layer_time")->values.front() - 0.5;
size_t minimum_time_honored = std::count_if(layer_times.begin(), layer_times.end(),
[slowdown_below_layer_time](double t){ return t > slowdown_below_layer_time; });
REQUIRE(minimum_time_honored == layer_times.size());
}
THEN("slowdown_below_layer_time does not alter external perimeters") {
// Broken by Vojtech
// check that all layers have at least one unaltered external perimeter speed
// my $external = all { $_ > 0 } values %layer_external;
// ok $external, '';
}
}
}

View File

@@ -0,0 +1,273 @@
#include <catch2/catch.hpp>
#include <exception>
#include <numeric>
#include <sstream>
#include <boost/regex.hpp>
#include "libslic3r/Config.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Output file format", "[CustomGCode]")
{
WHEN("output_file_format set") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "travel_speed", "130"},
{ "layer_height", "0.4"},
{ "output_filename_format", "ts_[travel_speed]_lh_[layer_height].gcode" },
{ "start_gcode", "TRAVEL:[travel_speed] HEIGHT:[layer_height]\n" }
});
Print print;
Model model;
Test::init_print({ Test::TestMesh::cube_2x20x10 }, print, model, config);
std::string output_file = print.output_filepath({}, {});
THEN("print config options are replaced in output filename") {
REQUIRE(output_file == "ts_130_lh_0.4.gcode");
}
}
}
SCENARIO("Custom G-code", "[CustomGCode]")
{
WHEN("start_gcode and layer_gcode set") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "start_gcode", "_MY_CUSTOM_START_GCODE_" }, // to avoid dealing with the nozzle lift in start G-code
{ "layer_gcode", "_MY_CUSTOM_LAYER_GCODE_" }
});
GCodeReader parser;
bool last_move_was_z_change = false;
int num_layer_changes_not_applied = 0;
parser.parse_buffer(Slic3r::Test::slice({ Test::TestMesh::cube_2x20x10 }, config),
[&last_move_was_z_change, &num_layer_changes_not_applied](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (last_move_was_z_change != line.cmd_is("_MY_CUSTOM_LAYER_GCODE_"))
++ num_layer_changes_not_applied;
last_move_was_z_change = line.dist_Z(self) > 0;
});
THEN("custom layer G-code is applied after Z move and before other moves") {
REQUIRE(num_layer_changes_not_applied == 0);
}
};
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "nozzle_diameter", { 0.6,0.6,0.6,0.6 } },
{ "extruder", 2 },
{ "first_layer_temperature", { 200, 205 } }
});
config.normalize_fdm();
WHEN("Printing with single but non-zero extruder") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("temperature set correctly for non-zero yet single extruder") {
REQUIRE(Slic3r::Test::contains(gcode, "\nM104 S205 T1 ;"));
}
THEN("unused extruder correctly ignored") {
REQUIRE(! Slic3r::Test::contains_regex(gcode, "M104 S\\d+ T0"));
}
}
WHEN("Printing with two extruders") {
config.opt_int("infill_extruder") = 1;
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("temperature set correctly for first extruder") {
REQUIRE(Slic3r::Test::contains(gcode, "\nM104 S200 T0 ;"));
};
THEN("temperature set correctly for second extruder") {
REQUIRE(Slic3r::Test::contains(gcode, "\nM104 S205 T1 ;"));
};
}
auto test = [](DynamicPrintConfig &config) {
// we use the [infill_extruder] placeholder to make sure this test doesn't
// catch a false positive caused by the unparsed start G-code option itself
// being embedded in the G-code
config.opt_int("infill_extruder") = 1;
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("temperature placeholder for first extruder correctly populated") {
REQUIRE(Slic3r::Test::contains(gcode, "temp0:200"));
}
THEN("temperature placeholder for second extruder correctly populated") {
REQUIRE(Slic3r::Test::contains(gcode, "temp1:205"));
}
THEN("temperature placeholder for unused extruder populated with first value") {
REQUIRE(Slic3r::Test::contains(gcode, "temp2:200"));
}
};
WHEN("legacy syntax") {
config.set_deserialize_strict("start_gcode",
";__temp0:[first_layer_temperature_0]__\n"
";__temp1:[first_layer_temperature_1]__\n"
";__temp2:[first_layer_temperature_2]__\n");
test(config);
}
WHEN("new syntax") {
config.set_deserialize_strict("start_gcode",
";__temp0:{first_layer_temperature[0]}__\n"
";__temp1:{first_layer_temperature[1]}__\n"
";__temp2:{first_layer_temperature[2]}__\n");
test(config);
}
WHEN("Vojtech's syntax") {
config.set_deserialize_strict({
{ "infill_extruder", 1 },
{ "start_gcode",
";substitution:{if infill_extruder==1}extruder1"
"{elsif infill_extruder==2}extruder2"
"{else}extruder3{endif}"
}
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("if / else / endif - first block returned") {
REQUIRE(Test::contains(gcode, "\n;substitution:extruder1\n"));
}
}
GIVEN("Layer change G-codes")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "before_layer_gcode", ";BEFORE [layer_num]" },
{ "layer_gcode", ";CHANGE [layer_num]" },
{ "support_material", 1 },
{ "layer_height", 0.2 }
});
WHEN("before and after layer change G-codes set") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config);
GCodeReader parser;
std::vector<int> before;
std::vector<int> change;
parser.parse_buffer(gcode, [&before, &change](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line){
int d;
if (sscanf(line.raw().c_str(), ";BEFORE %d", &d) == 1)
before.emplace_back(d);
else if (sscanf(line.raw().c_str(), ";CHANGE %d", &d) == 1) {
change.emplace_back(d);
if (d != before.back())
throw std::runtime_error("inconsistent layer_num before and after layer change");
}
});
THEN("layer_num is consistent before and after layer changes") {
REQUIRE(before == change);
}
THEN("layer_num grows continously") {
// i.e. no duplicates or regressions
bool successive = true;
for (size_t i = 1; i < change.size(); ++ i)
if (change[i - 1] + 1 != change[i])
successive = false;
REQUIRE(successive);
}
}
}
GIVEN("if / elsif / elsif / elsif / else / endif")
{
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "nozzle_diameter", { 0.6,0.6,0.6,0.6,0.6 } },
{ "start_gcode",
";substitution:{if infill_extruder==1}if block"
"{elsif infill_extruder==2}elsif block 1"
"{elsif infill_extruder==3}elsif block 2"
"{elsif infill_extruder==4}elsif block 3"
"{else}endif block{endif}"
":end"
}
});
std::string returned[] = { "" /* indexed by one based extruder ID */, "if block", "elsif block 1", "elsif block 2", "elsif block 3", "endif block" };
auto test = [&config, &returned](int i) {
config.set_deserialize_strict({ { "infill_extruder", i } });
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
int found_error = 0;
for (int j = 1; j <= 5; ++ j)
if (i != j && Slic3r::Test::contains(gcode, std::string("substitution:") + returned[j] + ":end"))
// failure
++ found_error;
THEN(std::string("if / else / endif returned ") + returned[i]) {
REQUIRE(Slic3r::Test::contains(gcode, std::string("substitution:") + returned[i] + ":end"));
}
THEN(std::string("if / else / endif - only ") + returned[i] + "returned") {
REQUIRE(found_error == 0);
}
};
WHEN("infill_extruder == 1") { test(1); }
WHEN("infill_extruder == 2") { test(2); }
WHEN("infill_extruder == 3") { test(3); }
WHEN("infill_extruder == 4") { test(4); }
WHEN("infill_extruder == 5") { test(5); }
}
GIVEN("nested if / if / else / endif") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", { 0.6,0.6,0.6,0.6,0.6 } },
{ "start_gcode",
";substitution:{if infill_extruder==1}{if perimeter_extruder==1}block11{else}block12{endif}"
"{elsif infill_extruder==2}{if perimeter_extruder==1}block21{else}block22{endif}"
"{else}{if perimeter_extruder==1}block31{else}block32{endif}{endif}:end"
}
});
auto test = [&config](int i) {
config.opt_int("infill_extruder") = i;
int failed = 0;
for (int j = 1; j <= 2; ++ j) {
config.opt_int("perimeter_extruder") = j;
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
if (! Slic3r::Test::contains(gcode, std::string("substitution:block") + std::to_string(i) + std::to_string(j) + ":end"))
++ failed;
}
THEN(std::string("two level if / else / endif - block for infill_extruder ") + std::to_string(i) + "succeeded") {
REQUIRE(failed == 0);
}
};
WHEN("infill_extruder == 1") { test(1); }
WHEN("infill_extruder == 2") { test(2); }
WHEN("infill_extruder == 3") { test(3); }
}
GIVEN("printer type in notes") {
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "start_gcode",
";substitution:{if notes==\"MK2\"}MK2{elsif notes==\"MK3\"}MK3{else}MK1{endif}:end"
}
});
auto test = [&config](const std::string &printer_name) {
config.set_deserialize_strict("notes", printer_name);
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN(std::string("printer name ") + printer_name + " matched") {
REQUIRE(Slic3r::Test::contains(gcode, std::string("substitution:") + printer_name + ":end"));
}
};
WHEN("printer MK2") { test("MK2"); }
WHEN("printer MK3") { test("MK3"); }
WHEN("printer MK1") { test("MK1"); }
}
GIVEN("sequential print with between_objects_gcode") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "complete_objects", 1 },
{ "between_objects_gcode", "_MY_CUSTOM_GCODE_" }
});
std::string gcode = Slic3r::Test::slice(
// 3x 20mm box
{ Slic3r::Test::TestMesh::cube_20x20x20, Slic3r::Test::TestMesh::cube_20x20x20, Slic3r::Test::TestMesh::cube_20x20x20 },
config);
THEN("between_objects_gcode is applied correctly") {
const boost::regex expression("^_MY_CUSTOM_GCODE_");
const std::ptrdiff_t match_count =
std::distance(boost::sregex_iterator(gcode.begin(), gcode.end(), expression), boost::sregex_iterator());
REQUIRE(match_count == 2);
}
}
GIVEN("before_layer_gcode increments global variable") {
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "start_gcode", "{global counter=0}" },
{ "before_layer_gcode", ";Counter{counter=counter+1;counter}\n" }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("The counter is emitted multiple times before layer change.") {
REQUIRE(Slic3r::Test::contains(gcode, ";Counter1\n"));
REQUIRE(Slic3r::Test::contains(gcode, ";Counter2\n"));
REQUIRE(Slic3r::Test::contains(gcode, ";Counter3\n"));
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,90 @@
#ifndef SLIC3R_TEST_DATA_HPP
#define SLIC3R_TEST_DATA_HPP
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <unordered_map>
namespace Slic3r { namespace Test {
constexpr double MM_PER_MIN = 60.0;
/// Enumeration of test meshes
enum class TestMesh {
A,
L,
V,
_40x10,
cube_20x20x20,
cube_2x20x10,
sphere_50mm,
bridge,
bridge_with_hole,
cube_with_concave_hole,
cube_with_hole,
gt2_teeth,
ipadstand,
overhang,
pyramid,
sloping_hole,
slopy_cube,
small_dorito,
step,
two_hollow_squares
};
// Neccessary for <c++17
struct TestMeshHash {
std::size_t operator()(TestMesh tm) const {
return static_cast<std::size_t>(tm);
}
};
/// Mesh enumeration to name mapping
extern const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names;
/// Port of Slic3r::Test::mesh
/// Basic cubes/boxes should call TriangleMesh::make_cube() directly and rescale/translate it
TriangleMesh mesh(TestMesh m);
TriangleMesh mesh(TestMesh m, Vec3d translate, Vec3d scale = Vec3d(1.0, 1.0, 1.0));
TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0);
/// Templated function to see if two values are equivalent (+/- epsilon)
template <typename T>
bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; }
template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string gcode(Print& print);
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
bool contains(const std::string &data, const std::string &pattern);
bool contains_regex(const std::string &data, const std::string &pattern);
} } // namespace Slic3r::Test
#endif // SLIC3R_TEST_DATA_HPP

View File

@@ -0,0 +1,409 @@
#include <catch2/catch.hpp>
#include <cstdlib>
#include "libslic3r/ExtrusionEntityCollection.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/ShortestPath.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
static inline Slic3r::Point random_point(float LO=-50, float HI=50)
{
Vec2f pt = Vec2f(LO, LO) + (Vec2d(rand(), rand()) * (HI-LO) / RAND_MAX).cast<float>();
return pt.cast<coord_t>();
}
// build a sample extrusion entity collection with random start and end points.
static Slic3r::ExtrusionPath random_path(size_t length = 20, float LO = -50, float HI = 50)
{
ExtrusionPath t { ExtrusionRole::Perimeter, 1.0, 1.0, 1.0 };
for (size_t j = 0; j < length; ++ j)
t.polyline.append(random_point(LO, HI));
return t;
}
static Slic3r::ExtrusionPaths random_paths(size_t count = 10, size_t length = 20, float LO = -50, float HI = 50)
{
Slic3r::ExtrusionPaths p;
for (size_t i = 0; i < count; ++ i)
p.push_back(random_path(length, LO, HI));
return p;
}
SCENARIO("ExtrusionPath", "[ExtrusionEntity]") {
GIVEN("Simple path") {
Slic3r::ExtrusionPath path{ ExtrusionRole::ExternalPerimeter };
path.polyline = { { 100, 100 }, { 200, 100 }, { 200, 200 } };
path.mm3_per_mm = 1.;
THEN("first point") {
REQUIRE(path.first_point() == path.polyline.front());
}
THEN("cloned") {
auto cloned = std::unique_ptr<ExtrusionEntity>(path.clone());
REQUIRE(cloned->role() == path.role());
}
}
}
static ExtrusionPath new_extrusion_path(const Polyline &polyline, ExtrusionRole role, double mm3_per_mm)
{
ExtrusionPath path(role);
path.polyline = polyline;
path.mm3_per_mm = 1.;
return path;
}
SCENARIO("ExtrusionLoop", "[ExtrusionEntity]")
{
GIVEN("Square") {
Polygon square { { 100, 100 }, { 200, 100 }, { 200, 200 }, { 100, 200 } };
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(square.split_at_first_point(), ExtrusionRole::ExternalPerimeter, 1.));
THEN("polygon area") {
REQUIRE(loop.polygon().area() == Approx(square.area()));
}
THEN("loop length") {
REQUIRE(loop.length() == Approx(square.length()));
}
WHEN("cloned") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
THEN("cloning worked") {
REQUIRE(loop2 != nullptr);
}
THEN("loop contains one path") {
REQUIRE(loop2->paths.size() == 1);
}
THEN("cloned role") {
REQUIRE(loop2->paths.front().role() == ExtrusionRole::ExternalPerimeter);
}
}
WHEN("cloned and split") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at_vertex(square.points[2]);
THEN("splitting a single-path loop results in a single path") {
REQUIRE(loop2->paths.size() == 1);
}
THEN("path has correct number of points") {
REQUIRE(loop2->paths.front().size() == 5);
}
THEN("expected point order") {
REQUIRE(loop2->paths.front().polyline[0] == square.points[2]);
REQUIRE(loop2->paths.front().polyline[1] == square.points[3]);
REQUIRE(loop2->paths.front().polyline[2] == square.points[0]);
REQUIRE(loop2->paths.front().polyline[3] == square.points[1]);
REQUIRE(loop2->paths.front().polyline[4] == square.points[2]);
}
}
}
GIVEN("Loop with two pieces") {
Polyline polyline1 { { 100, 100 }, { 200, 100 }, { 200, 200 } };
Polyline polyline2 { { 200, 200 }, { 100, 200 }, { 100, 100 } };
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(polyline1, ExtrusionRole::ExternalPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline2, ExtrusionRole::OverhangPerimeter, 1.));
double tot_len = polyline1.length() + polyline2.length();
THEN("length") {
REQUIRE(loop.length() == Approx(tot_len));
}
WHEN("splitting at intermediate point") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at_vertex(polyline1.points[1]);
THEN("length after splitting is unchanged") {
REQUIRE(loop2->length() == Approx(tot_len));
}
THEN("loop contains three paths after splitting") {
REQUIRE(loop2->paths.size() == 3);
}
THEN("expected starting point") {
REQUIRE(loop2->paths.front().polyline.front() == polyline1.points[1]);
}
THEN("expected ending point") {
REQUIRE(loop2->paths.back().polyline.back() == polyline1.points[1]);
}
THEN("paths have common point") {
REQUIRE(loop2->paths.front().polyline.back() == loop2->paths[1].polyline.front());
REQUIRE(loop2->paths[1].polyline.back() == loop2->paths[2].polyline.front());
}
THEN("expected order after splitting") {
REQUIRE(loop2->paths.front().role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(loop2->paths[1].role() == ExtrusionRole::OverhangPerimeter);
REQUIRE(loop2->paths[2].role() == ExtrusionRole::ExternalPerimeter);
}
THEN("path has correct number of points") {
REQUIRE(loop2->paths.front().polyline.size() == 2);
REQUIRE(loop2->paths[1].polyline.size() == 3);
REQUIRE(loop2->paths[2].polyline.size() == 2);
}
THEN("clipped path has expected length") {
double l = loop2->length();
ExtrusionPaths paths;
loop2->clip_end(3, &paths);
double l2 = 0;
for (const ExtrusionPath &p : paths)
l2 += p.length();
REQUIRE(l2 == Approx(l - 3.));
}
}
WHEN("splitting at endpoint") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at_vertex(polyline2.points.front());
THEN("length after splitting is unchanged") {
REQUIRE(loop2->length() == Approx(tot_len));
}
THEN("loop contains two paths after splitting") {
REQUIRE(loop2->paths.size() == 2);
}
THEN("expected starting point") {
REQUIRE(loop2->paths.front().polyline.front() == polyline2.points.front());
}
THEN("expected ending point") {
REQUIRE(loop2->paths.back().polyline.back() == polyline2.points.front());
}
THEN("paths have common point") {
REQUIRE(loop2->paths.front().polyline.back() == loop2->paths[1].polyline.front());
REQUIRE(loop2->paths[1].polyline.back() == loop2->paths.front().polyline.front());
}
THEN("expected order after splitting") {
REQUIRE(loop2->paths.front().role() == ExtrusionRole::OverhangPerimeter);
REQUIRE(loop2->paths[1].role() == ExtrusionRole::ExternalPerimeter);
}
THEN("path has correct number of points") {
REQUIRE(loop2->paths.front().polyline.size() == 3);
REQUIRE(loop2->paths[1].polyline.size() == 3);
}
}
WHEN("splitting at an edge") {
Point point(250, 150);
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at(point, false, 0);
THEN("length after splitting is unchanged") {
REQUIRE(loop2->length() == Approx(tot_len));
}
Point expected_start_point(200, 150);
THEN("expected starting point") {
REQUIRE(loop2->paths.front().polyline.front() == expected_start_point);
}
THEN("expected ending point") {
REQUIRE(loop2->paths.back().polyline.back() == expected_start_point);
}
}
}
GIVEN("Loop with four pieces") {
Polyline polyline1 { { 59312736, 4821067 }, { 64321068, 4821067 }, { 64321068, 4821067 }, { 64321068, 9321068 }, { 59312736, 9321068 } };
Polyline polyline2 { { 59312736, 9321068 }, { 9829401, 9321068 } };
Polyline polyline3 { { 9829401, 9321068 }, { 4821067, 9321068 }, { 4821067, 4821067 }, { 9829401, 4821067 } };
Polyline polyline4 { { 9829401, 4821067 }, { 59312736,4821067 } };
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(polyline1, ExtrusionRole::ExternalPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline2, ExtrusionRole::OverhangPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline3, ExtrusionRole::ExternalPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline4, ExtrusionRole::OverhangPerimeter, 1.));
double len = loop.length();
WHEN("splitting at vertex") {
Point point(4821067, 9321068);
if (! loop.split_at_vertex(point))
loop.split_at(point, false, 0);
THEN("total length is preserved after splitting") {
REQUIRE(loop.length() == Approx(len));
}
THEN("order is correctly preserved after splitting") {
REQUIRE(loop.paths.front().role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(loop.paths[1].role() == ExtrusionRole::OverhangPerimeter);
REQUIRE(loop.paths[2].role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(loop.paths[3].role() == ExtrusionRole::OverhangPerimeter);
}
}
}
GIVEN("Some complex loop") {
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(
Polyline { { 15896783, 15868739 }, { 24842049, 12117558 }, { 33853238, 15801279 }, { 37591780, 24780128 }, { 37591780, 24844970 },
{ 33853231, 33825297 }, { 24842049, 37509013 }, { 15896798, 33757841 }, { 12211841, 24812544 }, { 15896783, 15868739 } },
ExtrusionRole::ExternalPerimeter, 1.));
double len = loop.length();
THEN("split_at() preserves total length") {
loop.split_at({ 15896783, 15868739 }, false, 0);
REQUIRE(loop.length() == Approx(len));
}
}
}
SCENARIO("ExtrusionEntityCollection: Basics", "[ExtrusionEntity]")
{
Polyline polyline { { 100, 100 }, { 200, 100 }, { 200, 200 } };
ExtrusionPath path = new_extrusion_path(polyline, ExtrusionRole::ExternalPerimeter, 1.);
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(Polygon(polyline.points).split_at_first_point(), ExtrusionRole::InternalInfill, 1.));
ExtrusionEntityCollection collection;
collection.append(path);
THEN("no_sort is false by default") {
REQUIRE(! collection.no_sort);
}
collection.append(collection);
THEN("append ExtrusionEntityCollection") {
REQUIRE(collection.entities.size() == 2);
}
collection.append(path);
THEN("append ExtrusionPath") {
REQUIRE(collection.entities.size() == 3);
}
collection.append(loop);
THEN("append ExtrusionLoop") {
REQUIRE(collection.entities.size() == 4);
}
THEN("appended collection was duplicated") {
REQUIRE(dynamic_cast<ExtrusionEntityCollection*>(collection.entities[1])->entities.size() == 1);
}
WHEN("cloned") {
auto coll2 = std::unique_ptr<ExtrusionEntityCollection>(dynamic_cast<ExtrusionEntityCollection*>(collection.clone()));
THEN("expected no_sort value") {
assert(! coll2->no_sort);
}
coll2->no_sort = true;
THEN("no_sort is kept after clone") {
auto coll3 = std::unique_ptr<ExtrusionEntityCollection>(dynamic_cast<ExtrusionEntityCollection*>(coll2->clone()));
assert(coll3->no_sort);
}
}
}
SCENARIO("ExtrusionEntityCollection: Polygon flattening", "[ExtrusionEntity]")
{
srand(0xDEADBEEF); // consistent seed for test reproducibility.
// Generate one specific random path set and save it for later comparison
Slic3r::ExtrusionPaths nosort_path_set = random_paths();
Slic3r::ExtrusionEntityCollection sub_nosort;
sub_nosort.append(nosort_path_set);
sub_nosort.no_sort = true;
Slic3r::ExtrusionEntityCollection sub_sort;
sub_sort.no_sort = false;
sub_sort.append(random_paths());
GIVEN("A Extrusion Entity Collection with a child that has one child that is marked as no-sort") {
Slic3r::ExtrusionEntityCollection sample;
Slic3r::ExtrusionEntityCollection output;
sample.append(sub_sort);
sample.append(sub_nosort);
sample.append(sub_sort);
WHEN("The EEC is flattened with default options (preserve_order=false)") {
output = sample.flatten();
THEN("The output EEC contains no Extrusion Entity Collections") {
CHECK(std::count_if(output.entities.cbegin(), output.entities.cend(), [=](const ExtrusionEntity* e) {return e->is_collection();}) == 0);
}
}
WHEN("The EEC is flattened with preservation (preserve_order=true)") {
output = sample.flatten(true);
THEN("The output EECs contains one EEC.") {
CHECK(std::count_if(output.entities.cbegin(), output.entities.cend(), [=](const ExtrusionEntity* e) {return e->is_collection();}) == 1);
}
AND_THEN("The ordered EEC contains the same order of elements than the original") {
// find the entity in the collection
for (auto e : output.entities)
if (e->is_collection()) {
ExtrusionEntityCollection *temp = dynamic_cast<ExtrusionEntityCollection*>(e);
// check each Extrusion path against nosort_path_set to see if the first and last match the same
CHECK(nosort_path_set.size() == temp->entities.size());
for (size_t i = 0; i < nosort_path_set.size(); ++ i) {
CHECK(temp->entities[i]->first_point() == nosort_path_set[i].first_point());
CHECK(temp->entities[i]->last_point() == nosort_path_set[i].last_point());
}
}
}
}
}
}
TEST_CASE("ExtrusionEntityCollection: Chained path", "[ExtrusionEntity]") {
struct Test {
Polylines unchained;
Polylines chained;
Point initial_point;
};
std::vector<Test> tests {
{
{
{ {0,15}, {0,18}, {0,20} },
{ {0,10}, {0,8}, {0,5} }
},
{
{ {0,20}, {0,18}, {0,15} },
{ {0,10}, {0,8}, {0,5} }
},
{ 0, 30 }
},
{
{
{ {4,0}, {10,0}, {15,0} },
{ {10,5}, {15,5}, {20,5} }
},
{
{ {20,5}, {15,5}, {10,5} },
{ {15,0}, {10,0}, {4,0} }
},
{ 30, 0 }
},
{
{
{ {15,0}, {10,0}, {4,0} },
{ {10,5}, {15,5}, {20,5} }
},
{
{ {20,5}, {15,5}, {10,5} },
{ {15,0}, {10,0}, {4,0} }
},
{ 30, 0 }
},
};
for (const Test &test : tests) {
Polylines chained = chain_polylines(test.unchained, &test.initial_point);
REQUIRE(chained == test.chained);
ExtrusionEntityCollection unchained_extrusions;
extrusion_entities_append_paths(unchained_extrusions.entities, test.unchained,
ExtrusionRole::InternalInfill, 0., 0.4f, 0.3f);
THEN("Chaining works") {
ExtrusionEntityCollection chained_extrusions = unchained_extrusions.chained_path_from(test.initial_point);
REQUIRE(chained_extrusions.entities.size() == test.chained.size());
for (size_t i = 0; i < chained_extrusions.entities.size(); ++ i) {
const Points &p1 = test.chained[i].points;
const Points &p2 = dynamic_cast<const ExtrusionPath*>(chained_extrusions.entities[i])->polyline.points;
REQUIRE(p1 == p2);
}
}
THEN("Chaining produces no change with no_sort") {
unchained_extrusions.no_sort = true;
ExtrusionEntityCollection chained_extrusions = unchained_extrusions.chained_path_from(test.initial_point);
REQUIRE(chained_extrusions.entities.size() == test.unchained.size());
for (size_t i = 0; i < chained_extrusions.entities.size(); ++ i) {
const Points &p1 = test.unchained[i].points;
const Points &p2 = dynamic_cast<const ExtrusionPath*>(chained_extrusions.entities[i])->polyline.points;
REQUIRE(p1 == p2);
}
}
}
}
TEST_CASE("ExtrusionEntityCollection: Chained path with no explicit starting point", "[ExtrusionEntity]") {
auto polylines = Polylines { { { 0, 15 }, {0, 18}, {0, 20} }, { { 0, 10 }, {0, 8}, {0, 5} } };
auto target = Polylines { { {0, 5}, {0, 8}, { 0, 10 } }, { { 0, 15 }, {0, 18}, {0, 20} } };
auto chained = chain_polylines(polylines);
REQUIRE(chained == target);
}

View File

@@ -0,0 +1,715 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "libslic3r/libslic3r.h"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/SVG.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace std::literals;
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
#if 0
TEST_CASE("Fill: adjusted solid distance") {
int surface_width = 250;
int distance = Slic3r::Flow::solid_spacing(surface_width, 47);
REQUIRE(distance == Approx(50));
REQUIRE(surface_width % distance == 0);
}
#endif
TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
filler->angle = float(-(PI)/2.0);
FillParams fill_params;
filler->spacing = 5;
fill_params.dont_adjust = true;
//fill_params.endpoints_overlap = false;
fill_params.density = float(filler->spacing / 50.0);
auto test = [&filler, &fill_params] (const ExPolygon& poly) -> Slic3r::Polylines {
Slic3r::Surface surface(stTop, poly);
return filler->fill_surface(&surface, fill_params);
};
SECTION("Square") {
Slic3r::Points test_set;
test_set.reserve(4);
std::vector<Vec2d> points { {0,0}, {100,0}, {100,100}, {0,100} };
for (size_t i = 0; i < 4; ++i) {
std::transform(points.cbegin()+i, points.cend(), std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
std::transform(points.cbegin(), points.cbegin()+i, std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
Slic3r::Polylines paths = test(Slic3r::ExPolygon(test_set));
REQUIRE(paths.size() == 1); // one continuous path
// TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon.
// This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon.
// ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length';
REQUIRE(std::abs(paths[0].length() - static_cast<double>(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length
test_set.clear();
}
}
SECTION("Diamond with endpoints on grid") {
std::vector<Vec2d> points {Vec2d(0,0), Vec2d(100,0), Vec2d(150,50), Vec2d(100,100), Vec2d(0,100), Vec2d(-50,50)};
Slic3r::Points test_set;
test_set.reserve(6);
std::transform(points.cbegin(), points.cend(), std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
Slic3r::Polylines paths = test(Slic3r::ExPolygon(test_set));
REQUIRE(paths.size() == 1); // one continuous path
}
SECTION("Square with hole") {
std::vector<Vec2d> square {Vec2d(0,0), Vec2d(100,0), Vec2d(100,100), Vec2d(0,100)};
std::vector<Vec2d> hole {Vec2d(25,25), Vec2d(75,25), Vec2d(75,75), Vec2d(25,75) };
std::reverse(hole.begin(), hole.end());
Slic3r::Points test_hole;
Slic3r::Points test_square;
std::transform(square.cbegin(), square.cend(), std::back_inserter(test_square), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
std::transform(hole.cbegin(), hole.cend(), std::back_inserter(test_hole), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
for (double angle : {-(PI/2.0), -(PI/4.0), -(PI), PI/2.0, PI}) {
for (double spacing : {25.0, 5.0, 7.5, 8.5}) {
fill_params.density = float(filler->spacing / spacing);
filler->angle = float(angle);
ExPolygon e(test_square, test_hole);
Slic3r::Polylines paths = test(e);
#if 0
{
BoundingBox bbox = get_extents(e);
SVG svg("c:\\data\\temp\\square_with_holes.svg", bbox);
svg.draw(e);
svg.draw(paths);
svg.Close();
}
#endif
REQUIRE((paths.size() >= 1 && paths.size() <= 3));
// paths don't cross hole
REQUIRE(diff_pl(paths, offset(e, float(SCALED_EPSILON*10))).size() == 0);
}
}
}
SECTION("Regression: Missing infill segments in some rare circumstances") {
filler->angle = float(PI/4.0);
fill_params.dont_adjust = false;
filler->spacing = 0.654498;
//filler->endpoints_overlap = unscale(359974);
fill_params.density = 1;
filler->layer_id = 66;
filler->z = 20.15;
Slic3r::Points points {Point(25771516,14142125),Point(14142138,25771515),Point(2512749,14142131),Point(14142125,2512749)};
Slic3r::Polylines paths = test(Slic3r::ExPolygon(points));
REQUIRE(paths.size() == 1); // one continuous path
// TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon.
// This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon.
// ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length';
REQUIRE(std::abs(paths[0].length() - static_cast<double>(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length
}
SECTION("Rotated Square produces one continuous path") {
Slic3r::ExPolygon expolygon(Polygon::new_scale({ {0, 0}, {50, 0}, {50, 50}, {0, 50} }));
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
filler->bounding_box = get_extents(expolygon);
filler->angle = 0;
Surface surface(stTop, expolygon);
// width, height, nozzle_dmr
auto flow = Slic3r::Flow(0.69f, 0.4f, 0.5f);
FillParams fill_params;
for (auto density : { 0.4, 1.0 }) {
fill_params.density = density;
filler->spacing = flow.spacing();
REQUIRE(!fill_params.use_arachne); // Make this test fail when Arachne is used because this test is not ready for it.
for (auto angle : { 0.0, 45.0}) {
surface.expolygon.rotate(angle, Point(0,0));
Polylines paths = filler->fill_surface(&surface, fill_params);
// one continuous path
REQUIRE(paths.size() == 1);
}
}
}
#if 0 // Disabled temporarily due to precission issues on the Mac VM
SECTION("Solid surface fill") {
Slic3r::Points points {
Point::new_scale(6883102, 9598327.01296997),
Point::new_scale(6883102, 20327272.01297),
Point::new_scale(3116896, 20327272.01297),
Point::new_scale(3116896, 9598327.01296997)
};
Slic3r::ExPolygon expolygon(points);
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
for (size_t i = 0; i <= 20; ++i)
{
expolygon.scale(1.05);
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
}
}
#endif
SECTION("Solid surface fill") {
Slic3r::Points points {
Slic3r::Point(59515297,5422499),Slic3r::Point(59531249,5578697),Slic3r::Point(59695801,6123186),
Slic3r::Point(59965713,6630228),Slic3r::Point(60328214,7070685),Slic3r::Point(60773285,7434379),
Slic3r::Point(61274561,7702115),Slic3r::Point(61819378,7866770),Slic3r::Point(62390306,7924789),
Slic3r::Point(62958700,7866744),Slic3r::Point(63503012,7702244),Slic3r::Point(64007365,7434357),
Slic3r::Point(64449960,7070398),Slic3r::Point(64809327,6634999),Slic3r::Point(65082143,6123325),
Slic3r::Point(65245005,5584454),Slic3r::Point(65266967,5422499),Slic3r::Point(66267307,5422499),
Slic3r::Point(66269190,8310081),Slic3r::Point(66275379,17810072),Slic3r::Point(66277259,20697500),
Slic3r::Point(65267237,20697500),Slic3r::Point(65245004,20533538),Slic3r::Point(65082082,19994444),
Slic3r::Point(64811462,19488579),Slic3r::Point(64450624,19048208),Slic3r::Point(64012101,18686514),
Slic3r::Point(63503122,18415781),Slic3r::Point(62959151,18251378),Slic3r::Point(62453416,18198442),
Slic3r::Point(62390147,18197355),Slic3r::Point(62200087,18200576),Slic3r::Point(61813519,18252990),
Slic3r::Point(61274433,18415918),Slic3r::Point(60768598,18686517),Slic3r::Point(60327567,19047892),
Slic3r::Point(59963609,19493297),Slic3r::Point(59695865,19994587),Slic3r::Point(59531222,20539379),
Slic3r::Point(59515153,20697500),Slic3r::Point(58502480,20697500),Slic3r::Point(58502480,5422499)
};
Slic3r::ExPolygon expolygon(points);
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55, PI/2.0) == true);
}
SECTION("Solid surface fill") {
Slic3r::Points points {
Point::new_scale(0,0),Point::new_scale(98,0),Point::new_scale(98,10), Point::new_scale(0,10)
};
Slic3r::ExPolygon expolygon(points);
REQUIRE(test_if_solid_surface_filled(expolygon, 0.5, 45.0, 0.99) == true);
}
}
SCENARIO("Infill does not exceed perimeters", "[Fill]")
{
auto test = [](const std::string_view pattern) {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.4, 0.4, 0.4, 0.4" },
{ "fill_pattern", pattern },
{ "top_fill_pattern", pattern },
{ "bottom_fill_pattern", pattern },
{ "perimeters", 1 },
{ "skirts", 0 },
{ "fill_density", 0.2 },
{ "layer_height", 0.05 },
{ "perimeter_extruder", 1 },
{ "infill_extruder", 2 }
});
WHEN("40mm cube sliced") {
std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::cube_20x20x20, Vec3d::Zero(), 2.0) }, config);
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
THEN("infill does not exceed perimeters") {
GCodeReader parser;
const int perimeter_extruder = config.opt_int("perimeter_extruder");
const int infill_extruder = config.opt_int("infill_extruder");
int tool = -1;
Points perimeter_points;
Points infill_points;
parser.parse_buffer(gcode, [&tool, &perimeter_points, &infill_points, perimeter_extruder, infill_extruder]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1) + 1;
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (tool == perimeter_extruder)
perimeter_points.emplace_back(line.new_XY_scaled(self));
else if (tool == infill_extruder)
infill_points.emplace_back(line.new_XY_scaled(self));
}
});
auto convex_hull = Geometry::convex_hull(perimeter_points);
int num_inside = std::count_if(infill_points.begin(), infill_points.end(), [&convex_hull](const Point &pt){ return convex_hull.contains(pt); });
REQUIRE(num_inside == infill_points.size());
}
}
};
GIVEN("Rectilinear") { test("rectilinear"sv); }
GIVEN("Honeycomb") { test("honeycomb"sv); }
GIVEN("HilbertCurve") { test("hilbertcurve"sv); }
GIVEN("Concentric") { test("concentric"sv); }
}
// SCENARIO("Infill only where needed", "[Fill]")
// {
// DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
// config.set_deserialize_strict({
// { "nozzle_diameter", "0.4, 0.4, 0.4, 0.4" },
// { "infill_only_where_needed", true },
// { "bottom_solid_layers", 0 },
// { "infill_extruder", 2 },
// { "infill_extrusion_width", 0.5 },
// { "wipe_into_infill", false },
// { "fill_density", 0.4 },
// // for preventing speeds from being altered
// { "cooling", "0, 0, 0, 0" },
// // for preventing speeds from being altered
// { "first_layer_speed", "100%" }
// });
// auto test = [&config]() -> double {
// TriangleMesh pyramid = Test::mesh(Slic3r::Test::TestMesh::pyramid);
// // Arachne doesn't use "Detect thin walls," and because of this, it filters out tiny infill areas differently.
// // So, for Arachne, we cut the pyramid model to achieve similar results.
// if (config.opt_enum<PerimeterGeneratorType>("perimeter_generator") == Slic3r::PerimeterGeneratorType::Arachne) {
// indexed_triangle_set lower{};
// cut_mesh(pyramid.its, 35, nullptr, &lower);
// pyramid = TriangleMesh(lower);
// }
// std::string gcode = Slic3r::Test::slice({ pyramid }, config);
// THEN("gcode not empty") {
// REQUIRE(! gcode.empty());
// }
// GCodeReader parser;
// int tool = -1;
// const int infill_extruder = config.opt_int("infill_extruder");
// Points infill_points;
// parser.parse_buffer(gcode, [&tool, &infill_points, infill_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
// {
// // if the command is a T command, set the the current tool
// if (boost::starts_with(line.cmd(), "T")) {
// tool = atoi(line.cmd().data() + 1) + 1;
// } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
// if (tool == infill_extruder) {
// infill_points.emplace_back(self.xy_scaled());
// infill_points.emplace_back(line.new_XY_scaled(self));
// }
// }
// });
// // prevent calling convex_hull() with no points
// THEN("infill not empty") {
// REQUIRE(! infill_points.empty());
// }
// auto opt_width = config.opt<ConfigOptionFloatOrPercent>("infill_extrusion_width");
// REQUIRE(! opt_width->percent);
// Polygons convex_hull = expand(Geometry::convex_hull(infill_points), scaled<float>(opt_width->value / 2));
// return SCALING_FACTOR * SCALING_FACTOR * std::accumulate(convex_hull.begin(), convex_hull.end(), 0., [](double acc, const Polygon &poly){ return acc + poly.area(); });
// };
// double tolerance = 5; // mm^2
// // GIVEN("solid_infill_below_area == 0") {
// // config.opt_float("solid_infill_below_area") = 0;
// // WHEN("pyramid is sliced ") {
// // auto area = test();
// // THEN("no infill is generated when using infill_only_where_needed on a pyramid") {
// // REQUIRE(area < tolerance);
// // }
// // }
// // }
// // GIVEN("solid_infill_below_area == 70") {
// // config.opt_float("solid_infill_below_area") = 70;
// // WHEN("pyramid is sliced ") {
// // auto area = test();
// // THEN("infill is only generated under the forced solid shells") {
// // REQUIRE(std::abs(area - 70) < tolerance);
// // }
// // }
// // }
// }
SCENARIO("Combine infill", "[Fill]")
{
{
auto test = [](const DynamicPrintConfig &config) {
std::string gcode = Test::slice({ Test::TestMesh::cube_20x20x20 }, config);
THEN("infill_every_layers does not crash") {
REQUIRE(! gcode.empty());
}
Slic3r::GCodeReader parser;
int tool = -1;
std::set<coord_t> layers; // layer_z => 1
std::map<coord_t, bool> layer_infill; // layer_z => has_infill
const int infill_extruder = config.opt_int("infill_extruder");
const int support_material_extruder = config.opt_int("support_material_extruder");
parser.parse_buffer(gcode,
[&tool, &layers, &layer_infill, infill_extruder, support_material_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
coord_t z = line.new_Z(self) / SCALING_FACTOR;
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0 && tool + 1 != support_material_extruder) {
if (tool + 1 == infill_extruder)
layer_infill[z] = true;
else if (auto it = layer_infill.find(z); it == layer_infill.end())
layer_infill.insert(it, std::make_pair(z, false));
}
// Previously, all G-code commands had a fixed number of decimal points with means with redundant zeros after decimal points.
// We changed this behavior and got rid of these redundant padding zeros, which caused this test to fail
// because the position in Z-axis is compared as a string, and previously, G-code contained the following two commands:
// "G1 Z5 F5000 ; lift nozzle"
// "G1 Z5.000 F7800.000"
// That has a different Z-axis position from the view of string comparisons of floating-point numbers.
// To correct the computation of the number of printed layers, even in the case of string comparisons of floating-point numbers,
// we filtered out the G-code command with the commend 'lift nozzle'.
if (line.cmd_is("G1") && line.dist_Z(self) != 0 && line.comment().find("lift nozzle") == std::string::npos)
layers.insert(z);
});
auto layers_with_perimeters = int(layer_infill.size());
auto layers_with_infill = int(std::count_if(layer_infill.begin(), layer_infill.end(), [](auto &v){ return v.second; }));
THEN("expected number of layers") {
REQUIRE(layers.size() == layers_with_perimeters + config.opt_int("raft_layers"));
}
if (config.opt_int("raft_layers") == 0) {
// first infill layer printed directly on print bed is not combined, so we don't consider it.
-- layers_with_infill;
-- layers_with_perimeters;
}
// we expect that infill is generated for half the number of combined layers
// plus for each single layer that was not combined (remainder)
THEN("infill is only present in correct number of layers") {
int infill_every = config.opt_int("infill_every_layers");
REQUIRE(layers_with_infill == int(layers_with_perimeters / infill_every) + (layers_with_perimeters % infill_every));
}
};
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.5, 0.5, 0.5, 0.5" },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "infill_every_layers", 2 },
{ "perimeter_extruder", 1 },
{ "infill_extruder", 2 },
{ "wipe_into_infill", false },
{ "support_material_extruder", 3 },
{ "support_material_interface_extruder", 3 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 }
});
test(config);
// Reuse the config above
config.set_deserialize_strict({
{ "skirts", 0 }, // prevent usage of perimeter_extruder in raft layers
{ "raft_layers", 5 }
});
test(config);
}
WHEN("infill_every_layers == 2") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ Test::TestMesh::cube_20x20x20 }, print, {
{ "nozzle_diameter", "0.5" },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "infill_every_layers", 2 }
});
THEN("infill combination produces internal void surfaces") {
bool has_void = false;
for (const Layer *layer : print.get_object(0)->layers())
if (layer->get_region(0)->fill_surfaces().filter_by_type(stInternalVoid).size() > 0) {
has_void = true;
break;
}
REQUIRE(has_void);
}
}
WHEN("infill_every_layers disabled") {
// we disable combination after infill has been generated
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ Test::TestMesh::cube_20x20x20 }, print, {
{ "nozzle_diameter", "0.5" },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "infill_every_layers", 1 }
});
THEN("infill combination is idempotent") {
bool has_infill_on_each_layer = true;
for (const Layer *layer : print.get_object(0)->layers())
if (layer->get_region(0)->fill_surfaces().empty()) {
has_infill_on_each_layer = false;
break;
}
REQUIRE(has_infill_on_each_layer);
}
}
}
SCENARIO("Infill density zero", "[Fill]")
{
WHEN("20mm cube is sliced") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirts", 0 },
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
{ "solid_infill_below_area", 20000000 },
{ "solid_infill_every_layers", 2 },
{ "perimeter_speed", 99 },
{ "external_perimeter_speed", 99 },
{ "cooling", "0" },
{ "first_layer_speed", "100%" }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
THEN("solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0") {
GCodeReader parser;
const double perimeter_speed = config.opt_float("perimeter_speed");
std::map<double, double> layers_with_extrusion;
parser.parse_buffer(gcode, [&layers_with_extrusion, perimeter_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
double f = line.new_F(self);
if (std::abs(f - perimeter_speed * 60.) > 0.01)
// It is a perimeter.
layers_with_extrusion[self.z()] = f;
}
});
REQUIRE(layers_with_extrusion.empty());
}
}
WHEN("A is sliced") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirts", 0 },
{ "perimeters", 3 },
{ "fill_density", 0 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "nozzle_diameter", "0.35,0.35,0.35,0.35" },
{ "infill_extruder", 2 },
{ "solid_infill_extruder", 2 },
{ "infill_extrusion_width", 0.52 },
{ "solid_infill_extrusion_width", 0.52 },
{ "first_layer_extrusion_width", 0 }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::A }, config);
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
THEN("no missing parts in solid shell when fill_density is 0") {
GCodeReader parser;
int tool = -1;
const int infill_extruder = config.opt_int("infill_extruder");
std::map<coord_t, Lines> infill;
parser.parse_buffer(gcode, [&tool, &infill, infill_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1) + 1;
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (tool == infill_extruder)
infill[scaled<coord_t>(self.z())].emplace_back(self.xy_scaled(), line.new_XY_scaled(self));
}
});
auto opt_width = config.opt<ConfigOptionFloatOrPercent>("infill_extrusion_width");
REQUIRE(! opt_width->percent);
auto grow_d = scaled<float>(opt_width->value / 2);
auto inflate_lines = [grow_d](const Lines &lines) {
Polygons out;
for (const Line &line : lines)
append(out, offset(Polyline{ line.a, line.b }, grow_d, Slic3r::ClipperLib::jtSquare, 3.));
return union_(out);
};
Polygons layer0_infill = inflate_lines(infill[scaled<coord_t>(0.2)]);
Polygons layer1_infill = inflate_lines(infill[scaled<coord_t>(0.4)]);
ExPolygons poly = opening_ex(diff_ex(layer0_infill, layer1_infill), grow_d);
const double threshold = 2. * sqr(grow_d * 2.);
int missing_parts = std::count_if(poly.begin(), poly.end(), [threshold](const ExPolygon &poly){ return poly.area() > threshold; });
REQUIRE(missing_parts == 0);
}
}
}
/*
{
# GH: #2697
my $config = Slic3r::Config->new_from_defaults;
$config->set('perimeter_extrusion_width', 0.72);
$config->set('top_infill_extrusion_width', 0.1);
$config->set('infill_extruder', 2); # in order to distinguish infill
$config->set('solid_infill_extruder', 2); # in order to distinguish infill
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my %infill = (); # Z => [ Line, Line ... ]
my %other = (); # Z => [ Line, Line ... ]
my $tool = undef;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
my $z = 1 * $self->Z;
my $line = Slic3r::Line->new_scale(
[ $self->X, $self->Y ],
[ $info->{new_X}, $info->{new_Y} ],
);
if ($tool == $config->infill_extruder-1) {
$infill{$z} //= [];
push @{$infill{$z}}, $line;
} else {
$other{$z} //= [];
push @{$other{$z}}, $line;
}
}
});
my $top_z = max(keys %infill);
my $top_infill_grow_d = scale($config->top_infill_extrusion_width)/2;
my $top_infill = union([ map @{$_->grow($top_infill_grow_d)}, @{ $infill{$top_z} } ]);
my $perimeters_grow_d = scale($config->perimeter_extrusion_width)/2;
my $perimeters = union([ map @{$_->grow($perimeters_grow_d)}, @{ $other{$top_z} } ]);
my $covered = union_ex([ @$top_infill, @$perimeters ]);
my @holes = map @{$_->holes}, @$covered;
ok sum(map unscale unscale $_->area*-1, @holes) < 1, 'no gaps between top solid infill and perimeters';
}
{
skip "The FillRectilinear2 does not fill the surface completely", 1;
my $test = sub {
my ($expolygon, $flow_spacing, $angle, $density) = @_;
my $filler = Slic3r::Filler->new_from_type('rectilinear');
$filler->set_bounding_box($expolygon->bounding_box);
$filler->set_angle($angle // 0);
# Adjust line spacing to fill the region.
$filler->set_dont_adjust(0);
$filler->set_link_max_length(scale(1.2*$flow_spacing));
my $surface = Slic3r::Surface->new(
surface_type => S_TYPE_BOTTOM,
expolygon => $expolygon,
);
my $flow = Slic3r::Flow->new(
width => $flow_spacing,
height => 0.4,
nozzle_diameter => $flow_spacing,
);
$filler->set_spacing($flow->spacing);
my $paths = $filler->fill_surface(
$surface,
layer_height => $flow->height,
density => $density // 1,
);
# check whether any part was left uncovered
my @grown_paths = map @{Slic3r::Polyline->new(@$_)->grow(scale $filler->spacing/2)}, @$paths;
my $uncovered = diff_ex([ @$expolygon ], [ @grown_paths ], 1);
# ignore very small dots
my $uncovered_filtered = [ grep $_->area > (scale $flow_spacing)**2, @$uncovered ];
is scalar(@$uncovered_filtered), 0, 'solid surface is fully filled';
if (0 && @$uncovered_filtered) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output("uncovered.svg",
no_arrows => 1,
expolygons => [ $expolygon ],
blue_expolygons => [ @$uncovered ],
red_expolygons => [ @$uncovered_filtered ],
polylines => [ @$paths ],
);
exit;
}
};
my $expolygon = Slic3r::ExPolygon->new([
[6883102, 9598327.01296997],
[6883102, 20327272.01297],
[3116896, 20327272.01297],
[3116896, 9598327.01296997],
]);
$test->($expolygon, 0.55);
for (1..20) {
$expolygon->scale(1.05);
$test->($expolygon, 0.55);
}
$expolygon = Slic3r::ExPolygon->new(
[[59515297,5422499],[59531249,5578697],[59695801,6123186],[59965713,6630228],[60328214,7070685],[60773285,7434379],[61274561,7702115],[61819378,7866770],[62390306,7924789],[62958700,7866744],[63503012,7702244],[64007365,7434357],[64449960,7070398],[64809327,6634999],[65082143,6123325],[65245005,5584454],[65266967,5422499],[66267307,5422499],[66269190,8310081],[66275379,17810072],[66277259,20697500],[65267237,20697500],[65245004,20533538],[65082082,19994444],[64811462,19488579],[64450624,19048208],[64012101,18686514],[63503122,18415781],[62959151,18251378],[62453416,18198442],[62390147,18197355],[62200087,18200576],[61813519,18252990],[61274433,18415918],[60768598,18686517],[60327567,19047892],[59963609,19493297],[59695865,19994587],[59531222,20539379],[59515153,20697500],[58502480,20697500],[58502480,5422499]]
);
$test->($expolygon, 0.524341649025257);
$expolygon = Slic3r::ExPolygon->new([ scale_points [0,0], [98,0], [98,10], [0,10] ]);
$test->($expolygon, 0.5, 45, 0.99); # non-solid infill
}
*/
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle, double density)
{
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
filler->bounding_box = get_extents(expolygon.contour);
filler->angle = float(angle);
Flow flow(float(flow_spacing), 0.4f, float(flow_spacing));
filler->spacing = flow.spacing();
FillParams fill_params;
fill_params.density = float(density);
fill_params.dont_adjust = false;
Surface surface(stBottom, expolygon);
if (fill_params.use_arachne) // Make this test fail when Arachne is used because this test is not ready for it.
return false;
Slic3r::Polylines paths = filler->fill_surface(&surface, fill_params);
// check whether any part was left uncovered
Polygons grown_paths;
grown_paths.reserve(paths.size());
// figure out what is actually going on here re: data types
float line_offset = float(scale_(filler->spacing / 2.0 + EPSILON));
std::for_each(paths.begin(), paths.end(), [line_offset, &grown_paths] (const Slic3r::Polyline& p) {
polygons_append(grown_paths, offset(p, line_offset));
});
// Shrink the initial expolygon a bit, this simulates the infill / perimeter overlap that we usually apply.
ExPolygons uncovered = diff_ex(offset(expolygon, - float(0.2 * scale_(flow_spacing))), grown_paths, ApplySafetyOffset::Yes);
// ignore very small dots
const double scaled_flow_spacing = std::pow(scale_(flow_spacing), 2);
uncovered.erase(std::remove_if(uncovered.begin(), uncovered.end(), [scaled_flow_spacing](const ExPolygon& poly) { return poly.area() < scaled_flow_spacing; }), uncovered.end());
#if 0
if (! uncovered.empty()) {
BoundingBox bbox = get_extents(expolygon.contour);
bbox.merge(get_extents(uncovered));
bbox.merge(get_extents(grown_paths));
SVG svg("c:\\data\\temp\\test_if_solid_surface_filled.svg", bbox);
svg.draw(expolygon);
svg.draw(uncovered, "red");
svg.Close();
}
#endif
return uncovered.empty(); // solid surface is fully filled
}

View File

@@ -0,0 +1,226 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "test_data.hpp" // get access to init_print, etc
#include "libslic3r/Config.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Extrusion width specifics", "[Flow]") {
auto test = [](const DynamicPrintConfig &config) {
Slic3r::GCodeReader parser;
const double layer_height = config.opt_float("layer_height");
std::vector<double> E_per_mm_bottom;
parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
[&E_per_mm_bottom, layer_height] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (self.z() == Approx(layer_height).margin(0.01)) { // only consider first layer
if (line.extruding(self) && line.dist_XY(self) > 0)
E_per_mm_bottom.emplace_back(line.dist_E(self) / line.dist_XY(self));
}
});
THEN("First layer width applies to everything on first layer.") {
REQUIRE(E_per_mm_bottom.size() > 0);
const double E_per_mm_avg = std::accumulate(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), 0.0) / static_cast<double>(E_per_mm_bottom.size());
bool pass = (std::count_if(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), [E_per_mm_avg] (const double& v) { return v == Approx(E_per_mm_avg); }) == 0);
REQUIRE(pass);
}
THEN("First layer width does not apply to upper layer.") {
}
};
GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 1 },
{ "brim_width", 2 },
{ "perimeters", 3 },
{ "fill_density", "40%" },
{ "first_layer_height", 0.3 },
{ "first_layer_extrusion_width", "2" },
});
WHEN("Slicing a 20mm cube") {
test(config);
}
}
GIVEN("A config with more options and a 20mm cube ") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 1 },
{ "brim_width", 2 },
{ "perimeters", 3 },
{ "fill_density", "40%" },
{ "layer_height", "0.35" },
{ "first_layer_height", "0.35" },
{ "bottom_solid_layers", 1 },
{ "first_layer_extrusion_width", "2" },
{ "filament_diameter", "3" },
{ "nozzle_diameter", "0.5" }
});
WHEN("Slicing a 20mm cube") {
test(config);
}
}
}
SCENARIO(" Bridge flow specifics.", "[Flow]") {
auto config = DynamicPrintConfig::full_print_config_with({
{ "bridge_speed", 99 },
{ "bridge_flow_ratio", 1 },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" }
});
auto test = [](const DynamicPrintConfig &config) {
GCodeReader parser;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
std::vector<double> E_per_mm;
parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config),
[&E_per_mm, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.extruding(self) && line.dist_XY(self) > 0) {
if (is_approx<double>(line.new_F(self), bridge_speed))
E_per_mm.emplace_back(line.dist_E(self) / line.dist_XY(self));
}
});
const double nozzle_dmr = config.opt<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
const double filament_dmr = config.opt<ConfigOptionFloats>("filament_diameter")->get_at(0);
const double bridge_mm_per_mm = sqr(nozzle_dmr / filament_dmr) * config.opt_float("bridge_flow_ratio");
size_t num_errors = std::count_if(E_per_mm.begin(), E_per_mm.end(),
[bridge_mm_per_mm](double v){ return std::abs(v - bridge_mm_per_mm) > 0.01; });
return num_errors == 0;
};
GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio and an overhang mesh.") {
WHEN("bridge_flow_ratio is set to 0.5 and extrusion width to default") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 0.5}, { "extrusion_width", "0" } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 2.0 and extrusion width to default") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 2.0}, { "extrusion_width", "0" } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 0.5 and extrusion_width to 0.4") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 0.5}, { "extrusion_width", 0.4 } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 1.0 and extrusion_width to 0.4") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 1.0}, { "extrusion_width", 0.4 } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 2 and extrusion_width to 0.4") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 2.}, { "extrusion_width", 0.4 } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
}
GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio, fixed extrusion width of 0.4mm and an overhang mesh.") {
WHEN("bridge_flow_ratio is set to 1.0") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 0.5") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 2.0") {
THEN("Output flow is as expected.") {
}
}
}
}
/// Test the expected behavior for auto-width,
/// spacing, etc
SCENARIO("Flow: Flow math for non-bridges", "[Flow]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
ConfigOptionFloatOrPercent width(1.0, false);
float nozzle_diameter = 0.4f;
float layer_height = 0.4f;
// Spacing for non-bridges is has some overlap
THEN("External perimeter flow has spacing fixed to 1.125 * nozzle_diameter") {
auto flow = Flow::new_from_config_width(frExternalPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(1.125 * nozzle_diameter - layer_height * (1.0 - PI / 4.0)));
}
THEN("Internal perimeter flow has spacing fixed to 1.125 * nozzle_diameter") {
auto flow = Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(1.125 *nozzle_diameter - layer_height * (1.0 - PI / 4.0)));
}
THEN("Spacing for supplied width is 0.8927f") {
auto flow = Flow::new_from_config_width(frExternalPerimeter, width, nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(width.value - layer_height * (1.0 - PI / 4.0)));
flow = Flow::new_from_config_width(frPerimeter, width, nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(width.value - layer_height * (1.0 - PI / 4.0)));
}
}
/// Check the min/max
GIVEN("Nozzle Diameter of 0.25") {
float nozzle_diameter = 0.25f;
float layer_height = 0.5f;
WHEN("layer height is set to 0.2") {
layer_height = 0.15f;
THEN("Max width is set.") {
auto flow = Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.width() == Approx(1.125 * nozzle_diameter));
}
}
WHEN("Layer height is set to 0.25") {
layer_height = 0.25f;
THEN("Min width is set.") {
auto flow = Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.width() == Approx(1.125 * nozzle_diameter));
}
}
}
#if 0
/// Check for an edge case in the maths where the spacing could be 0; original
/// math is 0.99. Slic3r issue #4654
GIVEN("Input spacing of 0.414159 and a total width of 2") {
double in_spacing = 0.414159;
double total_width = 2.0;
auto flow = Flow::new_from_spacing(1.0, 0.4, 0.3);
WHEN("solid_spacing() is called") {
double result = flow.solid_spacing(total_width, in_spacing);
THEN("Yielded spacing is greater than 0") {
REQUIRE(result > 0);
}
}
}
#endif
}
/// Spacing, width calculation for bridge extrusions
SCENARIO("Flow: Flow math for bridges", "[Flow]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
float nozzle_diameter = 0.4f;
float bridge_flow = 1.0f;
WHEN("Flow role is frExternalPerimeter") {
auto flow = Flow::bridging_flow(nozzle_diameter * sqrt(bridge_flow), nozzle_diameter);
THEN("Bridge width is same as nozzle diameter") {
REQUIRE(flow.width() == Approx(nozzle_diameter));
}
THEN("Bridge spacing is same as nozzle diameter + BRIDGE_EXTRA_SPACING") {
REQUIRE(flow.spacing() == Approx(nozzle_diameter + BRIDGE_EXTRA_SPACING));
}
}
}
}

View File

@@ -0,0 +1,60 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Gaps", "[Gaps]") {
GIVEN("Two hollow squares") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeter_speed", 66 },
{ "external_perimeter_speed", 66 },
{ "small_perimeter_speed", 66 },
{ "gap_fill_speed", 99 },
{ "perimeters", 1 },
// to prevent speeds from being altered
{ "cooling", 0 },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" },
{ "perimeter_extrusion_width", 0.35 },
{ "first_layer_extrusion_width", 0.35 }
});
GCodeReader parser;
const double perimeter_speed = config.opt_float("perimeter_speed") * 60;
const double gap_fill_speed = config.opt_float("gap_fill_speed") * 60;
std::string last; // perimeter or gap
Points perimeter_points;
int gap_fills_outside_last_perimeters = 0;
parser.parse_buffer(
Slic3r::Test::slice({ Slic3r::Test::TestMesh::two_hollow_squares }, config),
[&perimeter_points, &gap_fills_outside_last_perimeters, &last, perimeter_speed, gap_fill_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0) {
double f = line.new_F(self);
Point point = line.new_XY_scaled(self);
if (is_approx(f, perimeter_speed)) {
if (last == "gap")
perimeter_points.clear();
perimeter_points.emplace_back(point);
last = "perimeter";
} else if (is_approx(f, gap_fill_speed)) {
Polygon convex_hull = Geometry::convex_hull(perimeter_points);
if (! convex_hull.contains(point))
++ gap_fills_outside_last_perimeters;
last = "gap";
}
}
});
THEN("gap fills are printed before leaving islands") {
REQUIRE(gap_fills_outside_last_perimeters == 0);
}
}
}

View File

@@ -0,0 +1,22 @@
#include <catch2/catch.hpp>
#include <memory>
#include "libslic3r/GCode.hpp"
using namespace Slic3r;
SCENARIO("Origin manipulation", "[GCode]") {
Slic3r::GCode gcodegen;
WHEN("set_origin to (10,0)") {
gcodegen.set_origin(Vec2d(10,0));
REQUIRE(gcodegen.origin() == Vec2d(10, 0));
}
WHEN("set_origin to (10,0) and translate by (5, 5)") {
gcodegen.set_origin(Vec2d(10,0));
gcodegen.set_origin(gcodegen.origin() + Vec2d(5, 5));
THEN("origin returns reference to point") {
REQUIRE(gcodegen.origin() == Vec2d(15,5));
}
}
}

View File

@@ -0,0 +1,292 @@
#include <catch2/catch.hpp>
#include <memory>
#include "libslic3r/GCode/FindReplace.hpp"
using namespace Slic3r;
SCENARIO("Find/Replace with plain text", "[GCodeFindReplace]") {
GIVEN("G-code") {
const std::string gcode =
"G1 Z0; home\n"
"G1 Z1; move up\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n";
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "i", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move UP\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "i", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Whole word
WHEN("Replace \"move up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move up", "move down", "w", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move u\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "w", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
WHEN("Replace \"ove up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "w", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Multi-line replace
WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") {
GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word.
WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "w", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word, fails.
WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "w", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
}
GIVEN("G-code with decimals") {
const std::string gcode =
"G1 Z0.123; home\n"
"G1 Z1.21; move up\n"
"G1 X0 Y.33 Z.431 E1.2; perimeter\n";
WHEN("Regular expression NOT processed in non-regex mode") {
GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
}
}
SCENARIO("Find/Replace with regexp", "[GCodeFindReplace]") {
GIVEN("G-code") {
const std::string gcode =
"G1 Z0; home\n"
"G1 Z1; move up\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n";
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "ri", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move UP\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "ri", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "r", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Whole word
WHEN("Replace \"move up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move up", "move down", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move u\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
WHEN("Replace \"ove up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Multi-line replace
WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") {
GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word.
WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word, fails.
WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
}
GIVEN("G-code with decimals") {
const std::string gcode =
"G1 Z0.123; home\n"
"G1 Z1.21; move up\n"
"G1 X0 Y.33 Z.431 E1.2; perimeter\n";
WHEN("Missing zeros before dot filled in") {
GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0.123; home\n"
"G1 Z1.21; move up\n"
"G1 X0 Y0.33 Z0.431 E1.2; perimeter\n");
}
}
GIVEN("Single layer G-code block with extrusion types") {
const std::string gcode =
// Start of a layer.
"G1 Z1.21; move up\n"
";TYPE:Infill\n"
"G1 X0 Y.33 Z.431 E1.2\n"
";TYPE:Solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Perimeter\n"
"G1 X0 Y.2 Z.431 E0.2\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n";
WHEN("Change extrusion rate of top solid infill, single line modifier") {
GCodeFindReplace find_replace({ "(;TYPE:Top solid infill\\n)(.*?)(;TYPE:[^T][^o][^p][^ ][^s]|$)", "${1}M221 S98\\n${2}M221 S95\\n${3}", "rs", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z1.21; move up\n"
";TYPE:Infill\n"
"G1 X0 Y.33 Z.431 E1.2\n"
";TYPE:Solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:Perimeter\n"
"G1 X0 Y.2 Z.431 E0.2\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n");
}
WHEN("Change extrusion rate of top solid infill, no single line modifier (incorrect)") {
GCodeFindReplace find_replace({ "(;TYPE:Top solid infill\\n)(.*?)(;TYPE:[^T][^o][^p][^ ][^s]|$)", "${1}M221 S98\\n${2}\\nM221 S95${3}", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z1.21; move up\n"
";TYPE:Infill\n"
"G1 X0 Y.33 Z.431 E1.2\n"
";TYPE:Solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:Perimeter\n"
"G1 X0 Y.2 Z.431 E0.2\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n");
}
}
}

View File

@@ -0,0 +1,96 @@
#include <catch2/catch.hpp>
#include <memory>
#include "libslic3r/GCodeWriter.hpp"
using namespace Slic3r;
SCENARIO("lift() is not ignored after unlift() at normal values of Z", "[GCodeWriter]") {
GIVEN("A config from a file and a single extruder.") {
GCodeWriter writer;
GCodeConfig &config = writer.config;
config.load(std::string(TEST_DATA_DIR) + "/fff_print_tests/test_gcodewriter/config_lift_unlift.ini", ForwardCompatibilitySubstitutionRule::Disable);
std::vector<unsigned int> extruder_ids {0};
writer.set_extruders(extruder_ids);
writer.set_extruder(0);
WHEN("Z is set to 203") {
double trouble_Z = 203;
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
WHEN("Z is set to 500003") {
double trouble_Z = 500003;
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
WHEN("Z is set to 10.3") {
double trouble_Z = 10.3;
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
// The test above will fail for trouble_Z == 9007199254740992, where trouble_Z + 1.5 will be rounded to trouble_Z + 2.0 due to double mantisa overflow.
}
}
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
GIVEN("GCodeWriter instance") {
GCodeWriter writer;
WHEN("set_speed is called to set speed to 99999.123") {
THEN("Output string is G1 F99999.123") {
REQUIRE_THAT(writer.set_speed(99999.123), Catch::Equals("G1 F99999.123\n"));
}
}
WHEN("set_speed is called to set speed to 1") {
THEN("Output string is G1 F1") {
REQUIRE_THAT(writer.set_speed(1.0), Catch::Equals("G1 F1\n"));
}
}
WHEN("set_speed is called to set speed to 203.200022") {
THEN("Output string is G1 F203.2") {
REQUIRE_THAT(writer.set_speed(203.200022), Catch::Equals("G1 F203.2\n"));
}
}
WHEN("set_speed is called to set speed to 203.200522") {
THEN("Output string is G1 F203.201") {
REQUIRE_THAT(writer.set_speed(203.200522), Catch::Equals("G1 F203.201\n"));
}
}
}
}

View File

@@ -0,0 +1,61 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include <boost/nowide/cstdio.hpp>
#include <boost/filesystem.hpp>
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("Model construction", "[Model]") {
GIVEN("A Slic3r Model") {
Slic3r::Model model;
Slic3r::TriangleMesh sample_mesh = Slic3r::make_cube(20,20,20);
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
Slic3r::Print print;
WHEN("Model object is added") {
Slic3r::ModelObject *model_object = model.add_object();
THEN("Model object list == 1") {
REQUIRE(model.objects.size() == 1);
}
model_object->add_volume(sample_mesh);
THEN("Model volume list == 1") {
REQUIRE(model_object->volumes.size() == 1);
}
THEN("Model volume is a part") {
REQUIRE(model_object->volumes.front()->is_model_part());
}
THEN("Mesh is equivalent to input mesh.") {
REQUIRE(! sample_mesh.its.vertices.empty());
const std::vector<Vec3f>& mesh_vertices = model_object->volumes.front()->mesh().its.vertices;
Vec3f mesh_offset = model_object->volumes.front()->source.mesh_offset.cast<float>();
for (size_t i = 0; i < sample_mesh.its.vertices.size(); ++ i) {
const Vec3f &p1 = sample_mesh.its.vertices[i];
const Vec3f p2 = mesh_vertices[i] + mesh_offset;
REQUIRE((p2 - p1).norm() < EPSILON);
}
}
model_object->add_instance();
arrange_objects(model, InfiniteBed{scaled(Vec2d(100, 100))}, ArrangeParams{scaled(min_object_distance(config))});
model_object->ensure_on_bed();
print.auto_assign_extruders(model_object);
THEN("Print works?") {
print.set_status_silent();
print.apply(model, config);
print.process();
boost::filesystem::path temp = boost::filesystem::unique_path();
print.export_gcode(temp.string(), nullptr, nullptr);
REQUIRE(boost::filesystem::exists(temp));
REQUIRE(boost::filesystem::is_regular_file(temp));
REQUIRE(boost::filesystem::file_size(temp) > 0);
boost::nowide::remove(temp.string().c_str());
}
}
}
}

View File

@@ -0,0 +1,271 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
using namespace std::literals;
SCENARIO("Basic tests", "[Multi]")
{
WHEN("Slicing multi-material print with non-consecutive extruders") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 },
{
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "extruder", 2 },
{ "infill_extruder", 4 },
{ "support_material_extruder", 0 }
});
THEN("Sliced successfully") {
REQUIRE(! gcode.empty());
}
THEN("T3 toolchange command found") {
bool T1_found = gcode.find("\nT3\n") != gcode.npos;
REQUIRE(T1_found);
}
}
WHEN("Slicing with multiple skirts with a single, non-zero extruder") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 },
{
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "perimeter_extruder", 2 },
{ "infill_extruder", 2 },
{ "support_material_extruder", 2 },
{ "support_material_interface_extruder", 2 },
});
THEN("Sliced successfully") {
REQUIRE(! gcode.empty());
}
}
}
SCENARIO("Ooze prevention", "[Multi]")
{
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "raft_layers", 2 },
{ "infill_extruder", 2 },
{ "solid_infill_extruder", 3 },
{ "support_material_extruder", 4 },
{ "ooze_prevention", 1 },
{ "extruder_offset", "0x0, 20x0, 0x20, 20x20" },
{ "temperature", "200, 180, 170, 160" },
{ "first_layer_temperature", "206, 186, 166, 156" },
// test that it doesn't crash when this is supplied
{ "toolchange_gcode", "T[next_extruder] ;toolchange" }
});
FullPrintConfig print_config;
print_config.apply(config);
// Since July 2019, QIDISlicer only emits automatic Tn command in case that the toolchange_gcode is empty
// The "T[next_extruder]" is therefore needed in this test.
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
GCodeReader parser;
int tool = -1;
int tool_temp[] = { 0, 0, 0, 0};
Points toolchange_points;
Points extrusion_points;
parser.parse_buffer(gcode, [&tool, &tool_temp, &toolchange_points, &extrusion_points, &print_config]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (boost::starts_with(line.cmd(), "T")) {
// Ignore initial toolchange.
if (tool != -1) {
int expected_temp = is_approx<double>(self.z(), print_config.get_abs_value("first_layer_height") + print_config.z_offset) ?
print_config.first_layer_temperature.get_at(tool) :
print_config.temperature.get_at(tool);
if (tool_temp[tool] != expected_temp + print_config.standby_temperature_delta)
throw std::runtime_error("Standby temperature was not set before toolchange.");
toolchange_points.emplace_back(self.xy_scaled());
}
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd_is("M104") || line.cmd_is("M109")) {
// May not be defined on this line.
int t = tool;
line.has_value('T', t);
// Should be available on this line.
int s;
if (! line.has_value('S', s))
throw std::runtime_error("M104 or M109 without S");
// Following is obsolete. The first printing extruder is newly set to its first layer temperature immediately, not to the standby.
//if (tool_temp[t] == 0 && s != print_config.first_layer_temperature.get_at(t) + print_config.standby_temperature_delta)
// throw std::runtime_error("initial temperature is not equal to first layer temperature + standby delta");
tool_temp[t] = s;
} else if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0) {
extrusion_points.emplace_back(line.new_XY_scaled(self) + scaled<coord_t>(print_config.extruder_offset.get_at(tool)));
}
});
Polygon convex_hull = Geometry::convex_hull(extrusion_points);
// THEN("all nozzles are outside skirt at toolchange") {
// Points t;
// sort_remove_duplicates(toolchange_points);
// size_t inside = 0;
// for (const auto &point : toolchange_points)
// for (const Vec2d &offset : print_config.extruder_offset.values) {
// Point p = point + scaled<coord_t>(offset);
// if (convex_hull.contains(p))
// ++ inside;
// }
// REQUIRE(inside == 0);
// }
#if 0
require "Slic3r/SVG.pm";
Slic3r::SVG::output(
"ooze_prevention_test.svg",
no_arrows => 1,
polygons => [$convex_hull],
red_points => \@t,
points => \@toolchange_points,
);
#endif
THEN("all toolchanges happen within expected area") {
// offset the skirt by the maximum displacement between extruders plus a safety extra margin
const float delta = scaled<float>(20. * sqrt(2.) + 1.);
Polygon outer_convex_hull = expand(convex_hull, delta).front();
size_t inside = std::count_if(toolchange_points.begin(), toolchange_points.end(), [&outer_convex_hull](const Point &p){ return outer_convex_hull.contains(p); });
REQUIRE(inside == toolchange_points.size());
}
}
std::string slice_stacked_cubes(const DynamicPrintConfig &config, const DynamicPrintConfig &volume1config, const DynamicPrintConfig &volume2config)
{
Model model;
ModelObject *object = model.add_object();
object->name = "object.stl";
ModelVolume *v1 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
v1->set_material_id("lower_material");
v1->config.assign_config(volume1config);
ModelVolume *v2 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
v2->set_material_id("upper_material");
v2->translate(0., 0., 20.);
v2->config.assign_config(volume2config);
object->add_instance();
object->ensure_on_bed();
Print print;
print.auto_assign_extruders(object);
THEN("auto_assign_extruders() assigned correct extruder to first volume") {
REQUIRE(v1->config.extruder() == 1);
}
THEN("auto_assign_extruders() assigned correct extruder to second volume") {
REQUIRE(v2->config.extruder() == 2);
}
print.apply(model, config);
print.validate();
return Test::gcode(print);
}
SCENARIO("Stacked cubes", "[Multi]")
{
DynamicPrintConfig lower_config;
lower_config.set_deserialize_strict({
{ "extruder", 1 },
{ "bottom_solid_layers", 0 },
{ "top_solid_layers", 1 },
});
DynamicPrintConfig upper_config;
upper_config.set_deserialize_strict({
{ "extruder", 2 },
{ "bottom_solid_layers", 1 },
{ "top_solid_layers", 0 }
});
static constexpr const double solid_infill_speed = 99;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "fill_density", 0 },
{ "solid_infill_speed", solid_infill_speed },
{ "top_solid_infill_speed", solid_infill_speed },
// for preventing speeds from being altered
{ "cooling", "0, 0, 0, 0" },
// for preventing speeds from being altered
{ "first_layer_speed", "100%" }
});
auto test_shells = [](const std::string &gcode) {
GCodeReader parser;
int tool = -1;
// Scaled Z heights.
std::set<coord_t> T0_shells, T1_shells;
parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (is_approx<double>(line.new_F(self), solid_infill_speed * 60.) && (tool == 0 || tool == 1))
(tool == 0 ? T0_shells : T1_shells).insert(scaled<coord_t>(self.z()));
}
});
return std::make_pair(T0_shells, T1_shells);
};
WHEN("Interface shells disabled") {
std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
auto [t0, t1] = test_shells(gcode);
THEN("no interface shells") {
REQUIRE(t0.empty());
REQUIRE(t1.empty());
}
}
WHEN("Interface shells enabled") {
config.set_deserialize_strict("interface_shells", "1");
std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
auto [t0, t1] = test_shells(gcode);
THEN("top interface shells") {
REQUIRE(t0.size() == lower_config.opt_int("top_solid_layers"));
}
THEN("bottom interface shells") {
REQUIRE(t1.size() == upper_config.opt_int("bottom_solid_layers"));
}
}
WHEN("Slicing with auto-assigned extruders") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
{ "layer_height", 0.4 },
{ "first_layer_height", 0.4 },
{ "skirts", 0 }
});
std::string gcode = slice_stacked_cubes(config, DynamicPrintConfig{}, DynamicPrintConfig{});
GCodeReader parser;
int tool = -1;
// Scaled Z heights.
std::set<coord_t> T0_shells, T1_shells;
parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (tool == 0 && self.z() > 20)
// Layers incorrectly extruded with T0 at the top object.
T0_shells.insert(scaled<coord_t>(self.z()));
else if (tool == 1 && self.z() < 20)
// Layers incorrectly extruded with T1 at the bottom object.
T1_shells.insert(scaled<coord_t>(self.z()));
}
});
THEN("T0 is never used for upper object") {
REQUIRE(T0_shells.empty());
}
THEN("T0 is never used for lower object") {
REQUIRE(T1_shells.empty());
}
}
}

View File

@@ -0,0 +1,629 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "libslic3r/Config.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/PerimeterGenerator.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/SurfaceCollection.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Perimeter nesting", "[Perimeters]")
{
struct TestData {
ExPolygons expolygons;
// expected number of loops
int total;
// expected number of external loops
int external;
// expected external perimeter
std::vector<bool> ext_order;
// expected number of internal contour loops
int cinternal;
// expected number of ccw loops
int ccw;
// expected ccw/cw order
std::vector<bool> ccw_order;
// expected nesting order
std::vector<std::vector<int>> nesting;
};
FullPrintConfig config;
auto test = [&config](const TestData &data) {
SurfaceCollection slices;
slices.append(data.expolygons, stInternal);
ExtrusionEntityCollection loops;
ExtrusionEntityCollection gap_fill;
ExPolygons fill_expolygons;
Flow flow(1., 1., 1.);
PerimeterGenerator::Parameters perimeter_generator_params(
1., // layer height
-1, // layer ID
flow, flow, flow, flow,
static_cast<const PrintRegionConfig&>(config),
static_cast<const PrintObjectConfig&>(config),
static_cast<const PrintConfig&>(config),
false); // spiral_vase
Polygons lower_layer_polygons_cache;
for (const Surface &surface : slices)
// FIXME Lukas H.: Disable this test for Arachne because it is failing and needs more investigation.
// if (config.perimeter_generator == PerimeterGeneratorType::Arachne)
// PerimeterGenerator::process_arachne();
// else
PerimeterGenerator::process_classic(
// input:
perimeter_generator_params,
surface,
nullptr,
// cache:
lower_layer_polygons_cache,
// output:
loops, gap_fill, fill_expolygons);
THEN("expected number of collections") {
REQUIRE(loops.entities.size() == data.expolygons.size());
}
loops = loops.flatten();
THEN("expected number of loops") {
REQUIRE(loops.entities.size() == data.total);
}
THEN("expected number of external loops") {
size_t num_external = std::count_if(loops.entities.begin(), loops.entities.end(),
[](const ExtrusionEntity *ee){ return ee->role() == ExtrusionRole::ExternalPerimeter; });
REQUIRE(num_external == data.external);
}
THEN("expected external order") {
std::vector<bool> ext_order;
for (auto *ee : loops.entities)
ext_order.emplace_back(ee->role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(ext_order == data.ext_order);
}
THEN("expected number of internal contour loops") {
size_t cinternal = std::count_if(loops.entities.begin(), loops.entities.end(),
[](const ExtrusionEntity *ee){ return dynamic_cast<const ExtrusionLoop*>(ee)->loop_role() == elrContourInternalPerimeter; });
REQUIRE(cinternal == data.cinternal);
}
THEN("expected number of ccw loops") {
size_t ccw = std::count_if(loops.entities.begin(), loops.entities.end(),
[](const ExtrusionEntity *ee){ return dynamic_cast<const ExtrusionLoop*>(ee)->polygon().is_counter_clockwise(); });
REQUIRE(ccw == data.ccw);
}
THEN("expected ccw/cw order") {
std::vector<bool> ccw_order;
for (auto *ee : loops.entities)
ccw_order.emplace_back(dynamic_cast<const ExtrusionLoop*>(ee)->polygon().is_counter_clockwise());
REQUIRE(ccw_order == data.ccw_order);
}
THEN("expected nesting order") {
for (const std::vector<int> &nesting : data.nesting) {
for (size_t i = 1; i < nesting.size(); ++ i)
REQUIRE(dynamic_cast<const ExtrusionLoop*>(loops.entities[nesting[i - 1]])->polygon().contains(loops.entities[nesting[i]]->first_point()));
}
}
};
WHEN("Rectangle") {
config.perimeters.value = 3;
TestData data;
data.expolygons = {
ExPolygon{ Polygon::new_scale({ {0,0}, {100,0}, {100,100}, {0,100} }) }
};
data.total = 3;
data.external = 1;
data.ext_order = { false, false, true };
data.cinternal = 1;
data.ccw = 3;
data.ccw_order = { true, true, true };
data.nesting = { { 2, 1, 0 } };
test(data);
}
WHEN("Rectangle with hole") {
config.perimeters.value = 3;
TestData data;
data.expolygons = {
ExPolygon{ Polygon::new_scale({ {0,0}, {100,0}, {100,100}, {0,100} }),
Polygon::new_scale({ {40,40}, {40,60}, {60,60}, {60,40} }) }
};
data.total = 6;
data.external = 2;
data.ext_order = { false, false, true, false, false, true };
data.cinternal = 1;
data.ccw = 3;
data.ccw_order = { false, false, false, true, true, true };
data.nesting = { { 5, 4, 3, 0, 1, 2 } };
test(data);
}
WHEN("Nested rectangles with holes") {
config.perimeters.value = 3;
TestData data;
data.expolygons = {
ExPolygon{ Polygon::new_scale({ {0,0}, {200,0}, {200,200}, {0,200} }),
Polygon::new_scale({ {20,20}, {20,180}, {180,180}, {180,20} }) },
ExPolygon{ Polygon::new_scale({ {50,50}, {150,50}, {150,150}, {50,150} }),
Polygon::new_scale({ {80,80}, {80,120}, {120,120}, {120,80} }) }
};
data.total = 4*3;
data.external = 4;
data.ext_order = { false, false, true, false, false, true, false, false, true, false, false, true };
data.cinternal = 2;
data.ccw = 2*3;
data.ccw_order = { false, false, false, true, true, true, false, false, false, true, true, true };
test(data);
}
WHEN("Rectangle with multiple holes") {
config.perimeters.value = 2;
TestData data;
ExPolygon expoly{ Polygon::new_scale({ {0,0}, {50,0}, {50,50}, {0,50} }) };
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,7.5}, {7.5,12.5}, {12.5,12.5}, {12.5,7.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,17.5}, {7.5,22.5}, {12.5,22.5}, {12.5,17.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,27.5}, {7.5,32.5}, {12.5,32.5}, {12.5,27.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,37.5}, {7.5,42.5}, {12.5,42.5}, {12.5,37.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {17.5,7.5}, {17.5,12.5}, {22.5,12.5}, {22.5,7.5} }));
data.expolygons = { expoly };
data.total = 12;
data.external = 6;
data.ext_order = { false, true, false, true, false, true, false, true, false, true, false, true };
data.cinternal = 1;
data.ccw = 2;
data.ccw_order = { false, false, false, false, false, false, false, false, false, false, true, true };
data.nesting = { {0,1},{2,3},{4,5},{6,7},{8,9} };
test(data);
};
}
SCENARIO("Perimeters", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "fill_density", 0 },
{ "perimeters", 3 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" }
});
WHEN("Bridging perimeters disabled") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config);
THEN("all perimeters extruded ccw") {
GCodeReader parser;
bool has_cw_loops = false;
Polygon current_loop;
parser.parse_buffer(gcode, [&has_cw_loops, &current_loop](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0) {
if (current_loop.empty())
current_loop.points.emplace_back(self.xy_scaled());
current_loop.points.emplace_back(line.new_XY_scaled(self));
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
if (! current_loop.empty() && current_loop.is_clockwise())
has_cw_loops = true;
current_loop.clear();
}
});
REQUIRE(! has_cw_loops);
}
}
auto test = [&config](Test::TestMesh model) {
// we test two copies to make sure ExtrusionLoop objects are not modified in-place (the second object would not detect cw loops and thus would calculate wrong)
std::string gcode = Slic3r::Test::slice({ model, model }, config);
GCodeReader parser;
bool has_cw_loops = false;
bool has_outwards_move = false;
bool starts_on_convex_point = false;
// print_z => count of external loops
std::map<coord_t, int> external_loops;
Polygon current_loop;
const double external_perimeter_speed = config.get_abs_value("external_perimeter_speed") * 60.;
parser.parse_buffer(gcode, [&has_cw_loops, &has_outwards_move, &starts_on_convex_point, &external_loops, &current_loop, external_perimeter_speed, model]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0) {
if (current_loop.empty())
current_loop.points.emplace_back(self.xy_scaled());
current_loop.points.emplace_back(line.new_XY_scaled(self));
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
if (! current_loop.empty()) {
if (current_loop.is_clockwise())
has_cw_loops = true;
if (is_approx<double>(self.f(), external_perimeter_speed)) {
// reset counter for second object
coord_t z = scaled<coord_t>(self.z());
auto it = external_loops.find(z);
if (it == external_loops.end())
it = external_loops.insert(std::make_pair(z, 0)).first;
else if (it->second == 2)
it->second = 0;
++ it->second;
bool is_contour = it->second == 2;
bool is_hole = it->second == 1;
// Testing whether the move point after loop ends up inside the extruded loop.
bool loop_contains_point = current_loop.contains(line.new_XY_scaled(self));
if (// contour should include destination
(! loop_contains_point && is_contour) ||
// hole should not
(loop_contains_point && is_hole))
has_outwards_move = true;
if (model == Test::TestMesh::cube_with_concave_hole) {
// check that loop starts at a concave vertex
double cross = cross2((current_loop.points.front() - current_loop.points[current_loop.points.size() - 2]).cast<double>(), (current_loop.points[1] - current_loop.points.front()).cast<double>());
bool convex = cross > 0.;
if ((convex && is_contour) || (! convex && is_hole))
starts_on_convex_point = true;
}
}
current_loop.clear();
}
}
});
THEN("all perimeters extruded ccw") {
REQUIRE(! has_cw_loops);
}
// FIXME Lukas H.: Arachne is printing external loops before hole loops in this test case.
if (config.opt_enum<PerimeterGeneratorType>("perimeter_generator") == Slic3r::PerimeterGeneratorType::Arachne) {
THEN("move outwards after completing external loop") {
// REQUIRE(! has_outwards_move);
}
// FIXME Lukas H.: Disable this test for Arachne because it is failing and needs more investigation.
THEN("loops start on concave point if any") {
// REQUIRE(! starts_on_convex_point);
}
} else {
THEN("move inwards after completing external loop") {
REQUIRE(! has_outwards_move);
}
THEN("loops start on concave point if any") {
REQUIRE(! starts_on_convex_point);
}
}
};
// Reusing the config above.
config.set_deserialize_strict({
{ "external_perimeter_speed", 68 }
});
GIVEN("Cube with hole") { test(Test::TestMesh::cube_with_hole); }
GIVEN("Cube with concave hole") { test(Test::TestMesh::cube_with_concave_hole); }
WHEN("Bridging perimeters enabled") {
// Reusing the config above.
config.set_deserialize_strict({
{ "perimeters", 1 },
{ "perimeter_speed", 77 },
{ "external_perimeter_speed", 66 },
{ "enable_dynamic_overhang_speeds", false },
{ "bridge_speed", 99 },
{ "cooling", "1" },
{ "fan_below_layer_time", "0" },
{ "slowdown_below_layer_time", "0" },
{ "bridge_fan_speed", "100" },
// arbitrary value
{ "bridge_flow_ratio", 33 },
{ "overhangs", true }
});
std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::overhang) }, config);
THEN("Bridging is applied to bridging perimeters") {
GCodeReader parser;
// print Z => speeds
std::map<coord_t, std::set<double>> layer_speeds;
int fan_speed = 0;
const double perimeter_speed = config.opt_float("perimeter_speed") * 60.;
const double external_perimeter_speed = config.get_abs_value("external_perimeter_speed") * 60.;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
const double nozzle_dmr = config.opt<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
const double filament_dmr = config.opt<ConfigOptionFloats>("filament_diameter")->get_at(0);
const double bridge_mm_per_mm = sqr(nozzle_dmr / filament_dmr) * config.opt_float("bridge_flow_ratio");
parser.parse_buffer(gcode, [&layer_speeds, &fan_speed, perimeter_speed, external_perimeter_speed, bridge_speed, nozzle_dmr, filament_dmr, bridge_mm_per_mm]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("M107"))
fan_speed = 0;
else if (line.cmd_is("M106"))
line.has_value('S', fan_speed);
else if (line.extruding(self) && line.dist_XY(self) > 0) {
double feedrate = line.new_F(self);
REQUIRE((is_approx(feedrate, perimeter_speed) || is_approx(feedrate, external_perimeter_speed) || is_approx(feedrate, bridge_speed)));
layer_speeds[self.z()].insert(feedrate);
bool bridging = is_approx(feedrate, bridge_speed);
double mm_per_mm = line.dist_E(self) / line.dist_XY(self);
// Fan enabled at full speed when bridging, disabled when not bridging.
REQUIRE((! bridging || fan_speed == 255));
REQUIRE((bridging || fan_speed == 0));
// When bridging, bridge flow is applied.
REQUIRE((! bridging || std::abs(mm_per_mm - bridge_mm_per_mm) <= 0.01));
}
});
// only overhang layer has more than one speed
size_t num_overhangs = std::count_if(layer_speeds.begin(), layer_speeds.end(), [](const std::pair<double, std::set<double>> &v){ return v.second.size() > 1; });
REQUIRE(num_overhangs == 1);
}
}
GIVEN("iPad stand") {
WHEN("Extra perimeters enabled") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeters", 3 },
{ "layer_height", 0.4 },
{ "first_layer_height", 0.35 },
{ "extra_perimeters", 1 },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" },
{ "perimeter_speed", 99 },
{ "external_perimeter_speed", 99 },
{ "small_perimeter_speed", 99 },
{ "thin_walls", 0 },
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::ipadstand }, config);
// z => number of loops
std::map<coord_t, int> perimeters;
bool in_loop = false;
const double perimeter_speed = config.opt_float("perimeter_speed") * 60.;
GCodeReader parser;
parser.parse_buffer(gcode, [&perimeters, &in_loop, perimeter_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0 && is_approx<double>(line.new_F(self), perimeter_speed)) {
if (! in_loop) {
coord_t z = scaled<coord_t>(self.z());
auto it = perimeters.find(z);
if (it == perimeters.end())
it = perimeters.insert(std::make_pair(z, 0)).first;
++ it->second;
}
in_loop = true;
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
in_loop = false;
}
});
THEN("no superfluous extra perimeters") {
const int num_perimeters = config.opt_int("perimeters");
size_t extra_perimeters = std::count_if(perimeters.begin(), perimeters.end(), [num_perimeters](const std::pair<const coord_t, int> &v){ return (v.second % num_perimeters) > 0; });
REQUIRE(extra_perimeters == 0);
}
}
}
}
SCENARIO("Some weird coverage test", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.4" },
{ "perimeters", 2 },
{ "perimeter_extrusion_width", 0.4 },
{ "external_perimeter_extrusion_width", 0.4 },
{ "infill_extrusion_width", 0.53 },
{ "solid_infill_extrusion_width", 0.53 }
});
// we just need a pre-filled Print object
Print print;
Model model;
Slic3r::Test::init_print({ Test::TestMesh::cube_20x20x20 }, print, model, config);
// override a layer's slices
ExPolygon expolygon;
expolygon.contour = {
{-71974463,-139999376},{-71731792,-139987456},{-71706544,-139985616},{-71682119,-139982639},{-71441248,-139946912},{-71417487,-139942895},{-71379384,-139933984},{-71141800,-139874480},
{-71105247,-139862895},{-70873544,-139779984},{-70838592,-139765856},{-70614943,-139660064},{-70581783,-139643567},{-70368368,-139515680},{-70323751,-139487872},{-70122160,-139338352},
{-70082399,-139306639},{-69894800,-139136624},{-69878679,-139121327},{-69707992,-138933008},{-69668575,-138887343},{-69518775,-138685359},{-69484336,-138631632},{-69356423,-138418207},
{-69250040,-138193296},{-69220920,-138128976},{-69137992,-137897168},{-69126095,-137860255},{-69066568,-137622608},{-69057104,-137582511},{-69053079,-137558751},{-69017352,-137317872},
{-69014392,-137293456},{-69012543,-137268207},{-68999369,-137000000},{-63999999,-137000000},{-63705947,-136985551},{-63654984,-136977984},{-63414731,-136942351},{-63364756,-136929840},
{-63129151,-136870815},{-62851950,-136771631},{-62585807,-136645743},{-62377483,-136520895},{-62333291,-136494415},{-62291908,-136463728},{-62096819,-136319023},{-62058644,-136284432},
{-61878676,-136121328},{-61680968,-135903184},{-61650275,-135861807},{-61505591,-135666719},{-61354239,-135414191},{-61332211,-135367615},{-61228359,-135148063},{-61129179,-134870847},
{-61057639,-134585262},{-61014451,-134294047},{-61000000,-134000000},{-61000000,-107999999},{-61014451,-107705944},{-61057639,-107414736},{-61129179,-107129152},{-61228359,-106851953},
{-61354239,-106585808},{-61505591,-106333288},{-61680967,-106096816},{-61878675,-105878680},{-62096820,-105680967},{-62138204,-105650279},{-62333292,-105505591},{-62585808,-105354239},
{-62632384,-105332207},{-62851951,-105228360},{-62900463,-105211008},{-63129152,-105129183},{-63414731,-105057640},{-63705947,-105014448},{-63999999,-105000000},{-68999369,-105000000},
{-69012543,-104731792},{-69014392,-104706544},{-69017352,-104682119},{-69053079,-104441248},{-69057104,-104417487},{-69066008,-104379383},{-69125528,-104141799},{-69137111,-104105248},
{-69220007,-103873544},{-69234136,-103838591},{-69339920,-103614943},{-69356415,-103581784},{-69484328,-103368367},{-69512143,-103323752},{-69661647,-103122160},{-69693352,-103082399},
{-69863383,-102894800},{-69878680,-102878679},{-70066999,-102707992},{-70112656,-102668576},{-70314648,-102518775},{-70368367,-102484336},{-70581783,-102356424},{-70806711,-102250040},
{-70871040,-102220919},{-71102823,-102137992},{-71139752,-102126095},{-71377383,-102066568},{-71417487,-102057104},{-71441248,-102053079},{-71682119,-102017352},{-71706535,-102014392},
{-71731784,-102012543},{-71974456,-102000624},{-71999999,-102000000},{-104000000,-102000000},{-104025536,-102000624},{-104268207,-102012543},{-104293455,-102014392},
{-104317880,-102017352},{-104558751,-102053079},{-104582512,-102057104},{-104620616,-102066008},{-104858200,-102125528},{-104894751,-102137111},{-105126455,-102220007},
{-105161408,-102234136},{-105385056,-102339920},{-105418215,-102356415},{-105631632,-102484328},{-105676247,-102512143},{-105877839,-102661647},{-105917600,-102693352},
{-106105199,-102863383},{-106121320,-102878680},{-106292007,-103066999},{-106331424,-103112656},{-106481224,-103314648},{-106515663,-103368367},{-106643575,-103581783},
{-106749959,-103806711},{-106779080,-103871040},{-106862007,-104102823},{-106873904,-104139752},{-106933431,-104377383},{-106942896,-104417487},{-106946920,-104441248},
{-106982648,-104682119},{-106985607,-104706535},{-106987456,-104731784},{-107000630,-105000000},{-112000000,-105000000},{-112294056,-105014448},{-112585264,-105057640},
{-112870848,-105129184},{-112919359,-105146535},{-113148048,-105228360},{-113194624,-105250392},{-113414191,-105354239},{-113666711,-105505591},{-113708095,-105536279},
{-113903183,-105680967},{-114121320,-105878679},{-114319032,-106096816},{-114349720,-106138200},{-114494408,-106333288},{-114645760,-106585808},{-114667792,-106632384},
{-114771640,-106851952},{-114788991,-106900463},{-114870815,-107129151},{-114942359,-107414735},{-114985551,-107705943},{-115000000,-107999999},{-115000000,-134000000},
{-114985551,-134294048},{-114942359,-134585263},{-114870816,-134870847},{-114853464,-134919359},{-114771639,-135148064},{-114645759,-135414192},{-114494407,-135666720},
{-114319031,-135903184},{-114121320,-136121327},{-114083144,-136155919},{-113903184,-136319023},{-113861799,-136349712},{-113666711,-136494416},{-113458383,-136619264},
{-113414192,-136645743},{-113148049,-136771631},{-112870848,-136870815},{-112820872,-136883327},{-112585264,-136942351},{-112534303,-136949920},{-112294056,-136985551},
{-112000000,-137000000},{-107000630,-137000000},{-106987456,-137268207},{-106985608,-137293440},{-106982647,-137317872},{-106946920,-137558751},{-106942896,-137582511},
{-106933991,-137620624},{-106874471,-137858208},{-106862888,-137894751},{-106779992,-138126463},{-106765863,-138161424},{-106660080,-138385055},{-106643584,-138418223},
{-106515671,-138631648},{-106487855,-138676256},{-106338352,-138877839},{-106306647,-138917600},{-106136616,-139105199},{-106121320,-139121328},{-105933000,-139291999},
{-105887344,-139331407},{-105685351,-139481232},{-105631632,-139515663},{-105418216,-139643567},{-105193288,-139749951},{-105128959,-139779072},{-104897175,-139862016},
{-104860247,-139873904},{-104622616,-139933423},{-104582511,-139942896},{-104558751,-139946912},{-104317880,-139982656},{-104293463,-139985616},{-104268216,-139987456},
{-104025544,-139999376},{-104000000,-140000000},{-71999999,-140000000}
};
expolygon.holes = {
{{-105000000,-138000000},{-105000000,-104000000},{-71000000,-104000000},{-71000000,-138000000}},
{{-69000000,-132000000},{-69000000,-110000000},{-64991180,-110000000},{-64991180,-132000000}},
{{-111008824,-132000000},{-111008824,-110000000},{-107000000,-110000000},{-107000000,-132000000}}
};
PrintObject *object = print.get_object(0);
object->slice();
Layer *layer = object->get_layer(1);
LayerRegion *layerm = layer->get_region(0);
layerm->m_slices.clear();
layerm->m_slices.append({ expolygon }, stInternal);
layer->lslices = { expolygon };
layer->lslices_ex = { { get_extents(expolygon) } };
// make perimeters
layer->make_perimeters();
// compute the covered area
Flow pflow = layerm->flow(frPerimeter);
Flow iflow = layerm->flow(frInfill);
Polygons covered_by_perimeters;
Polygons covered_by_infill;
{
Polygons acc;
for (const ExtrusionEntity *ee : layerm->perimeters())
for (const ExtrusionEntity *ee : dynamic_cast<const ExtrusionEntityCollection*>(ee)->entities)
append(acc, offset(dynamic_cast<const ExtrusionLoop*>(ee)->polygon().split_at_first_point(), float(pflow.scaled_width() / 2.f + SCALED_EPSILON)));
covered_by_perimeters = union_(acc);
}
{
Polygons acc;
for (const ExPolygon &expolygon : layerm->fill_expolygons())
append(acc, to_polygons(expolygon));
for (const ExtrusionEntity *ee : layerm->thin_fills().entities)
append(acc, offset(dynamic_cast<const ExtrusionPath*>(ee)->polyline, float(iflow.scaled_width() / 2.f + SCALED_EPSILON)));
covered_by_infill = union_(acc);
}
// compute the non covered area
ExPolygons non_covered = diff_ex(to_polygons(layerm->slices().surfaces), union_(covered_by_perimeters, covered_by_infill));
/*
if (0) {
printf "max non covered = %f\n", List::Util::max(map unscale unscale $_->area, @$non_covered);
require "Slic3r/SVG.pm";
Slic3r::SVG::output(
"gaps.svg",
expolygons => [ map $_->expolygon, @{$layerm->slices} ],
red_expolygons => union_ex([ map @$_, (@$covered_by_perimeters, @$covered_by_infill) ]),
green_expolygons => union_ex($non_covered),
no_arrows => 1,
polylines => [
map $_->polygon->split_at_first_point, map @$_, @{$layerm->perimeters},
],
);
}
*/
THEN("no gap between perimeters and infill") {
size_t num_non_convered = std::count_if(non_covered.begin(), non_covered.end(),
[&iflow](const ExPolygon &ex){ return ex.area() > sqr(double(iflow.scaled_width())); });
REQUIRE(num_non_convered == 0);
}
}
SCENARIO("Perimeters3", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeters", 3 },
{ "layer_height", 0.15 },
{ "bridge_speed", 99 },
{ "enable_dynamic_overhang_speeds", false },
// to prevent bridging over sparse infill
{ "fill_density", 0 },
{ "overhangs", true },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" }
});
auto test = [&config](const Vec3d &scale) {
std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::V, Vec3d::Zero(), scale) }, config);
GCodeReader parser;
std::set<coord_t> z_with_bridges;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
parser.parse_buffer(gcode, [&z_with_bridges, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0 && is_approx<double>(line.new_F(self), bridge_speed))
z_with_bridges.insert(scaled<coord_t>(self.z()));
});
return z_with_bridges.size();
};
GIVEN("V shape, unscaled") {
int n = test(Vec3d(1., 1., 1.));
// One bridge layer under the V middle and one layer (two briding areas) under tops
THEN("no overhangs printed with bridge speed") {
REQUIRE(n == 2);
}
}
GIVEN("V shape, scaled 3x in X") {
int n = test(Vec3d(3., 1., 1.));
// except for the two internal solid layers above void
THEN("overhangs printed with bridge speed") {
REQUIRE(n > 2);
}
}
}
SCENARIO("Perimeters4", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "seam_position", "random" }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("successful generation of G-code with seam_position = random") {
REQUIRE(! gcode.empty());
}
}
SCENARIO("Seam alignment", "[Perimeters]")
{
auto test = [](Test::TestMesh model) {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "seam_position", "aligned" },
{ "skirts", 0 },
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
{ "retract_layer_change", "0" }
});
std::string gcode = Slic3r::Test::slice({ model }, config);
bool was_extruding = false;
Points seam_points;
GCodeReader parser;
parser.parse_buffer(gcode, [&was_extruding, &seam_points](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self)) {
if (! was_extruding)
seam_points.emplace_back(self.xy_scaled());
was_extruding = true;
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
was_extruding = false;
}
});
THEN("seam is aligned") {
size_t num_not_aligned = 0;
for (size_t i = 1; i < seam_points.size(); ++ i) {
double d = (seam_points[i] - seam_points[i - 1]).cast<double>().norm();
// Seams shall be aligned up to 3mm.
if (d > scaled<double>(3.))
++ num_not_aligned;
}
REQUIRE(num_not_aligned == 0);
}
};
GIVEN("20mm cube") {
test(Slic3r::Test::TestMesh::cube_20x20x20);
}
GIVEN("small_dorito") {
test(Slic3r::Test::TestMesh::small_dorito);
}
}

View File

@@ -0,0 +1,187 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Print.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
GIVEN("20mm cube and default config") {
WHEN("make_perimeters() is called") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { { "fill_density", 0 } });
const PrintObject &object = *print.objects().front();
THEN("67 layers exist in the model") {
REQUIRE(object.layers().size() == 66);
}
THEN("Every layer in region 0 has 1 island of perimeters") {
for (const Layer *layer : object.layers())
REQUIRE(layer->regions().front()->perimeters().size() == 1);
}
THEN("Every layer in region 0 has 3 paths in its perimeters list.") {
for (const Layer *layer : object.layers())
REQUIRE(layer->regions().front()->perimeters().items_count() == 3);
}
}
}
}
SCENARIO("Print: Skirt generation", "[Print]") {
GIVEN("20mm cube and default config") {
WHEN("Skirts is set to 2 loops") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "skirt_height", 1 },
{ "skirt_distance", 1 },
{ "skirts", 2 }
});
THEN("Skirt Extrusion collection has 2 loops in it") {
REQUIRE(print.skirt().items_count() == 2);
REQUIRE(print.skirt().flatten().entities.size() == 2);
}
}
}
}
SCENARIO("Print: Changing number of solid surfaces does not cause all surfaces to become internal.", "[Print]") {
GIVEN("sliced 20mm cube and config with top_solid_surfaces = 2 and bottom_solid_surfaces = 1") {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "top_solid_layers", 2 },
{ "bottom_solid_layers", 1 },
{ "layer_height", 0.25 }, // get a known number of layers
{ "first_layer_height", 0.25 }
});
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
// Precondition: Ensure that the model has 2 solid top layers (39, 38)
// and one solid bottom layer (0).
auto test_is_solid_infill = [&print](size_t obj_id, size_t layer_id) {
const Layer &layer = *print.objects()[obj_id]->get_layer((int)layer_id);
// iterate over all of the regions in the layer
for (const LayerRegion *region : layer.regions()) {
// for each region, iterate over the fill surfaces
for (const Surface &surface : region->fill_surfaces())
CHECK(surface.is_solid());
}
};
print.process();
test_is_solid_infill(0, 0); // should be solid
test_is_solid_infill(0, 79); // should be solid
test_is_solid_infill(0, 78); // should be solid
WHEN("Model is re-sliced with top_solid_layers == 3") {
config.set("top_solid_layers", 3);
print.apply(model, config);
print.process();
THEN("Print object does not have 0 solid bottom layers.") {
test_is_solid_infill(0, 0);
}
AND_THEN("Print object has 3 top solid layers") {
test_is_solid_infill(0, 79);
test_is_solid_infill(0, 78);
test_is_solid_infill(0, 77);
}
}
}
}
SCENARIO("Print: Brim generation", "[Print]") {
GIVEN("20mm cube and default config, 1mm first layer width") {
WHEN("Brim is set to 3mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_extrusion_width", 1 },
{ "brim_width", 3 }
});
THEN("Brim Extrusion collection has 3 loops in it") {
REQUIRE(print.brim().items_count() == 3);
}
}
WHEN("Brim is set to 6mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_extrusion_width", 1 },
{ "brim_width", 6 }
});
THEN("Brim Extrusion collection has 6 loops in it") {
REQUIRE(print.brim().items_count() == 6);
}
}
WHEN("Brim is set to 6mm, extrusion width 0.5mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_extrusion_width", 1 },
{ "brim_width", 6 },
{ "first_layer_extrusion_width", 0.5 }
});
print.process();
THEN("Brim Extrusion collection has 12 loops in it") {
REQUIRE(print.brim().items_count() == 14);
}
}
}
}
SCENARIO("Ported from Perl", "[Print]") {
GIVEN("20mm cube") {
WHEN("Print center is set to 100x100 (test framework default)") {
auto config = Slic3r::DynamicPrintConfig::full_print_config();
std::string gcode = Slic3r::Test::slice({ TestMesh::cube_20x20x20 }, config);
GCodeReader parser;
Points extrusion_points;
parser.parse_buffer(gcode, [&extrusion_points](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0)
extrusion_points.emplace_back(line.new_XY_scaled(self));
});
Vec2d center = unscaled<double>(BoundingBox(extrusion_points).center());
THEN("print is centered around print_center") {
REQUIRE(is_approx(center.x(), 100.));
REQUIRE(is_approx(center.y(), 100.));
}
}
}
GIVEN("Model with multiple objects") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", { 0.4, 0.4, 0.4, 0.4 } }
});
Print print;
Model model;
Slic3r::Test::init_print({ TestMesh::cube_20x20x20 }, print, model, config);
// User sets a per-region option, also testing a deep copy of Model.
Model model2(model);
model2.objects.front()->config.set_deserialize_strict("fill_density", "100%");
WHEN("fill_density overridden") {
print.apply(model2, config);
THEN("region config inherits model object config") {
REQUIRE(print.get_print_region(0).config().fill_density == 100);
}
}
model2.objects.front()->config.erase("fill_density");
WHEN("fill_density resetted") {
print.apply(model2, config);
THEN("region config is resetted") {
REQUIRE(print.get_print_region(0).config().fill_density == 20);
}
}
WHEN("extruder is assigned") {
model2.objects.front()->config.set("extruder", 3);
model2.objects.front()->config.set("perimeter_extruder", 2);
print.apply(model2, config);
THEN("extruder setting is correctly expanded") {
REQUIRE(print.get_print_region(0).config().infill_extruder == 3);
}
THEN("extruder setting does not override explicitely specified extruders") {
REQUIRE(print.get_print_region(0).config().perimeter_extruder == 2);
}
}
}
}

View File

@@ -0,0 +1,271 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp"
#include <algorithm>
#include <boost/regex.hpp>
using namespace Slic3r;
using namespace Slic3r::Test;
boost::regex perimeters_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; perimeter");
boost::regex infill_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; infill");
boost::regex skirt_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; skirt");
SCENARIO( "PrintGCode basic functionality", "[PrintGCode]") {
GIVEN("A default configuration and a print test object") {
WHEN("the output is executed with no support material") {
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, {
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "first_layer_extrusion_width", 0 },
{ "gcode_comments", true },
{ "start_gcode", "" }
});
std::string gcode = Slic3r::Test::gcode(print);
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains slic3r version") {
REQUIRE(gcode.find(SLIC3R_VERSION) != std::string::npos);
}
//THEN("Exported text contains git commit id") {
// REQUIRE(gcode.find("; Git Commit") != std::string::npos);
// REQUIRE(gcode.find(SLIC3R_BUILD_ID) != std::string::npos);
//}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
}
THEN("Exported text does not contain cooling markers (they were consumed)") {
REQUIRE(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos);
}
THEN("GCode preamble is emitted.") {
REQUIRE(gcode.find("G21 ; set units to millimeters") != std::string::npos);
}
THEN("Config options emitted for print config, default region config, default object config") {
REQUIRE(gcode.find("; first_layer_temperature") != std::string::npos);
REQUIRE(gcode.find("; layer_height") != std::string::npos);
REQUIRE(gcode.find("; fill_density") != std::string::npos);
}
THEN("Infill is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
}
THEN("Perimeters are emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
}
THEN("Skirt is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
}
THEN("final Z height is 20mm") {
double final_z = 0.0;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
final_z = std::max<double>(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
});
REQUIRE(final_z == Approx(20.));
}
}
WHEN("output is executed with complete objects and two differently-sized meshes") {
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20,TestMesh::cube_20x20x20}, print, model, {
{ "first_layer_extrusion_width", 0 },
{ "first_layer_height", 0.3 },
{ "layer_height", 0.2 },
{ "support_material", false },
{ "raft_layers", 0 },
{ "complete_objects", true },
{ "gcode_comments", true },
{ "between_objects_gcode", "; between-object-gcode" }
});
std::string gcode = Slic3r::Test::gcode(print);
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Infill is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
}
THEN("Perimeters are emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
}
THEN("Skirt is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
}
THEN("Between-object-gcode is emitted.") {
REQUIRE(gcode.find("; between-object-gcode") != std::string::npos);
}
THEN("final Z height is 20.1mm") {
double final_z = 0.0;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
});
REQUIRE(final_z == Approx(20.1));
}
THEN("Z height resets on object change") {
double final_z = 0.0;
bool reset = false;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z, &reset] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
if (final_z > 0 && std::abs(self.z() - 0.3) < 0.01 ) { // saw higher Z before this, now it's lower
reset = true;
} else {
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
}
});
REQUIRE(reset == true);
}
THEN("Shorter object is printed before taller object.") {
double final_z = 0.0;
bool reset = false;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z, &reset] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
if (final_z > 0 && std::abs(self.z() - 0.3) < 0.01 ) {
reset = (final_z > 20.0);
} else {
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
}
});
REQUIRE(reset == true);
}
}
WHEN("the output is executed with support material") {
std::string gcode = ::Test::slice({TestMesh::cube_20x20x20}, {
{ "first_layer_extrusion_width", 0 },
{ "support_material", true },
{ "raft_layers", 3 },
{ "gcode_comments", true }
});
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") != std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
}
THEN("Raft is emitted.") {
REQUIRE(gcode.find("; raft") != std::string::npos);
}
}
WHEN("the output is executed with a separate first layer extrusion width") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "first_layer_extrusion_width", "0.5" }
});
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") != std::string::npos);
}
}
WHEN("Cooling is enabled and the fan is disabled.") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "cooling", true },
{ "disable_fan_first_layers", 5 }
});
THEN("GCode to disable fan is emitted."){
REQUIRE(gcode.find("M107") != std::string::npos);
}
}
WHEN("end_gcode exists with layer_num and layer_z") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "end_gcode", "; Layer_num [layer_num]\n; Layer_z [layer_z]" },
{ "layer_height", 0.1 },
{ "first_layer_height", 0.1 }
});
THEN("layer_num and layer_z are processed in the end gcode") {
REQUIRE(gcode.find("; Layer_num 199") != std::string::npos);
REQUIRE(gcode.find("; Layer_z 20") != std::string::npos);
}
}
WHEN("current_extruder exists in start_gcode") {
{
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "start_gcode", "; Extruder [current_extruder]" }
});
THEN("current_extruder is processed in the start gcode and set for first extruder") {
REQUIRE(gcode.find("; Extruder 0") != std::string::npos);
}
}
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_num_extruders(4);
config.set_deserialize_strict({
{ "start_gcode", "; Extruder [current_extruder]" },
{ "infill_extruder", 2 },
{ "solid_infill_extruder", 2 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 2 },
{ "support_material_interface_extruder", 2 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
THEN("current_extruder is processed in the start gcode and set for second extruder") {
REQUIRE(gcode.find("; Extruder 1") != std::string::npos);
}
}
}
WHEN("layer_num represents the layer's index from z=0") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, {
{ "complete_objects", true },
{ "gcode_comments", true },
{ "layer_gcode", ";Layer:[layer_num] ([layer_z] mm)" },
{ "layer_height", 0.1 },
{ "first_layer_height", 0.1 }
});
// End of the 1st object.
std::string token = ";Layer:199 ";
size_t pos = gcode.find(token);
THEN("First and second object last layer is emitted") {
// First object
REQUIRE(pos != std::string::npos);
pos += token.size();
REQUIRE(pos < gcode.size());
double z = 0;
REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1));
REQUIRE(z == Approx(20.));
// Second object
pos = gcode.find(";Layer:399 ", pos);
REQUIRE(pos != std::string::npos);
pos += token.size();
REQUIRE(pos < gcode.size());
REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1));
REQUIRE(z == Approx(20.));
}
}
}
}

View File

@@ -0,0 +1,89 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Print.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
GIVEN("20mm cube and default initial config, initial layer height of 2mm") {
WHEN("generate_object_layers() is called for 2mm layer heights and nozzle diameter of 3mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 2 },
{ "nozzle_diameter", 3 }
});
SpanOfConstPtrs<Layer> layers = print.objects().front()->layers();
THEN("The output vector has 10 entries") {
REQUIRE(layers.size() == 10);
}
AND_THEN("Each layer is approximately 2mm above the previous Z") {
coordf_t last = 0.0;
for (size_t i = 0; i < layers.size(); ++ i) {
REQUIRE((layers[i]->print_z - last) == Approx(2.0));
last = layers[i]->print_z;
}
}
}
WHEN("generate_object_layers() is called for 10mm layer heights and nozzle diameter of 11mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 10 },
{ "nozzle_diameter", 11 }
});
SpanOfConstPtrs<Layer> layers = print.objects().front()->layers();
THEN("The output vector has 3 entries") {
REQUIRE(layers.size() == 3);
}
AND_THEN("Layer 0 is at 2mm") {
REQUIRE(layers.front()->print_z == Approx(2.0));
}
AND_THEN("Layer 1 is at 12mm") {
REQUIRE(layers[1]->print_z == Approx(12.0));
}
}
WHEN("generate_object_layers() is called for 15mm layer heights and nozzle diameter of 16mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 15 },
{ "nozzle_diameter", 16 }
});
SpanOfConstPtrs<Layer> layers = print.objects().front()->layers();
THEN("The output vector has 2 entries") {
REQUIRE(layers.size() == 2);
}
AND_THEN("Layer 0 is at 2mm") {
REQUIRE(layers[0]->print_z == Approx(2.0));
}
AND_THEN("Layer 1 is at 17mm") {
REQUIRE(layers[1]->print_z == Approx(17.0));
}
}
#if 0
WHEN("generate_object_layers() is called for 15mm layer heights and nozzle diameter of 5mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 15 },
{ "nozzle_diameter", 5 }
});
const std::vector<Slic3r::Layer*> &layers = print.objects().front()->layers();
THEN("The layer height is limited to 5mm.") {
CHECK(layers.size() == 5);
coordf_t last = 2.0;
for (size_t i = 1; i < layers.size(); i++) {
REQUIRE((layers[i]->print_z - last) == Approx(5.0));
last = layers[i]->print_z;
}
}
}
#endif
}
}

View File

@@ -0,0 +1,405 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Shells", "[Shells]") {
GIVEN("20mm box") {
auto test = [](const DynamicPrintConfig &config){
std::vector<coord_t> zs;
std::set<coord_t> layers_with_solid_infill;
std::set<coord_t> layers_with_bridge_infill;
const double solid_infill_speed = config.opt_float("solid_infill_speed") * 60;
const double bridge_speed = config.opt_float("bridge_speed") * 60;
GCodeReader parser;
parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
[&zs, &layers_with_solid_infill, &layers_with_bridge_infill, solid_infill_speed, bridge_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
double z = line.new_Z(self);
REQUIRE(z >= 0);
if (z > 0) {
coord_t scaled_z = scaled<float>(z);
zs.emplace_back(scaled_z);
if (line.extruding(self) && line.dist_XY(self) > 0) {
double f = line.new_F(self);
if (std::abs(f - solid_infill_speed) < EPSILON)
layers_with_solid_infill.insert(scaled_z);
if (std::abs(f - bridge_speed) < EPSILON)
layers_with_bridge_infill.insert(scaled_z);
}
}
});
sort_remove_duplicates(zs);
auto has_solid_infill = [&layers_with_solid_infill](coord_t z) { return layers_with_solid_infill.find(z) != layers_with_solid_infill.end(); };
auto has_bridge_infill = [&layers_with_bridge_infill](coord_t z) { return layers_with_bridge_infill.find(z) != layers_with_bridge_infill.end(); };
auto has_shells = [&has_solid_infill, &has_bridge_infill, &zs](int layer_idx) { coord_t z = zs[layer_idx]; return has_solid_infill(z) || has_bridge_infill(z); };
const int bottom_solid_layers = config.opt_int("bottom_solid_layers");
const int top_solid_layers = config.opt_int("top_solid_layers");
THEN("correct number of bottom solid layers") {
for (int i = 0; i < bottom_solid_layers; ++ i)
REQUIRE(has_shells(i));
for (int i = bottom_solid_layers; i < int(zs.size() / 2); ++ i)
REQUIRE(! has_shells(i));
}
THEN("correct number of top solid layers") {
// NOTE: there is one additional layer with enusring line under the bridge layer, bridges would be otherwise anchored weakly to the perimeter.
size_t additional_ensuring_anchors = top_solid_layers > 0 ? 1 : 0;
for (int i = 0; i < top_solid_layers + additional_ensuring_anchors; ++ i)
REQUIRE(has_shells(int(zs.size()) - i - 1));
for (int i = top_solid_layers + additional_ensuring_anchors; i < int(zs.size() / 2); ++ i)
REQUIRE(! has_shells(int(zs.size()) - i - 1));
}
if (top_solid_layers > 0) {
THEN("solid infill speed is used on solid infill") {
for (int i = 0; i < top_solid_layers - 1; ++ i) {
auto z = zs[int(zs.size()) - i - 1];
REQUIRE(has_solid_infill(z));
REQUIRE(! has_bridge_infill(z));
}
}
THEN("bridge used in first solid layer over sparse infill") {
auto z = zs[int(zs.size()) - top_solid_layers];
REQUIRE(! has_solid_infill(z));
REQUIRE(has_bridge_infill(z));
}
}
};
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeters", 0 },
{ "solid_infill_speed", 99 },
{ "top_solid_infill_speed", 99 },
{ "bridge_speed", 72 },
{ "first_layer_speed", "100%" },
{ "cooling", "0" }
});
WHEN("three top and bottom layers") {
// proper number of shells is applied
config.set_deserialize_strict({
{ "top_solid_layers", 3 },
{ "bottom_solid_layers", 3 }
});
test(config);
}
WHEN("zero top and bottom layers") {
// no shells are applied when both top and bottom are set to zero
config.set_deserialize_strict({
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 }
});
test(config);
}
WHEN("three top and bottom layers, zero infill") {
// proper number of shells is applied even when fill density is none
config.set_deserialize_strict({
{ "perimeters", 1 },
{ "top_solid_layers", 3 },
{ "bottom_solid_layers", 3 }
});
test(config);
}
}
}
static std::set<double> layers_with_speed(const std::string &gcode, int speed)
{
std::set<double> out;
GCodeReader parser;
parser.parse_buffer(gcode, [&out, speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.extruding(self) && is_approx<double>(line.new_F(self), speed * 60.))
out.insert(self.z());
});
return out;
}
SCENARIO("Shells (from Perl)", "[Shells]") {
GIVEN("V shape, Slic3r GH #1161") {
int solid_speed = 99;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "layer_height", 0.3 },
{ "first_layer_height", 0.3 },
{ "bottom_solid_layers", 0 },
{ "top_solid_layers", 3 },
// to prevent speeds from being altered
{ "cooling", "0" },
{ "bridge_speed", solid_speed },
{ "solid_infill_speed", solid_speed },
{ "top_solid_infill_speed", solid_speed },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" },
// prevent speed alteration
{ "enable_dynamic_overhang_speeds", 0 }
});
THEN("correct number of top solid shells is generated in V-shaped object") {
size_t n = 0;
for (auto z : layers_with_speed(Slic3r::Test::slice({TestMesh::V}, config), solid_speed))
if (z <= 7.2)
++ n;
REQUIRE(n == 3 + 1/*one additional layer with ensuring for bridge anchors*/);
}
}
//TODO CHECK AFTER REMOVAL OF "ensure_vertical_wall_thickness"
// GIVEN("V shape") {
// // we need to check against one perimeter because this test is calibrated
// // (shape, extrusion_width) so that perimeters cover the bottom surfaces of
// // their lower layer - the test checks that shells are not generated on the
// // above layers (thus 'across' the shadow perimeter)
// // the test is actually calibrated to leave a narrow bottom region for each
// // layer - we test that in case of fill_density = 0 such narrow shells are
// // discarded instead of grown
// int bottom_solid_layers = 3;
// auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
// { "perimeters", 1 },
// { "fill_density", 0 },
// // to prevent speeds from being altered
// { "cooling", "0" },
// // to prevent speeds from being altered
// { "first_layer_speed", "100%" },
// // prevent speed alteration
// { "enable_dynamic_overhang_speeds", 0 },
// { "layer_height", 0.4 },
// { "first_layer_height", 0.4 },
// { "extrusion_width", 0.55 },
// { "bottom_solid_layers", bottom_solid_layers },
// { "top_solid_layers", 0 },
// { "solid_infill_speed", 99 }
// });
// THEN("shells are not propagated across perimeters of the neighbor layer") {
// std::string gcode = Slic3r::Test::slice({TestMesh::V}, config);
// REQUIRE(layers_with_speed(gcode, 99).size() == bottom_solid_layers);
// }
// }
// GIVEN("sloping_hole") {
// int bottom_solid_layers = 3;
// int top_solid_layers = 3;
// int solid_speed = 99;
// auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
// { "perimeters", 3 },
// // to prevent speeds from being altered
// { "cooling", "0" },
// // to prevent speeds from being altered
// { "first_layer_speed", "100%" },
// // prevent speed alteration
// { "enable_dynamic_overhang_speeds", 0 },
// { "layer_height", 0.4 },
// { "first_layer_height", 0.4 },
// { "bottom_solid_layers", bottom_solid_layers },
// { "top_solid_layers", top_solid_layers },
// { "solid_infill_speed", solid_speed },
// { "top_solid_infill_speed", solid_speed },
// { "bridge_speed", solid_speed },
// { "filament_diameter", 3. },
// { "nozzle_diameter", 0.5 }
// });
// THEN("no superfluous shells are generated") {
// std::string gcode = Slic3r::Test::slice({TestMesh::sloping_hole}, config);
// REQUIRE(layers_with_speed(gcode, solid_speed).size() == bottom_solid_layers + top_solid_layers);
// }
// }
GIVEN("20mm_cube, spiral vase") {
double layer_height = 0.3;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "layer_height", layer_height },
{ "first_layer_height", layer_height },
{ "top_solid_layers", 0 },
{ "spiral_vase", 1 },
{ "bottom_solid_layers", 0 },
{ "skirts", 0 },
{ "start_gcode", "" },
{ "temperature", 200 },
{ "first_layer_temperature", 205}
});
// TODO: this needs to be tested with a model with sloping edges, where starting
// points of each layer are not aligned - in that case we would test that no
// travel moves are left to move to the new starting point - in a cube, end
// points coincide with next layer starting points (provided there's no clipping)
auto test = [layer_height](const DynamicPrintConfig &config) {
size_t travel_moves_after_first_extrusion = 0;
bool started_extruding = false;
bool first_layer_temperature_set = false;
bool temperature_set = false;
std::vector<double> z_steps;
GCodeReader parser;
parser.parse_buffer(Slic3r::Test::slice({TestMesh::cube_20x20x20}, config),
[&](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("G1")) {
if (line.extruding(self))
started_extruding = true;
if (started_extruding) {
if (double dz = line.dist_Z(self); dz > 0)
z_steps.emplace_back(dz);
if (line.travel() && line.dist_XY(self) > 0 && ! line.has(Z))
++ travel_moves_after_first_extrusion;
}
} else if (line.cmd_is("M104")) {
int s;
if (line.has_value('S', s)) {
if (s == 205)
first_layer_temperature_set = true;
else if (s == 200)
temperature_set = true;
}
}
});
THEN("first layer temperature is set") {
REQUIRE(first_layer_temperature_set);
}
THEN("temperature is set") {
REQUIRE(temperature_set);
}
// we allow one travel move after first extrusion: i.e. when moving to the first
// spiral point after moving to second layer (bottom layer had loop clipping, so
// we're slightly distant from the starting point of the loop)
THEN("no gaps in spiral vase") {
REQUIRE(travel_moves_after_first_extrusion <= 1);
}
THEN("no gaps in Z") {
REQUIRE(std::count_if(z_steps.begin(), z_steps.end(),
[&layer_height](auto z_step) { return z_step > layer_height + EPSILON; }) == 0);
}
};
WHEN("solid model") {
test(config);
}
WHEN("solid model with negative z-offset") {
config.set_deserialize_strict("z_offset", "-10");
test(config);
}
// Disabled because the current unreliable medial axis code doesn't always produce valid loops.
// $test->('40x10', 'hollow model with negative z-offset');
}
GIVEN("20mm_cube, spiral vase") {
double layer_height = 0.4;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "spiral_vase", 1 },
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
{ "retract_layer_change", 0 },
{ "skirts", 0 },
{ "layer_height", layer_height },
{ "first_layer_height", layer_height },
{ "start_gcode", "" },
// { "use_relative_e_distances", 1}
});
config.validate();
std::vector<std::pair<double, double>> this_layer; // [ dist_Z, dist_XY ], ...
int z_moves = 0;
bool bottom_layer_not_flat = false;
bool null_z_moves_not_layer_changes = false;
bool null_z_moves_not_multiples_of_layer_height = false;
bool sum_of_partial_z_equals_to_layer_height = false;
bool all_layer_segments_have_same_slope = false;
bool horizontal_extrusions = false;
GCodeReader parser;
parser.parse_buffer(Slic3r::Test::slice({TestMesh::cube_20x20x20}, config),
[&](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("G1")) {
if (z_moves < 2) {
// skip everything up to the second Z move
// (i.e. start of second layer)
if (line.has(Z)) {
++ z_moves;
if (double dz = line.dist_Z(self); dz > 0 && ! is_approx<double>(dz, layer_height))
bottom_layer_not_flat = true;
}
} else if (line.dist_Z(self) == 0 && line.has(Z)) {
if (line.dist_XY(self) != 0)
null_z_moves_not_layer_changes = true;
double z = line.new_Z(self);
if (fmod(z + EPSILON, layer_height) > 2 * EPSILON)
null_z_moves_not_multiples_of_layer_height = true;
double total_dist_XY = 0;
double total_dist_Z = 0;
for (auto &seg : this_layer) {
total_dist_Z += seg.first;
total_dist_XY += seg.second;
}
if (std::abs(total_dist_Z - layer_height) >
// The first segment on the 2nd layer has extrusion interpolated from zero
// and the 1st segment has such a low extrusion assigned, that it is effectively zero, thus the move
// is considered non-extruding and a higher epsilon is required.
(z_moves == 2 ? 0.0021 : EPSILON))
sum_of_partial_z_equals_to_layer_height = true;
//printf("Total height: %f, layer height: %f, good: %d\n", sum(map $_->[0], @this_layer), $config->layer_height, $sum_of_partial_z_equals_to_layer_height);
for (auto &seg : this_layer)
// check that segment's dist_Z is proportioned to its dist_XY
if (std::abs(seg.first * total_dist_XY / layer_height - seg.second) > 0.2)
all_layer_segments_have_same_slope = true;
this_layer.clear();
} else if (line.extruding(self) && line.dist_XY(self) > 0) {
if (line.dist_Z(self) == 0)
horizontal_extrusions = true;
//printf("Pushing dist_z: %f, dist_xy: %f\n", $info->{dist_Z}, $info->{dist_XY});
this_layer.emplace_back(line.dist_Z(self), line.dist_XY(self));
}
}
});
THEN("bottom layer is flat when using spiral vase") {
REQUIRE(! bottom_layer_not_flat);
}
THEN("null Z moves are layer changes") {
REQUIRE(! null_z_moves_not_layer_changes);
}
THEN("null Z moves are multiples of layer height") {
REQUIRE(! null_z_moves_not_multiples_of_layer_height);
}
THEN("sum of partial Z increments equals to a full layer height") {
REQUIRE(! sum_of_partial_z_equals_to_layer_height);
}
THEN("all layer segments have the same slope") {
REQUIRE(! all_layer_segments_have_same_slope);
}
THEN("no horizontal extrusions") {
REQUIRE(! horizontal_extrusions);
}
}
}
#if 0
// The current Spiral Vase slicing code removes the holes and all but the largest contours from each slice,
// therefore the following test is no more valid.
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('perimeters', 1);
$config->set('fill_density', 0);
$config->set('top_solid_layers', 0);
$config->set('spiral_vase', 1);
$config->set('bottom_solid_layers', 0);
$config->set('skirts', 0);
$config->set('first_layer_height', $config->layer_height);
$config->set('start_gcode', '');
my $print = Slic3r::Test::init_print('two_hollow_squares', config => $config);
my $diagonal_moves = 0;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd eq 'G1') {
if ($info->{extruding} && $info->{dist_XY} > 0) {
if ($info->{dist_Z} > 0) {
$diagonal_moves++;
}
}
}
});
is $diagonal_moves, 0, 'no spiral moves on two-island object';
}
#endif

View File

@@ -0,0 +1,268 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include <boost/algorithm/string.hpp>
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
/// Helper method to find the tool used for the brim (always the first extrusion)
static int get_brim_tool(const std::string &gcode)
{
int brim_tool = -1;
int tool = -1;
GCodeReader parser;
parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0 && brim_tool < 0) {
brim_tool = tool;
}
});
return brim_tool;
}
TEST_CASE("Skirt height is honored", "[Skirt]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirts", 1 },
{ "skirt_height", 5 },
{ "perimeters", 0 },
{ "support_material_speed", 99 },
// avoid altering speeds unexpectedly
{ "cooling", false },
// avoid altering speeds unexpectedly
{ "first_layer_speed", "100%" }
});
std::string gcode;
SECTION("printing a single object") {
gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
}
SECTION("printing multiple objects") {
gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20, TestMesh::cube_20x20x20}, config);
}
std::map<double, bool> layers_with_skirt;
double support_speed = config.opt<Slic3r::ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
GCodeReader parser;
parser.parse_buffer(gcode, [&layers_with_skirt, &support_speed] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.extruding(self) && self.f() == Approx(support_speed)) {
layers_with_skirt[self.z()] = 1;
}
});
REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height"));
}
SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") {
GIVEN("A default configuration") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_num_extruders(4);
config.set_deserialize_strict({
{ "support_material_speed", 99 },
{ "first_layer_height", 0.3 },
{ "gcode_comments", true },
// avoid altering speeds unexpectedly
{ "cooling", false },
{ "first_layer_speed", "100%" },
// remove noise from top/solid layers
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 1 },
{ "start_gcode", "T[initial_tool]\n" }
});
WHEN("Brim width is set to 5") {
config.set_deserialize_strict({
{ "perimeters", 0 },
{ "skirts", 0 },
{ "brim_width", 5 }
});
THEN("Brim is generated") {
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
bool brim_generated = false;
double support_speed = config.opt<Slic3r::ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
Slic3r::GCodeReader parser;
parser.parse_buffer(gcode, [&brim_generated, support_speed] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line) {
if (self.z() == Approx(0.3) || line.new_Z(self) == Approx(0.3)) {
if (line.extruding(self) && self.f() == Approx(support_speed)) {
brim_generated = true;
}
}
});
REQUIRE(brim_generated);
}
}
WHEN("Skirt area is smaller than the brim") {
config.set_deserialize_strict({
{ "skirts", 1 },
{ "brim_width", 10}
});
THEN("Gcode generates") {
REQUIRE(! Slic3r::Test::slice({TestMesh::cube_20x20x20}, config).empty());
}
}
WHEN("Skirt height is 0 and skirts > 0") {
config.set_deserialize_strict({
{ "skirts", 2 },
{ "skirt_height", 0 }
});
THEN("Gcode generates") {
REQUIRE(! Slic3r::Test::slice({TestMesh::cube_20x20x20}, config).empty());
}
}
#if 0
// This is a real error! One shall print the brim with the external perimeter extruder!
WHEN("Perimeter extruder = 2 and support extruders = 3") {
THEN("Brim is printed with the extruder used for the perimeters of first object") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "brim_width", 5 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 3 },
{ "infill_extruder", 4 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
int tool = get_brim_tool(gcode);
REQUIRE(tool == config.opt_int("perimeter_extruder") - 1);
}
}
WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") {
THEN("brim is printed with same extruder as skirt") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "brim_width", 5 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 3 },
{ "infill_extruder", 4 },
{ "raft_layers", 1 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
int tool = get_brim_tool(gcode);
REQUIRE(tool == config.opt_int("support_material_extruder") - 1);
}
}
#endif
WHEN("brim width to 1 with layer_width of 0.5") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 }
});
THEN("2 brim lines") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config);
REQUIRE(print.brim().entities.size() == 2);
}
}
#if 0
WHEN("brim ears on a square") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 },
{ "brim_ears", 1 },
{ "brim_ears_max_angle", 91 }
});
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config);
THEN("Four brim ears") {
REQUIRE(print.brim().entities.size() == 4);
}
}
WHEN("brim ears on a square but with a too small max angle") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 },
{ "brim_ears", 1 },
{ "brim_ears_max_angle", 89 }
});
THEN("no brim") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ TestMesh::cube_20x20x20 }, print, config);
REQUIRE(print.brim().entities.size() == 0);
}
}
#endif
WHEN("Object is plated with overhang support and a brim") {
config.set_deserialize_strict({
{ "layer_height", 0.4 },
{ "first_layer_height", 0.4 },
{ "skirts", 1 },
{ "skirt_distance", 0 },
{ "support_material_speed", 99 },
{ "perimeter_extruder", 1 },
{ "support_material_extruder", 2 },
{ "infill_extruder", 3 }, // ensure that a tool command gets emitted.
{ "cooling", false }, // to prevent speeds to be altered
{ "first_layer_speed", "100%" }, // to prevent speeds to be altered
{ "start_gcode", "T[initial_tool]\n" }
});
THEN("overhang generates?") {
//FIXME does it make sense?
REQUIRE(! Slic3r::Test::slice({TestMesh::overhang}, config).empty());
}
// config.set("support_material", true); // to prevent speeds to be altered
#if 0
// This test is not finished.
THEN("skirt length is large enough to contain object with support") {
CHECK(config.opt_bool("support_material")); // test is not valid if support material is off
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
double support_speed = config.opt<ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
double skirt_length = 0.0;
Points extrusion_points;
int tool = -1;
GCodeReader parser;
parser.parse_buffer(gcode, [config, &extrusion_points, &tool, &skirt_length, support_speed] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line) {
// std::cerr << line.cmd() << "\n";
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (self.z() == Approx(config.opt<ConfigOptionFloat>("first_layer_height")->value)) {
// on first layer
if (line.extruding(self) && line.dist_XY(self) > 0) {
float speed = ( self.f() > 0 ? self.f() : line.new_F(self));
// std::cerr << "Tool " << tool << "\n";
if (speed == Approx(support_speed) && tool == config.opt_int("perimeter_extruder") - 1) {
// Skirt uses first material extruder, support material speed.
skirt_length += line.dist_XY(self);
} else
extrusion_points.push_back(Slic3r::Point::new_scale(line.new_X(self), line.new_Y(self)));
}
}
if (self.z() == Approx(0.3) || line.new_Z(self) == Approx(0.3)) {
if (line.extruding(self) && self.f() == Approx(support_speed)) {
}
}
});
Slic3r::Polygon convex_hull = Slic3r::Geometry::convex_hull(extrusion_points);
double hull_perimeter = unscale<double>(convex_hull.split_at_first_point().length());
REQUIRE(skirt_length > hull_perimeter);
}
#endif
}
WHEN("Large minimum skirt length is used.") {
config.set("min_skirt_length", 20);
THEN("Gcode generation doesn't crash") {
REQUIRE(! Slic3r::Test::slice({TestMesh::cube_20x20x20}, config).empty());
}
}
}
}

View File

@@ -0,0 +1,497 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
TEST_CASE("SupportMaterial: Three raft layers created", "[SupportMaterial]")
{
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ TestMesh::cube_20x20x20 }, print, {
{ "support_material", 1 },
{ "raft_layers", 3 }
});
REQUIRE(print.objects().front()->support_layers().size() == 3);
}
SCENARIO("SupportMaterial: support_layers_z and contact_distance", "[SupportMaterial]")
{
// Box h = 20mm, hole bottom at 5mm, hole height 10mm (top edge at 15mm).
TriangleMesh mesh = Slic3r::Test::mesh(Slic3r::Test::TestMesh::cube_with_hole);
mesh.rotate_x(float(M_PI / 2));
// mesh.write_binary("d:\\temp\\cube_with_hole.stl");
auto check = [](Slic3r::Print &print, bool &first_support_layer_height_ok, bool &layer_height_minimum_ok, bool &layer_height_maximum_ok, bool &top_spacing_ok)
{
SpanOfConstPtrs<SupportLayer> support_layers = print.objects().front()->support_layers();
first_support_layer_height_ok = support_layers.front()->print_z == print.config().first_layer_height.value;
layer_height_minimum_ok = true;
layer_height_maximum_ok = true;
double min_layer_height = print.config().min_layer_height.values.front();
double max_layer_height = print.config().nozzle_diameter.values.front();
if (print.config().max_layer_height.values.front() > EPSILON)
max_layer_height = std::min(max_layer_height, print.config().max_layer_height.values.front());
for (size_t i = 1; i < support_layers.size(); ++ i) {
if (support_layers[i]->print_z - support_layers[i - 1]->print_z < min_layer_height - EPSILON)
layer_height_minimum_ok = false;
if (support_layers[i]->print_z - support_layers[i - 1]->print_z > max_layer_height + EPSILON)
layer_height_maximum_ok = false;
}
#if 0
double expected_top_spacing = print.default_object_config().layer_height + print.config().nozzle_diameter.get_at(0);
bool wrong_top_spacing = 0;
std::vector<coordf_t> top_z { 1.1 };
for (coordf_t top_z_el : top_z) {
// find layer index of this top surface.
size_t layer_id = -1;
for (size_t i = 0; i < support_z.size(); ++ i) {
if (abs(support_z[i] - top_z_el) < EPSILON) {
layer_id = i;
i = static_cast<int>(support_z.size());
}
}
// check that first support layer above this top surface (or the next one) is spaced with nozzle diameter
if (abs(support_z[layer_id + 1] - support_z[layer_id] - expected_top_spacing) > EPSILON &&
abs(support_z[layer_id + 2] - support_z[layer_id] - expected_top_spacing) > EPSILON) {
wrong_top_spacing = 1;
}
}
d = ! wrong_top_spacing;
#else
top_spacing_ok = true;
#endif
};
GIVEN("A print object having one modelObject") {
WHEN("First layer height = 0.4") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ mesh }, print, {
{ "support_material", 1 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.4 },
{ "dont_support_bridges", false },
});
bool a, b, c, d;
check(print, a, b, c, d);
THEN("First layer height is honored") { REQUIRE(a == true); }
THEN("No null or negative support layers") { REQUIRE(b == true); }
THEN("No layers thicker than nozzle diameter") { REQUIRE(c == true); }
// THEN("Layers above top surfaces are spaced correctly") { REQUIRE(d == true); }
}
WHEN("Layer height = 0.2 and, first layer height = 0.3") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ mesh }, print, {
{ "support_material", 1 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.3 },
{ "dont_support_bridges", false },
});
bool a, b, c, d;
check(print, a, b, c, d);
THEN("First layer height is honored") { REQUIRE(a == true); }
THEN("No null or negative support layers") { REQUIRE(b == true); }
THEN("No layers thicker than nozzle diameter") { REQUIRE(c == true); }
// THEN("Layers above top surfaces are spaced correctly") { REQUIRE(d == true); }
}
WHEN("Layer height = nozzle_diameter[0]") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ mesh }, print, {
{ "support_material", 1 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.3 },
{ "dont_support_bridges", false },
});
bool a, b, c, d;
check(print, a, b, c, d);
THEN("First layer height is honored") { REQUIRE(a == true); }
THEN("No null or negative support layers") { REQUIRE(b == true); }
THEN("No layers thicker than nozzle diameter") { REQUIRE(c == true); }
// THEN("Layers above top surfaces are spaced correctly") { REQUIRE(d == true); }
}
}
}
#if 0
// Test 8.
TEST_CASE("SupportMaterial: forced support is generated", "[SupportMaterial]")
{
// Create a mesh & modelObject.
TriangleMesh mesh = TriangleMesh::make_cube(20, 20, 20);
Model model = Model();
ModelObject *object = model.add_object();
object->add_volume(mesh);
model.add_default_instances();
model.align_instances_to_origin();
Print print = Print();
std::vector<coordf_t> contact_z = {1.9};
std::vector<coordf_t> top_z = {1.1};
print.default_object_config.support_material_enforce_layers = 100;
print.default_object_config.support_material = 0;
print.default_object_config.layer_height = 0.2;
print.default_object_config.set_deserialize("first_layer_height", "0.3");
print.add_model_object(model.objects[0]);
print.objects.front()->_slice();
SupportMaterial *support = print.objects.front()->_support_material();
auto support_z = support->support_layers_z(contact_z, top_z, print.default_object_config.layer_height);
bool check = true;
for (size_t i = 1; i < support_z.size(); i++) {
if (support_z[i] - support_z[i - 1] <= 0)
check = false;
}
REQUIRE(check == true);
}
// TODO
bool test_6_checks(Print& print)
{
bool has_bridge_speed = true;
// Pre-Processing.
PrintObject* print_object = print.objects.front();
print_object->infill();
SupportMaterial* support_material = print.objects.front()->_support_material();
support_material->generate(print_object);
// TODO but not needed in test 6 (make brims and make skirts).
// Exporting gcode.
// TODO validation found in Simple.pm
return has_bridge_speed;
}
// Test 6.
SCENARIO("SupportMaterial: Checking bridge speed", "[SupportMaterial]")
{
GIVEN("Print object") {
// Create a mesh & modelObject.
TriangleMesh mesh = TriangleMesh::make_cube(20, 20, 20);
Model model = Model();
ModelObject *object = model.add_object();
object->add_volume(mesh);
model.add_default_instances();
model.align_instances_to_origin();
Print print = Print();
print.config.brim_width = 0;
print.config.skirts = 0;
print.config.skirts = 0;
print.default_object_config.support_material = 1;
print.default_region_config.top_solid_layers = 0; // so that we don't have the internal bridge over infill.
print.default_region_config.bridge_speed = 99;
print.config.cooling = 0;
print.config.set_deserialize("first_layer_speed", "100%");
WHEN("support_material_contact_distance = 0.2") {
print.default_object_config.support_material_contact_distance = 0.2;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is used.
}
WHEN("support_material_contact_distance = 0") {
print.default_object_config.support_material_contact_distance = 0;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is not used.
}
WHEN("support_material_contact_distance = 0.2 & raft_layers = 5") {
print.default_object_config.support_material_contact_distance = 0.2;
print.default_object_config.raft_layers = 5;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is used.
}
WHEN("support_material_contact_distance = 0 & raft_layers = 5") {
print.default_object_config.support_material_contact_distance = 0;
print.default_object_config.raft_layers = 5;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is not used.
}
}
}
#endif
/*
Old Perl tests, which were disabled by Vojtech at the time of first Support Generator refactoring.
#if 0
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('support_material', 1);
my @contact_z = my @top_z = ();
my $test = sub {
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $object_config = $print->print->objects->[0]->config;
my $flow = Slic3r::Flow->new_from_width(
width => $object_config->support_material_extrusion_width || $object_config->extrusion_width,
role => FLOW_ROLE_SUPPORT_MATERIAL,
nozzle_diameter => $print->config->nozzle_diameter->[$object_config->support_material_extruder-1] // $print->config->nozzle_diameter->[0],
layer_height => $object_config->layer_height,
);
my $support = Slic3r::Print::SupportMaterial->new(
object_config => $print->print->objects->[0]->config,
print_config => $print->print->config,
flow => $flow,
interface_flow => $flow,
first_layer_flow => $flow,
);
my $support_z = $support->support_layers_z($print->print->objects->[0], \@contact_z, \@top_z, $config->layer_height);
my $expected_top_spacing = $support->contact_distance($config->layer_height, $config->nozzle_diameter->[0]);
is $support_z->[0], $config->first_layer_height,
'first layer height is honored';
is scalar(grep { $support_z->[$_]-$support_z->[$_-1] <= 0 } 1..$#$support_z), 0,
'no null or negative support layers';
is scalar(grep { $support_z->[$_]-$support_z->[$_-1] > $config->nozzle_diameter->[0] + epsilon } 1..$#$support_z), 0,
'no layers thicker than nozzle diameter';
my $wrong_top_spacing = 0;
foreach my $top_z (@top_z) {
# find layer index of this top surface
my $layer_id = first { abs($support_z->[$_] - $top_z) < epsilon } 0..$#$support_z;
# check that first support layer above this top surface (or the next one) is spaced with nozzle diameter
$wrong_top_spacing = 1
if ($support_z->[$layer_id+1] - $support_z->[$layer_id]) != $expected_top_spacing
&& ($support_z->[$layer_id+2] - $support_z->[$layer_id]) != $expected_top_spacing;
}
ok !$wrong_top_spacing, 'layers above top surfaces are spaced correctly';
};
$config->set('layer_height', 0.2);
$config->set('first_layer_height', 0.3);
@contact_z = (1.9);
@top_z = (1.1);
$test->();
$config->set('first_layer_height', 0.4);
$test->();
$config->set('layer_height', $config->nozzle_diameter->[0]);
$test->();
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('raft_layers', 3);
$config->set('brim_width', 0);
$config->set('skirts', 0);
$config->set('support_material_extruder', 2);
$config->set('support_material_interface_extruder', 2);
$config->set('layer_height', 0.4);
$config->set('first_layer_height', 0.4);
my $print = Slic3r::Test::init_print('overhang', config => $config);
ok my $gcode = Slic3r::Test::gcode($print), 'no conflict between raft/support and brim';
my $tool = 0;
Slic3r::GCode::Reader->new->parse($gcode, sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($info->{extruding}) {
if ($self->Z <= ($config->raft_layers * $config->layer_height)) {
fail 'not extruding raft with support material extruder'
if $tool != ($config->support_material_extruder-1);
} else {
fail 'support material exceeds raft layers'
if $tool == $config->support_material_extruder-1;
# TODO: we should test that full support is generated when we use raft too
}
}
});
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('skirts', 0);
$config->set('raft_layers', 3);
$config->set('support_material_pattern', 'honeycomb');
$config->set('support_material_extrusion_width', 0.6);
$config->set('first_layer_extrusion_width', '100%');
$config->set('bridge_speed', 99);
$config->set('cooling', [ 0 ]); # prevent speed alteration
$config->set('first_layer_speed', '100%'); # prevent speed alteration
$config->set('start_gcode', ''); # prevent any unexpected Z move
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $layer_id = -1; # so that first Z move sets this to 0
my @raft = my @first_object_layer = ();
my %first_object_layer_speeds = (); # F => 1
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($info->{extruding} && $info->{dist_XY} > 0) {
if ($layer_id <= $config->raft_layers) {
# this is a raft layer or the first object layer
my $line = Slic3r::Line->new_scale([ $self->X, $self->Y ], [ $info->{new_X}, $info->{new_Y} ]);
my @path = @{$line->grow(scale($config->support_material_extrusion_width/2))};
if ($layer_id < $config->raft_layers) {
# this is a raft layer
push @raft, @path;
} else {
push @first_object_layer, @path;
$first_object_layer_speeds{ $args->{F} // $self->F } = 1;
}
}
} elsif ($cmd eq 'G1' && $info->{dist_Z} > 0) {
$layer_id++;
}
});
ok !@{diff(\@first_object_layer, \@raft)},
'first object layer is completely supported by raft';
is scalar(keys %first_object_layer_speeds), 1,
'only one speed used in first object layer';
ok +(keys %first_object_layer_speeds)[0] == $config->bridge_speed*60,
'bridge speed used in first object layer';
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('skirts', 0);
$config->set('layer_height', 0.35);
$config->set('first_layer_height', 0.3);
$config->set('nozzle_diameter', [0.5]);
$config->set('support_material_extruder', 2);
$config->set('support_material_interface_extruder', 2);
my $test = sub {
my ($raft_layers) = @_;
$config->set('raft_layers', $raft_layers);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my %raft_z = (); # z => 1
my $tool = undef;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($info->{extruding} && $info->{dist_XY} > 0) {
if ($tool == $config->support_material_extruder-1) {
$raft_z{$self->Z} = 1;
}
}
});
is scalar(keys %raft_z), $config->raft_layers, 'correct number of raft layers is generated';
};
$test->(2);
$test->(70);
$config->set('layer_height', 0.4);
$config->set('first_layer_height', 0.35);
$test->(3);
$test->(70);
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('brim_width', 0);
$config->set('skirts', 0);
$config->set('support_material', 1);
$config->set('top_solid_layers', 0); # so that we don't have the internal bridge over infill
$config->set('bridge_speed', 99);
$config->set('cooling', [ 0 ]);
$config->set('first_layer_speed', '100%');
my $test = sub {
my $print = Slic3r::Test::init_print('overhang', config => $config);
my $has_bridge_speed = 0;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($info->{extruding}) {
if (($args->{F} // $self->F) == $config->bridge_speed*60) {
$has_bridge_speed = 1;
}
}
});
return $has_bridge_speed;
};
$config->set('support_material_contact_distance', 0.2);
ok $test->(), 'bridge speed is used when support_material_contact_distance > 0';
$config->set('support_material_contact_distance', 0);
ok !$test->(), 'bridge speed is not used when support_material_contact_distance == 0';
$config->set('raft_layers', 5);
$config->set('support_material_contact_distance', 0.2);
ok $test->(), 'bridge speed is used when raft_layers > 0 and support_material_contact_distance > 0';
$config->set('support_material_contact_distance', 0);
ok !$test->(), 'bridge speed is not used when raft_layers > 0 and support_material_contact_distance == 0';
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('skirts', 0);
$config->set('start_gcode', '');
$config->set('raft_layers', 8);
$config->set('nozzle_diameter', [0.4, 1]);
$config->set('layer_height', 0.1);
$config->set('first_layer_height', 0.8);
$config->set('support_material_extruder', 2);
$config->set('support_material_interface_extruder', 2);
$config->set('support_material_contact_distance', 0);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
ok my $gcode = Slic3r::Test::gcode($print), 'first_layer_height is validated with support material extruder nozzle diameter when using raft layers';
my $tool = undef;
my @z = (0);
my %layer_heights_by_tool = (); # tool => [ lh, lh... ]
Slic3r::GCode::Reader->new->parse($gcode, sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($cmd eq 'G1' && exists $args->{Z} && $args->{Z} != $self->Z) {
push @z, $args->{Z};
} elsif ($info->{extruding} && $info->{dist_XY} > 0) {
$layer_heights_by_tool{$tool} ||= [];
push @{ $layer_heights_by_tool{$tool} }, $z[-1] - $z[-2];
}
});
ok !defined(first { $_ > $config->nozzle_diameter->[0] + epsilon }
@{ $layer_heights_by_tool{$config->perimeter_extruder-1} }),
'no object layer is thicker than nozzle diameter';
ok !defined(first { abs($_ - $config->layer_height) < epsilon }
@{ $layer_heights_by_tool{$config->support_material_extruder-1} }),
'no support material layer is as thin as object layers';
}
*/

View File

@@ -0,0 +1,191 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "test_data.hpp" // get access to init_print, etc
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
SCENARIO("Medial Axis", "[ThinWalls]") {
GIVEN("Square with hole") {
auto square = Polygon::new_scale({ {100, 100}, {200, 100}, {200, 200}, {100, 200} });
auto hole_in_square = Polygon::new_scale({ {140, 140}, {140, 160}, {160, 160}, {160, 140} });
ExPolygon expolygon{ square, hole_in_square };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(0.5), scaled<double>(40.));
THEN("medial axis of a square shape is a single path") {
REQUIRE(res.size() == 1);
}
THEN("polyline forms a closed loop") {
REQUIRE(res.front().first_point() == res.front().last_point());
}
THEN("medial axis loop has reasonable length") {
REQUIRE(res.front().length() > hole_in_square.length());
REQUIRE(res.front().length() < square.length());
}
}
}
GIVEN("narrow rectangle") {
ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {120, 200}, {100, 200} }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(0.5), scaled<double>(20.));
THEN("medial axis of a narrow rectangle is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
}
}
#if 0
//FIXME this test never worked
GIVEN("narrow rectangle with an extra vertex") {
ExPolygon expolygon{ Polygon::new_scale({
{100, 100}, {120, 100}, {120, 200},
{105, 200} /* extra point in the short side*/,
{100, 200}
})};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(0.5), scaled<double>(1.));
THEN("medial axis of a narrow rectangle with an extra vertex is still a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has still a reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
THEN("extra vertices don't influence medial axis") {
size_t invalid = 0;
for (const Polyline &pl : res)
for (const Point &p : pl.points)
if (std::abs(p.y() - scaled<coord_t>(150.)) < SCALED_EPSILON)
++ invalid;
REQUIRE(invalid == 0);
}
}
}
#endif
GIVEN("semicircumference") {
ExPolygon expolygon{{
{1185881,829367},{1421988,1578184},{1722442,2303558},{2084981,2999998},{2506843,3662186},{2984809,4285086},{3515250,4863959},{4094122,5394400},
{4717018,5872368},{5379210,6294226},{6075653,6656769},{6801033,6957229},{7549842,7193328},{8316383,7363266},{9094809,7465751},{9879211,7500000},
{10663611,7465750},{11442038,7363265},{12208580,7193327},{12957389,6957228},{13682769,6656768},{14379209,6294227},{15041405,5872366},
{15664297,5394401},{16243171,4863960},{16758641,4301424},{17251579,3662185},{17673439,3000000},{18035980,2303556},{18336441,1578177},
{18572539,829368},{18750748,0},{19758422,0},{19727293,236479},{19538467,1088188},{19276136,1920196},{18942292,2726179},{18539460,3499999},
{18070731,4235755},{17539650,4927877},{16950279,5571067},{16307090,6160437},{15614974,6691519},{14879209,7160248},{14105392,7563079},
{13299407,7896927},{12467399,8159255},{11615691,8348082},{10750769,8461952},{9879211,8500000},{9007652,8461952},{8142729,8348082},
{7291022,8159255},{6459015,7896927},{5653029,7563079},{4879210,7160247},{4143447,6691519},{3451331,6160437},{2808141,5571066},{2218773,4927878},
{1687689,4235755},{1218962,3499999},{827499,2748020},{482284,1920196},{219954,1088186},{31126,236479},{0,0},{1005754,0}
}};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(0.25), scaled<double>(1.324888));
THEN("medial axis of a semicircumference is a single line") {
REQUIRE(res.size() == 1);
}
THEN("all medial axis segments of a semicircumference have the same orientation") {
int nccw = 0;
int ncw = 0;
for (const Polyline &pl : res)
for (size_t i = 1; i + 1 < pl.size(); ++ i) {
double cross = cross2((pl.points[i] - pl.points[i - 1]).cast<double>(), (pl.points[i + 1] - pl.points[i]).cast<double>());
if (cross > 0.)
++ nccw;
else if (cross < 0.)
++ ncw;
}
REQUIRE((ncw == 0 || nccw == 0));
}
}
}
GIVEN("narrow trapezoid") {
ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {112, 200}, {108, 200} }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(0.5), scaled<double>(20.));
THEN("medial axis of a narrow trapezoid is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
}
}
GIVEN("L shape") {
ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {120, 180}, {200, 180}, {200, 200}, {100, 200}, }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(0.5), scaled<double>(20.));
THEN("medial axis of an L shape is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
// 20 is the thickness of the expolygon, which is subtracted from the ends
auto len = unscale<double>(res.front().length()) + 20;
REQUIRE(len > 80. * 2.);
REQUIRE(len < 100. * 2.);
}
}
}
GIVEN("whatever shape") {
ExPolygon expolygon{{
{-203064906,-51459966},{-219312231,-51459966},{-219335477,-51459962},{-219376095,-51459962},{-219412047,-51459966},
{-219572094,-51459966},{-219624814,-51459962},{-219642183,-51459962},{-219656665,-51459966},{-220815482,-51459966},
{-220815482,-37738966},{-221117540,-37738966},{-221117540,-51762024},{-203064906,-51762024},
}};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(102499.75, 819998.);
THEN("medial axis is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
double perimeter = expolygon.contour.split_at_first_point().length();
REQUIRE(total_length(res) > perimeter / 2. / 4. * 3.);
}
}
}
GIVEN("narrow triangle") {
ExPolygon expolygon{ Polygon::new_scale({ {50, 100}, {1000, 102}, {50, 104} }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(0.5), scaled<double>(4.));
THEN("medial axis of a narrow triangle is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
}
}
GIVEN("GH #2474") {
ExPolygon expolygon{{ {91294454,31032190},{11294481,31032190},{11294481,29967810},{44969182,29967810},{89909960,29967808},{91294454,29967808} }};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(500000, 1871238);
THEN("medial axis is a single line") {
REQUIRE(res.size() == 1);
}
Polyline &polyline = res.front();
THEN("medial axis is horizontal and is centered") {
double expected_y = expolygon.contour.bounding_box().center().y();
double center_y = 0.;
for (auto &p : polyline.points)
center_y += double(p.y());
REQUIRE(std::abs(center_y / polyline.size() - expected_y) < SCALED_EPSILON);
}
// order polyline from left to right
if (polyline.first_point().x() > polyline.last_point().x())
polyline.reverse();
BoundingBox polyline_bb = polyline.bounding_box();
THEN("expected x_min") {
REQUIRE(polyline.first_point().x() == polyline_bb.min.x());
}
THEN("expected x_max") {
REQUIRE(polyline.last_point().x() == polyline_bb.max.x());
}
THEN("medial axis is monotonous in x (not self intersecting)") {
Polyline sorted { polyline };
std::sort(sorted.begin(), sorted.end());
REQUIRE(polyline == sorted);
}
}
}
}

View File

@@ -0,0 +1,359 @@
#include <catch2/catch.hpp>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleMeshSlicer.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <future>
#include <chrono>
//#include "test_options.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace std;
static inline TriangleMesh make_cube() { return make_cube(20., 20, 20); }
SCENARIO( "TriangleMesh: Basic mesh statistics") {
GIVEN( "A 20mm cube, built from constexpr std::array" ) {
std::vector<Vec3f> vertices { {20,20,0}, {20,0,0}, {0,0,0}, {0,20,0}, {20,20,20}, {0,20,20}, {0,0,20}, {20,0,20} };
std::vector<Vec3i> facets { {0,1,2}, {0,2,3}, {4,5,6}, {4,6,7}, {0,4,7}, {0,7,1}, {1,7,6}, {1,6,2}, {2,6,5}, {2,5,3}, {4,0,3}, {4,3,5} };
TriangleMesh cube(vertices, facets);
THEN( "Volume is appropriate for 20mm square cube.") {
REQUIRE(abs(cube.volume() - 20.0*20.0*20.0) < 1e-2);
}
THEN( "Vertices array matches input.") {
for (size_t i = 0U; i < cube.its.vertices.size(); i++) {
REQUIRE(cube.its.vertices.at(i) == vertices.at(i).cast<float>());
}
for (size_t i = 0U; i < vertices.size(); i++) {
REQUIRE(vertices.at(i).cast<float>() == cube.its.vertices.at(i));
}
}
THEN( "Vertex count matches vertex array size.") {
REQUIRE(cube.facets_count() == facets.size());
}
THEN( "Facet array matches input.") {
for (size_t i = 0U; i < cube.its.indices.size(); i++) {
REQUIRE(cube.its.indices.at(i) == facets.at(i));
}
for (size_t i = 0U; i < facets.size(); i++) {
REQUIRE(facets.at(i) == cube.its.indices.at(i));
}
}
THEN( "Facet count matches facet array size.") {
REQUIRE(cube.facets_count() == facets.size());
}
#if 0
THEN( "Number of normals is equal to the number of facets.") {
REQUIRE(cube.normals().size() == facets.size());
}
#endif
THEN( "center() returns the center of the object.") {
REQUIRE(cube.center() == Vec3d(10.0,10.0,10.0));
}
THEN( "Size of cube is (20,20,20)") {
REQUIRE(cube.size() == Vec3d(20,20,20));
}
}
}
SCENARIO( "TriangleMesh: Transformation functions affect mesh as expected.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN( "The cube is scaled 200% uniformly") {
cube.scale(2.0);
THEN( "The volume is equivalent to 40x40x40 (all dimensions increased by 200%") {
REQUIRE(abs(cube.volume() - 40.0*40.0*40.0) < 1e-2);
}
}
WHEN( "The resulting cube is scaled 200% in the X direction") {
cube.scale(Vec3f(2.0, 1, 1));
THEN( "The volume is doubled.") {
REQUIRE(abs(cube.volume() - 2*20.0*20.0*20.0) < 1e-2);
}
THEN( "The X coordinate size is 200%.") {
REQUIRE(cube.its.vertices.at(0).x() == 40.0);
}
}
WHEN( "The cube is scaled 25% in the X direction") {
cube.scale(Vec3f(0.25, 1, 1));
THEN( "The volume is 25% of the previous volume.") {
REQUIRE(abs(cube.volume() - 0.25*20.0*20.0*20.0) < 1e-2);
}
THEN( "The X coordinate size is 25% from previous.") {
REQUIRE(cube.its.vertices.at(0).x() == 5.0);
}
}
WHEN( "The cube is rotated 45 degrees.") {
cube.rotate_z(float(M_PI / 4.));
THEN( "The X component of the size is sqrt(2)*20") {
REQUIRE(abs(cube.size().x() - sqrt(2.0)*20) < 1e-2);
}
}
WHEN( "The cube is translated (5, 10, 0) units with a Vec3f ") {
cube.translate(Vec3f(5.0, 10.0, 0.0));
THEN( "The first vertex is located at 25, 30, 0") {
REQUIRE(cube.its.vertices.at(0) == Vec3f(25.0, 30.0, 0.0));
}
}
WHEN( "The cube is translated (5, 10, 0) units with 3 doubles") {
cube.translate(5.0, 10.0, 0.0);
THEN( "The first vertex is located at 25, 30, 0") {
REQUIRE(cube.its.vertices.at(0) == Vec3f(25.0, 30.0, 0.0));
}
}
WHEN( "The cube is translated (5, 10, 0) units and then aligned to origin") {
cube.translate(5.0, 10.0, 0.0);
cube.align_to_origin();
THEN( "The third vertex is located at 0,0,0") {
REQUIRE(cube.its.vertices.at(2) == Vec3f::Zero());
}
THEN( "Size is OK") {
REQUIRE(cube.stats().size == Vec3f(20.f, 20.f, 20.f));
}
}
}
}
SCENARIO( "TriangleMesh: slice behavior.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN("Cube is sliced with z = [0+EPSILON,2,4,8,6,8,10,12,14,16,18,20]") {
std::vector<double> z { 0+EPSILON,2,4,8,6,8,10,12,14,16,18,20 };
std::vector<ExPolygons> result = cube.slice(z);
THEN( "The correct number of polygons are returned per layer.") {
for (size_t i = 0U; i < z.size(); i++) {
REQUIRE(result.at(i).size() == 1);
}
}
THEN( "The area of the returned polygons is correct.") {
for (size_t i = 0U; i < z.size(); i++) {
REQUIRE(result.at(i).at(0).area() == 20.0*20/(std::pow(SCALING_FACTOR,2)));
}
}
}
}
GIVEN( "A STL with an irregular shape.") {
const std::vector<Vec3f> vertices {{0,0,0},{0,0,20},{0,5,0},{0,5,20},{50,0,0},{50,0,20},{15,5,0},{35,5,0},{15,20,0},{50,5,0},{35,20,0},{15,5,10},{50,5,20},{35,5,10},{35,20,10},{15,20,10}};
const std::vector<Vec3i> facets {{0,1,2},{2,1,3},{1,0,4},{5,1,4},{0,2,4},{4,2,6},{7,6,8},{4,6,7},{9,4,7},{7,8,10},{2,3,6},{11,3,12},{7,12,9},{13,12,7},{6,3,11},{11,12,13},{3,1,5},{12,3,5},{5,4,9},{12,5,9},{13,7,10},{14,13,10},{8,15,10},{10,15,14},{6,11,8},{8,11,15},{15,11,13},{14,15,13}};
auto cube = make_cube();
WHEN(" a top tangent plane is sliced") {
// At Z = 10 we have a top horizontal surface.
std::vector<ExPolygons> slices = cube.slice({5.0, 10.0});
THEN( "its area is included") {
REQUIRE(slices.at(0).at(0).area() > 0);
REQUIRE(slices.at(1).at(0).area() > 0);
}
}
WHEN(" a model that has been transformed is sliced") {
cube.mirror_z();
std::vector<ExPolygons> slices = cube.slice({-5.0, -10.0});
THEN( "it is sliced properly (mirrored bottom plane area is included)") {
REQUIRE(slices.at(0).at(0).area() > 0);
REQUIRE(slices.at(1).at(0).area() > 0);
}
}
}
}
SCENARIO( "make_xxx functions produce meshes.") {
GIVEN("make_cube() function") {
WHEN("make_cube() is called with arguments 20,20,20") {
TriangleMesh cube = make_cube(20,20,20);
THEN("The resulting mesh has one and only one vertex at 0,0,0") {
const std::vector<Vec3f> &verts = cube.its.vertices;
REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 0; } ) == 1);
}
THEN("The mesh volume is 20*20*20") {
REQUIRE(abs(cube.volume() - 20.0*20.0*20.0) < 1e-2);
}
THEN("There are 12 facets.") {
REQUIRE(cube.its.indices.size() == 12);
}
}
}
GIVEN("make_cylinder() function") {
WHEN("make_cylinder() is called with arguments 10,10, PI / 3") {
TriangleMesh cyl = make_cylinder(10, 10, PI / 243.0);
double angle = (2*PI / floor(2*PI / (PI / 243.0)));
THEN("The resulting mesh has one and only one vertex at 0,0,0") {
const std::vector<Vec3f> &verts = cyl.its.vertices;
REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 0; } ) == 1);
}
THEN("The resulting mesh has one and only one vertex at 0,0,10") {
const std::vector<Vec3f> &verts = cyl.its.vertices;
REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 10; } ) == 1);
}
THEN("Resulting mesh has 2 + (2*PI/angle * 2) vertices.") {
REQUIRE(cyl.its.vertices.size() == (2 + ((2*PI/angle)*2)));
}
THEN("Resulting mesh has 2*PI/angle * 4 facets") {
REQUIRE(cyl.its.indices.size() == (2*PI/angle)*4);
}
THEN( "The mesh volume is approximately 10pi * 10^2") {
REQUIRE(abs(cyl.volume() - (10.0 * M_PI * std::pow(10,2))) < 1);
}
}
}
GIVEN("make_sphere() function") {
WHEN("make_sphere() is called with arguments 10, PI / 3") {
TriangleMesh sph = make_sphere(10, PI / 243.0);
THEN( "Edge length is smaller than limit but not smaller than half of it") {
double len = (sph.its.vertices[sph.its.indices[0][0]] - sph.its.vertices[sph.its.indices[0][1]]).norm();
double limit = 10*PI/243.;
REQUIRE(len <= limit);
REQUIRE(len >= limit/2.);
}
THEN( "Vertices are about the correct distance from the origin") {
bool all_vertices_ok = std::all_of(sph.its.vertices.begin(), sph.its.vertices.end(),
[](const stl_vertex& pt) { return is_approx(pt.squaredNorm(), 100.f); });
REQUIRE(all_vertices_ok);
}
THEN( "The mesh volume is approximately 4/3 * pi * 10^3") {
REQUIRE(abs(sph.volume() - (4.0/3.0 * M_PI * std::pow(10,3))) < 1); // 1% tolerance?
}
}
}
}
SCENARIO( "TriangleMesh: split functionality.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN( "The mesh is split into its component parts.") {
std::vector<TriangleMesh> meshes = cube.split();
THEN(" The bounding box statistics are propagated to the split copies") {
REQUIRE(meshes.size() == 1);
REQUIRE((meshes.front().bounding_box() == cube.bounding_box()));
}
}
}
GIVEN( "Two 20mm cubes, each with one corner on the origin, merged into a single TriangleMesh") {
auto cube = make_cube();
TriangleMesh cube2(cube);
cube.merge(cube2);
WHEN( "The combined mesh is split") {
THEN( "Number of faces is 2x the source.") {
REQUIRE(cube.facets_count() == 2 * cube2.facets_count());
}
std::vector<TriangleMesh> meshes = cube.split();
THEN( "Two meshes are in the output vector.") {
REQUIRE(meshes.size() == 2);
}
}
}
}
SCENARIO( "TriangleMesh: Mesh merge functions") {
GIVEN( "Two 20mm cubes, each with one corner on the origin") {
auto cube = make_cube();
TriangleMesh cube2(cube);
WHEN( "The two meshes are merged") {
cube.merge(cube2);
THEN( "There are twice as many facets in the merged mesh as the original.") {
REQUIRE(cube.facets_count() == 2 * cube2.facets_count());
}
}
}
}
SCENARIO( "TriangleMeshSlicer: Cut behavior.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN( "Object is cut at the bottom") {
indexed_triangle_set upper {};
indexed_triangle_set lower {};
cut_mesh(cube.its, 0, &upper, &lower);
THEN("Upper mesh has all facets except those belonging to the slicing plane.") {
REQUIRE(upper.indices.size() == 12);
}
THEN("Lower mesh has no facets.") {
REQUIRE(lower.indices.size() == 0);
}
}
WHEN( "Object is cut at the center") {
indexed_triangle_set upper {};
indexed_triangle_set lower {};
cut_mesh(cube.its, 10, &upper, &lower);
THEN("Upper mesh has 2 external horizontal facets, 3 facets on each side, and 6 facets on the triangulated side (2 + 12 + 6).") {
REQUIRE(upper.indices.size() == 2+12+6);
}
THEN("Lower mesh has 2 external horizontal facets, 3 facets on each side, and 6 facets on the triangulated side (2 + 12 + 6).") {
REQUIRE(lower.indices.size() == 2+12+6);
}
}
}
}
#ifdef TEST_PERFORMANCE
TEST_CASE("Regression test for issue #4486 - files take forever to slice") {
TriangleMesh mesh;
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/100_000.stl");
config.set("layer_height", 500);
config.set("first_layer_height", 250);
config.set("nozzle_diameter", 500);
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({mesh}, print, model, config);
print.status_cb = [] (int ln, const std::string& msg) { Slic3r::Log::info("Print") << ln << " " << msg << "\n";};
std::future<void> fut = std::async([&print] () { print.process(); });
std::chrono::milliseconds span {120000};
bool timedout {false};
if(fut.wait_for(span) == std::future_status::timeout) {
timedout = true;
}
REQUIRE(timedout == false);
}
#endif // TEST_PERFORMANCE
#ifdef BUILD_PROFILE
TEST_CASE("Profile test for issue #4486 - files take forever to slice") {
TriangleMesh mesh;
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/10_000.stl");
config.set("layer_height", 500);
config.set("first_layer_height", 250);
config.set("nozzle_diameter", 500);
config.set("fill_density", "5%");
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({mesh}, print, model, config);
print.status_cb = [] (int ln, const std::string& msg) { Slic3r::Log::info("Print") << ln << " " << msg << "\n";};
print.process();
REQUIRE(true);
}
#endif //BUILD_PROFILE

View File

@@ -0,0 +1,11 @@
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)
# mold linker for successful linking needs also to link TBB library and link it before libslic3r.
target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb TBB::tbbmalloc libnest2d )
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
# catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
set(_catch_args "exclude:[NotWorking]")
list(APPEND _catch_args "${CATCH_EXTRA_ARGS}")
add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${_catch_args})

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/libnest2d.hpp>
using TestData = std::vector<libnest2d::PathImpl>;
using TestDataEx = std::vector<libnest2d::PolygonImpl>;
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,55 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_3mf.cpp
test_aabbindirect.cpp
test_kdtreeindirect.cpp
test_arachne.cpp
test_clipper_offset.cpp
test_clipper_utils.cpp
test_color.cpp
test_config.cpp
test_curve_fitting.cpp
test_cut_surface.cpp
test_elephant_foot_compensation.cpp
test_expolygon.cpp
test_geometry.cpp
test_placeholder_parser.cpp
test_polygon.cpp
test_polyline.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_stl.cpp
test_meshboolean.cpp
test_marchingsquares.cpp
test_region_expansion.cpp
test_timeutils.cpp
test_utils.cpp
test_voronoi.cpp
test_optimizers.cpp
test_png_io.cpp
test_surface_mesh.cpp
test_timeutils.cpp
test_quadric_edge_collapse.cpp
test_triangulation.cpp
test_emboss.cpp
test_indexed_triangle_set.cpp
test_astar.cpp
test_jump_point_search.cpp
../libnest2d/printer_parts.cpp
)
if (TARGET OpenVDB::openvdb)
target_sources(${_TEST_NAME}_tests PRIVATE test_hollowing.cpp)
endif()
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
if (WIN32)
qidislicer_copy_dlls(${_TEST_NAME}_tests)
endif()
# catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${CATCH_EXTRA_ARGS})

View File

@@ -0,0 +1,88 @@
#include <catch_main.hpp>
#include "libslic3r/Utils.hpp"
// bimap test
#include <string_view>
#include <boost/bimap.hpp>
#include <boost/assign.hpp>
namespace {
TEST_CASE("sort_remove_duplicates", "[utils]") {
std::vector<int> data_src = { 3, 0, 2, 1, 15, 3, 5, 6, 3, 1, 0 };
std::vector<int> data_dst = { 0, 1, 2, 3, 5, 6, 15 };
Slic3r::sort_remove_duplicates(data_src);
REQUIRE(data_src == data_dst);
}
TEST_CASE("string_printf", "[utils]") {
SECTION("Empty format with empty data should return empty string") {
std::string outs = Slic3r::string_printf("");
REQUIRE(outs.empty());
}
SECTION("String output length should be the same as input") {
std::string outs = Slic3r::string_printf("1234");
REQUIRE(outs.size() == 4);
}
SECTION("String format should be interpreted as with sprintf") {
std::string outs = Slic3r::string_printf("%d %f %s", 10, 11.4, " This is a string");
char buffer[1024];
sprintf(buffer, "%d %f %s", 10, 11.4, " This is a string");
REQUIRE(outs.compare(buffer) == 0);
}
SECTION("String format should survive large input data") {
std::string input(2048, 'A');
std::string outs = Slic3r::string_printf("%s", input.c_str());
REQUIRE(outs.compare(input) == 0);
}
}
TEST_CASE("Bimap duplicity behavior") {
enum class number {
one = 1,
three = 3,
tri = 3 // ONLY alias
};
using BimapType = boost::bimap<std::string_view, number>;
BimapType bimap = boost::assign::list_of<BimapType::relation>
("one", number::one)
("three", number::three)
("tri", number::tri) // no matter if it is there
;
const auto& to_type = bimap.left;
auto item_number1 = to_type.find("one");
REQUIRE(item_number1 != to_type.end());
CHECK(item_number1->second == number::one);
auto item_number3 = to_type.find("three");
REQUIRE(item_number3 != to_type.end());
CHECK(item_number3->second == number::three);
// to_type.find("tri"); // not in map
const auto &to_name = bimap.right;
auto it1 = to_name.find(number::one);
REQUIRE(it1 != to_name.end());
CHECK(it1->second == "one");
auto it2 = to_name.find(number::three);
REQUIRE(it2 != to_name.end());
CHECK(it2->second == "three");
auto it3 = to_name.find(number::tri);
REQUIRE(it3 != to_name.end());
REQUIRE(number::three == number::tri);
CHECK(it3->second == "three");
}
} // end namespace

View File

@@ -0,0 +1,133 @@
#include <catch2/catch.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/Format/STL.hpp"
#include <boost/filesystem/operations.hpp>
using namespace Slic3r;
SCENARIO("Reading 3mf file", "[3mf]") {
GIVEN("umlauts in the path of the file") {
Model model;
WHEN("3mf model is read") {
std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf";
DynamicPrintConfig config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
bool ret = load_3mf(path.c_str(), config, ctxt, &model, false);
THEN("load should succeed") {
REQUIRE(ret);
}
}
}
}
SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") {
GIVEN("world vertices coordinates before save") {
// load a model from stl file
Model src_model;
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/QIDI.stl";
load_stl(src_file.c_str(), &src_model);
src_model.add_default_instances();
ModelObject* src_object = src_model.objects.front();
// apply generic transformation to the 1st volume
Geometry::Transformation src_volume_transform;
src_volume_transform.set_offset({ 10.0, 20.0, 0.0 });
src_volume_transform.set_rotation({ Geometry::deg2rad(25.0), Geometry::deg2rad(35.0), Geometry::deg2rad(45.0) });
src_volume_transform.set_scaling_factor({ 1.1, 1.2, 1.3 });
src_volume_transform.set_mirror({ -1.0, 1.0, -1.0 });
src_object->volumes.front()->set_transformation(src_volume_transform);
// apply generic transformation to the 1st instance
Geometry::Transformation src_instance_transform;
src_instance_transform.set_offset({ 5.0, 10.0, 0.0 });
src_instance_transform.set_rotation({ Geometry::deg2rad(12.0), Geometry::deg2rad(13.0), Geometry::deg2rad(14.0) });
src_instance_transform.set_scaling_factor({ 0.9, 0.8, 0.7 });
src_instance_transform.set_mirror({ 1.0, -1.0, -1.0 });
src_object->instances.front()->set_transformation(src_instance_transform);
WHEN("model is saved+loaded to/from 3mf file") {
// save the model to 3mf file
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/qidi.3mf";
store_3mf(test_file.c_str(), &src_model, nullptr, false);
// load back the model from the 3mf file
Model dst_model;
DynamicPrintConfig dst_config;
{
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false);
}
boost::filesystem::remove(test_file);
// compare meshes
TriangleMesh src_mesh = src_model.mesh();
TriangleMesh dst_mesh = dst_model.mesh();
bool res = src_mesh.its.vertices.size() == dst_mesh.its.vertices.size();
if (res) {
for (size_t i = 0; i < dst_mesh.its.vertices.size(); ++i) {
res &= dst_mesh.its.vertices[i].isApprox(src_mesh.its.vertices[i]);
}
}
THEN("world vertices coordinates after load match") {
REQUIRE(res);
}
}
}
}
SCENARIO("2D convex hull of sinking object", "[3mf]") {
GIVEN("model") {
// load a model
Model model;
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/QIDI.stl";
load_stl(src_file.c_str(), &model);
model.add_default_instances();
WHEN("model is rotated, scaled and set as sinking") {
ModelObject* object = model.objects.front();
object->center_around_origin(false);
// set instance's attitude so that it is rotated, scaled and sinking
ModelInstance* instance = object->instances.front();
instance->set_rotation(X, -M_PI / 4.0);
instance->set_offset(Vec3d::Zero());
instance->set_scaling_factor({ 2.0, 2.0, 2.0 });
// calculate 2D convex hull
Polygon hull_2d = object->convex_hull_2d(instance->get_transformation().get_matrix());
// verify result
Points result = {
{ -91501496, -15914144 },
{ 91501496, -15914144 },
{ 91501496, 4243 },
{ 78229680, 4246883 },
{ 56898100, 4246883 },
{ -85501496, 4242641 },
{ -91501496, 4243 }
};
// Allow 1um error due to floating point rounding.
bool res = hull_2d.points.size() == result.size();
if (res)
for (size_t i = 0; i < result.size(); ++ i) {
const Point &p1 = result[i];
const Point &p2 = hull_2d.points[i];
if (std::abs(p1.x() - p2.x()) > 1 || std::abs(p1.y() - p2.y()) > 1) {
res = false;
break;
}
}
THEN("2D convex hull should match with reference") {
REQUIRE(res);
}
}
}
}

View File

@@ -0,0 +1,409 @@
#include <algorithm>
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/AABBTreeIndirect.hpp>
#include <libslic3r/AABBTreeLines.hpp>
using namespace Slic3r;
TEST_CASE("Building a tree over a box, ray caster and closest query", "[AABBIndirect]")
{
TriangleMesh tmesh = make_cube(1., 1., 1.);
auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(tmesh.its.vertices, tmesh.its.indices);
REQUIRE(! tree.empty());
igl::Hit hit;
bool intersected = AABBTreeIndirect::intersect_ray_first_hit(
tmesh.its.vertices, tmesh.its.indices,
tree,
Vec3d(0.5, 0.5, -5.),
Vec3d(0., 0., 1.),
hit);
REQUIRE(intersected);
REQUIRE(hit.t == Approx(5.));
std::vector<igl::Hit> hits;
bool intersected2 = AABBTreeIndirect::intersect_ray_all_hits(
tmesh.its.vertices, tmesh.its.indices,
tree,
Vec3d(0.3, 0.5, -5.),
Vec3d(0., 0., 1.),
hits);
REQUIRE(intersected2);
REQUIRE(hits.size() == 2);
REQUIRE(hits.front().t == Approx(5.));
REQUIRE(hits.back().t == Approx(6.));
size_t hit_idx;
Vec3d closest_point;
double squared_distance = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
tmesh.its.vertices, tmesh.its.indices,
tree,
Vec3d(0.3, 0.5, -5.),
hit_idx, closest_point);
REQUIRE(squared_distance == Approx(5. * 5.));
REQUIRE(closest_point.x() == Approx(0.3));
REQUIRE(closest_point.y() == Approx(0.5));
REQUIRE(closest_point.z() == Approx(0.));
squared_distance = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
tmesh.its.vertices, tmesh.its.indices,
tree,
Vec3d(0.3, 0.5, 5.),
hit_idx, closest_point);
REQUIRE(squared_distance == Approx(4. * 4.));
REQUIRE(closest_point.x() == Approx(0.3));
REQUIRE(closest_point.y() == Approx(0.5));
REQUIRE(closest_point.z() == Approx(1.));
}
TEST_CASE("Creating a several 2d lines, testing closest point query", "[AABBIndirect]")
{
std::vector<Linef> lines { };
lines.push_back(Linef(Vec2d(0.0, 0.0), Vec2d(1.0, 0.0)));
lines.push_back(Linef(Vec2d(1.0, 0.0), Vec2d(1.0, 1.0)));
lines.push_back(Linef(Vec2d(1.0, 1.0), Vec2d(0.0, 1.0)));
lines.push_back(Linef(Vec2d(0.0, 1.0), Vec2d(0.0, 0.0)));
auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines);
size_t hit_idx_out;
Vec2d hit_point_out;
auto sqr_dist = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, Vec2d(0.0, 0.0), hit_idx_out,
hit_point_out);
REQUIRE(sqr_dist == Approx(0.0));
REQUIRE((hit_idx_out == 0 || hit_idx_out == 3));
REQUIRE(hit_point_out.x() == Approx(0.0));
REQUIRE(hit_point_out.y() == Approx(0.0));
sqr_dist = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, Vec2d(1.5, 0.5), hit_idx_out,
hit_point_out);
REQUIRE(sqr_dist == Approx(0.25));
REQUIRE(hit_idx_out == 1);
REQUIRE(hit_point_out.x() == Approx(1.0));
REQUIRE(hit_point_out.y() == Approx(0.5));
}
TEST_CASE("Creating a several 2d lines, testing all lines in radius query", "[AABBIndirect]")
{
std::vector<Linef> lines { };
lines.push_back(Linef(Vec2d(0.0, 0.0), Vec2d(10.0, 0.0)));
lines.push_back(Linef(Vec2d(-10.0, 10.0), Vec2d(10.0, -10.0)));
lines.push_back(Linef(Vec2d(-2.0, -1.0), Vec2d(-2.0, 1.0)));
lines.push_back(Linef(Vec2d(-1.0, -1.0), Vec2d(-1.0, -1.0)));
lines.push_back(Linef(Vec2d(1.0, 1.0), Vec2d(1.0, 1.0)));
auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines);
auto indices = AABBTreeLines::all_lines_in_radius(lines, tree, Vec2d{1.0,1.0}, 4.0);
REQUIRE(std::find(indices.begin(),indices.end(), 0) != indices.end());
REQUIRE(std::find(indices.begin(),indices.end(), 1) != indices.end());
REQUIRE(std::find(indices.begin(),indices.end(), 4) != indices.end());
REQUIRE(indices.size() == 3);
}
TEST_CASE("Find the closest point from ExPolys", "[ClosestPoint]") {
//////////////////////////////
// 0 - 3
// |Ex0| 0 - 3
// | |p |Ex1|
// 1 - 2 | |
// 1 - 2
//[0,0]
///////////////////
ExPolygons ex_polys{
/*Ex0*/ {{0, 4}, {0, 1}, {2, 1}, {2, 4}},
/*Ex1*/ {{4, 3}, {4, 0}, {6, 0}, {6, 3}}
};
Vec2d p{2.5, 3.5};
std::vector<Linef> lines;
auto add_lines = [&lines](const Polygon& poly) {
for (const auto &line : poly.lines())
lines.emplace_back(
line.a.cast<double>(),
line.b.cast<double>());
};
for (const ExPolygon &ex_poly : ex_polys) {
add_lines(ex_poly.contour);
for (const Polygon &hole : ex_poly.holes)
add_lines(hole);
}
AABBTreeIndirect::Tree<2, double> tree =
AABBTreeLines::build_aabb_tree_over_indexed_lines(lines);
size_t hit_idx_out = std::numeric_limits<size_t>::max();
Vec2d hit_point_out;
[[maybe_unused]] double distance_sq =
AABBTreeLines::squared_distance_to_indexed_lines(
lines, tree, p, hit_idx_out, hit_point_out, 0.24/* < (0.5*0.5) */);
CHECK(hit_idx_out == std::numeric_limits<size_t>::max());
distance_sq = AABBTreeLines::squared_distance_to_indexed_lines(
lines, tree, p, hit_idx_out, hit_point_out, 0.26);
CHECK(hit_idx_out != std::numeric_limits<size_t>::max());
//double distance = sqrt(distance_sq);
//const Linef &line = lines[hit_idx_out];
}
#if 0
#include "libslic3r/EdgeGrid.hpp"
#include <iostream>
#include <ctime>
#include <ratio>
#include <chrono>
TEST_CASE("AABBTreeLines vs SignedDistanceGrid time Benchmark", "[AABBIndirect]")
{
std::vector<Points> lines { Points { } };
std::vector<Linef> linesf { };
Vec2d prevf { };
// NOTE: max coord value of the lines is approx 83 mm
for (int r = 1; r < 1000; ++r) {
lines[0].push_back(Point::new_scale(Vec2d(exp(0.005f * r) * cos(r), exp(0.005f * r) * cos(r))));
linesf.emplace_back(prevf, Vec2d(exp(0.005f * r) * cos(r), exp(0.005f * r) * cos(r)));
prevf = linesf.back().b;
}
int build_num = 10000;
using namespace std::chrono;
{
std::cout << "building the tree " << build_num << " times..." << std::endl;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (int i = 0; i < build_num; ++i) {
volatile auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl;
}
{
std::cout << "building the grid res 1mm ONLY " << build_num/100 << " !!! times..." << std::endl;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (int i = 0; i < build_num/100; ++i) {
EdgeGrid::Grid grid { };
grid.create(lines, scaled(1.0), true);
grid.calculate_sdf();
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl;
}
{
std::cout << "building the grid res 10mm " << build_num << " times..." << std::endl;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (int i = 0; i < build_num; ++i) {
EdgeGrid::Grid grid { };
grid.create(lines, scaled(10.0), true);
grid.calculate_sdf();
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl;
}
EdgeGrid::Grid grid10 { };
grid10.create(lines, scaled(10.0), true);
coord_t query10_res = scaled(10.0);
grid10.calculate_sdf();
EdgeGrid::Grid grid1 { };
grid1.create(lines, scaled(1.0), true);
coord_t query1_res = scaled(1.0);
grid1.calculate_sdf();
auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf);
int query_num = 10000;
Points query_points { };
std::vector<Vec2d> query_pointsf { };
for (int x = 0; x < query_num; ++x) {
Vec2d qp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0
- 100.0 };
query_pointsf.push_back(qp);
query_points.push_back(Point::new_scale(qp));
}
{
std::cout << "querying tree " << query_num << " times..." << std::endl;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (const Vec2d &qp : query_pointsf) {
size_t hit_idx_out;
Vec2d hit_point_out;
AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, qp, hit_idx_out, hit_point_out);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl;
}
{
std::cout << "querying grid res 1mm " << query_num << " times..." << std::endl;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (const Point &qp : query_points) {
volatile auto dist = grid1.closest_point_signed_distance(qp, query1_res);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl;
}
{
std::cout << "querying grid res 10mm " << query_num << " times..." << std::endl;
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (const Point &qp : query_points) {
volatile auto dist = grid10.closest_point_signed_distance(qp, query10_res);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl;
}
std::cout << "Test build and queries together - same number of contour points and query points" << std::endl << std::endl;
std::vector<int> point_counts { 100, 300, 500, 1000, 3000 };
for (auto count : point_counts) {
std::vector<Points> lines { Points { } };
std::vector<Linef> linesf { };
Vec2d prevf { };
Points query_points { };
std::vector<Vec2d> query_pointsf { };
for (int x = 0; x < count; ++x) {
Vec2d cp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0
- 100.0 };
lines[0].push_back(Point::new_scale(cp));
linesf.emplace_back(prevf, cp);
prevf = linesf.back().b;
Vec2d qp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0
- 100.0 };
query_pointsf.push_back(qp);
query_points.push_back(Point::new_scale(qp));
}
std::cout << "Test for point count: " << count << std::endl;
{
high_resolution_clock::time_point t1 = high_resolution_clock::now();
auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf);
for (const Vec2d &qp : query_pointsf) {
size_t hit_idx_out;
Vec2d hit_point_out;
AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, qp, hit_idx_out, hit_point_out);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << " Tree took " << time_span.count() << " seconds." << std::endl;
}
{
high_resolution_clock::time_point t1 = high_resolution_clock::now();
EdgeGrid::Grid grid1 { };
grid1.create(lines, scaled(1.0), true);
coord_t query1_res = scaled(1.0);
grid1.calculate_sdf();
for (const Point &qp : query_points) {
volatile auto dist = grid1.closest_point_signed_distance(qp, query1_res);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << " Grid 1mm took " << time_span.count() << " seconds." << std::endl;
}
{
high_resolution_clock::time_point t1 = high_resolution_clock::now();
EdgeGrid::Grid grid10 { };
grid10.create(lines, scaled(10.0), true);
coord_t query10_res = scaled(10.0);
grid10.calculate_sdf();
for (const Point &qp : query_points) {
volatile auto dist = grid10.closest_point_signed_distance(qp, query10_res);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << " Grid 10mm took " << time_span.count() << " seconds." << std::endl;
}
}
std::cout << "Test build and queries together - same number of contour points and query points" << std::endl <<
"And with limited contour edge length to 4mm " << std::endl;
for (auto count : point_counts) {
std::vector<Points> lines { Points { } };
std::vector<Linef> linesf { };
Vec2d prevf { };
Points query_points { };
std::vector<Vec2d> query_pointsf { };
for (int x = 0; x < count; ++x) {
Vec2d cp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0
- 100.0 };
Vec2d contour = prevf + cp.normalized()*4.0; // limits the cnotour edge len to 4mm
lines[0].push_back(Point::new_scale(contour));
linesf.emplace_back(prevf, contour);
prevf = linesf.back().b;
Vec2d qp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0
- 100.0 };
query_pointsf.push_back(qp);
query_points.push_back(Point::new_scale(qp));
}
std::cout << "Test for point count: " << count << std::endl;
{
high_resolution_clock::time_point t1 = high_resolution_clock::now();
auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf);
for (const Vec2d &qp : query_pointsf) {
size_t hit_idx_out;
Vec2d hit_point_out;
AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, qp, hit_idx_out, hit_point_out);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << " Tree took " << time_span.count() << " seconds." << std::endl;
}
{
high_resolution_clock::time_point t1 = high_resolution_clock::now();
EdgeGrid::Grid grid1 { };
grid1.create(lines, scaled(1.0), true);
coord_t query1_res = scaled(1.0);
grid1.calculate_sdf();
for (const Point &qp : query_points) {
volatile auto dist = grid1.closest_point_signed_distance(qp, query1_res);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << " Grid 1mm took " << time_span.count() << " seconds." << std::endl;
}
{
high_resolution_clock::time_point t1 = high_resolution_clock::now();
EdgeGrid::Grid grid10 { };
grid10.create(lines, scaled(10.0), true);
coord_t query10_res = scaled(10.0);
grid10.calculate_sdf();
for (const Point &qp : query_points) {
volatile auto dist = grid10.closest_point_signed_distance(qp, query10_res);
}
high_resolution_clock::time_point t2 = high_resolution_clock::now();
duration<double> time_span = duration_cast<duration<double>>(t2 - t1);
std::cout << " Grid 10mm took " << time_span.count() << " seconds." << std::endl;
}
}
}
#endif

View File

@@ -0,0 +1,746 @@
#include <catch2/catch.hpp>
#include "libslic3r/Arachne/WallToolPaths.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/SVG.hpp"
#include "libslic3r/Utils.hpp"
using namespace Slic3r;
using namespace Slic3r::Arachne;
//#define ARACHNE_DEBUG_OUT
#ifdef ARACHNE_DEBUG_OUT
static void export_perimeters_to_svg(const std::string &path, const Polygons &contours, const std::vector<Arachne::VariableWidthLines> &perimeters, const ExPolygons &infill_area)
{
coordf_t stroke_width = scale_(0.03);
BoundingBox bbox = get_extents(contours);
bbox.offset(scale_(1.));
::Slic3r::SVG svg(path.c_str(), bbox);
svg.draw(infill_area, "cyan");
for (const Arachne::VariableWidthLines &perimeter : perimeters)
for (const Arachne::ExtrusionLine &extrusion_line : perimeter) {
ThickPolyline thick_polyline = to_thick_polyline(extrusion_line);
svg.draw({thick_polyline}, "green", "blue", stroke_width);
}
for (const Line &line : to_lines(contours))
svg.draw(line, "red", stroke_width);
}
#endif
TEST_CASE("Arachne - Closed ExtrusionLine", "[ArachneClosedExtrusionLine]") {
Polygon poly = {
Point(-40000000, 10000000),
Point(-62480000, 10000000),
Point(-62480000, -7410000),
Point(-58430000, -7330000),
Point(-58400000, -5420000),
Point(-58720000, -4710000),
Point(-58940000, -3870000),
Point(-59020000, -3000000),
};
Polygons polygons = {poly};
coord_t spacing = 407079;
coord_t inset_count = 5;
Arachne::WallToolPaths wallToolPaths(polygons, spacing, spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-closed-extrusion-line.svg"), polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
for (VariableWidthLines &perimeter : perimeters)
for (ExtrusionLine &el : perimeter)
if (el.is_closed) {
REQUIRE(el.junctions.front().p == el.junctions.back().p);
}
}
// This test case was distilled from GitHub issue #8472.
// Where for wall_distribution_count == 3 sometime middle perimeter was missing.
TEST_CASE("Arachne - Missing perimeter - #8472", "[ArachneMissingPerimeter8472]") {
Polygon poly = {
Point(-9000000, 8054793),
Point( 7000000, 8054793),
Point( 7000000, 10211874),
Point(-8700000, 10211874),
Point(-9000000, 9824444)
};
Polygons polygons = {poly};
coord_t spacing = 437079;
coord_t inset_count = 3;
PrintObjectConfig print_object_config = PrintObjectConfig::defaults();
print_object_config.wall_distribution_count.setInt(3);
Arachne::WallToolPaths wallToolPaths(polygons, spacing, spacing, inset_count, 0, 0.2, print_object_config, PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-missing-perimeter-8472.svg"), polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
REQUIRE(perimeters.size() == 3);
}
// This test case was distilled from GitHub issue #8593.
// Where on the symmetrical model, there were missing parts of extrusions in gear teeth based on model rotation.
TEST_CASE("Arachne - #8593 - Missing a part of the extrusion", "[ArachneMissingPartOfExtrusion8593]") {
const Polygon poly_orig = {
Point( 1800000, 28500000),
Point( 1100000, 30000000),
Point( 1000000, 30900000),
Point( 600000, 32300000),
Point( -600000, 32300000),
Point(-1000000, 30900000),
Point(-1100000, 30000000),
Point(-1800000, 29000000),
};
coord_t spacing = 377079;
coord_t inset_count = 3;
PrintObjectConfig print_object_config = PrintObjectConfig::defaults();
print_object_config.min_bead_width = ConfigOptionFloatOrPercent(0.315, false);
print_object_config.wall_transition_angle = ConfigOptionFloat(40.);
print_object_config.wall_transition_length = ConfigOptionFloatOrPercent(1., false);
// This behavior seems to be related to the rotation of the input polygon.
// There are specific angles in which this behavior is always triggered.
for (const double angle : {0., -PI / 2., -PI / 15.}) {
Polygon poly = poly_orig;
if (angle != 0.)
poly.rotate(angle);
Polygons polygons = {poly};
Arachne::WallToolPaths wall_tool_paths(polygons, spacing, spacing, inset_count, 0, 0.2, print_object_config, PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
{
static int iRun = 0;
export_perimeters_to_svg(debug_out_path("arachne-missing-part-of-extrusion-8593-%d.svg", iRun++), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
}
#endif
}
}
// This test case was distilled from GitHub issue #8573.
TEST_CASE("Arachne - #8573 - A gap in the perimeter - 1", "[ArachneGapInPerimeter8573_1]") {
const Polygon poly = {
Point(13960000, 500000),
Point(13920000, 1210000),
Point(13490000, 2270000),
Point(12960000, 3400000),
Point(12470000, 4320000),
Point(12160000, 4630000),
Point(12460000, 3780000),
Point(12700000, 2850000),
Point(12880000, 1910000),
Point(12950000, 1270000),
Point(13000000, 500000),
};
Polygons polygons = {poly};
coord_t spacing = 407079;
coord_t inset_count = 2;
PrintObjectConfig print_object_config = PrintObjectConfig::defaults();
// print_object_config.wall_transition_angle = ConfigOptionFloat(20.);
Arachne::WallToolPaths wallToolPaths(polygons, spacing, spacing, inset_count, 0, 0.2, print_object_config, PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-gap-in-perimeter-1-8573.svg"), polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
}
// This test case was distilled from GitHub issue #8444.
TEST_CASE("Arachne - #8444 - A gap in the perimeter - 2", "[ArachneGapInPerimeter8444_2]") {
const Polygon poly = {
Point(14413938, 3825902),
Point(16817613, 711749),
Point(19653030, 67154),
Point(20075592, 925370),
Point(20245428, 1339788),
Point(20493219, 2121894),
Point(20570295, 2486625),
Point(20616559, 2835232),
Point(20631964, 3166882),
Point(20591800, 3858877),
Point(19928267, 2153012),
Point(19723020, 1829802),
Point(19482017, 1612364),
Point(19344810, 1542433),
Point(19200249, 1500902),
Point(19047680, 1487200),
Point(18631073, 1520777),
Point(18377524, 1567627),
Point(18132517, 1641174),
Point(17896307, 1741360),
Point(17669042, 1868075),
Point(17449999, 2021790),
};
Polygons polygons = {poly};
coord_t spacing = 594159;
coord_t inset_count = 2;
PrintObjectConfig print_object_config = PrintObjectConfig::defaults();
// print_object_config.wall_transition_angle = ConfigOptionFloat(20.);
Arachne::WallToolPaths wallToolPaths(polygons, spacing, spacing, inset_count, 0, 0.4, print_object_config, PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-gap-in-perimeter-2-8444.svg"), polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
}
// This test case was distilled from GitHub issue #8528.
// There is a hole in the place where the number of perimeters is changing from 6 perimeters to 7 perimeters.
TEST_CASE("Arachne - #8528 - A hole when number of perimeters is changing", "[ArachneHoleOnPerimetersChange8528]") {
const Polygon poly = {
Point(-30000000, 27650000),
Point(-30000000, 33500000),
Point(-40000000, 33500000),
Point(-40500000, 33500000),
Point(-41100000, 33400000),
Point(-41600000, 33200000),
Point(-42100000, 32900000),
Point(-42600000, 32600000),
Point(-43000000, 32200000),
Point(-43300000, 31700000),
Point(-43600000, 31200000),
Point(-43800000, 30700000),
Point(-43900000, 30100000),
Point(-43900000, 29600000),
Point(-43957080, 25000000),
Point(-39042920, 25000000),
Point(-39042920, 27650000),
};
Polygons polygons = {poly};
coord_t spacing = 814159;
coord_t inset_count = 5;
PrintObjectConfig print_object_config = PrintObjectConfig::defaults();
print_object_config.min_bead_width = ConfigOptionFloatOrPercent(0.68, false);
// Changing min_bead_width to 0.66 seems that resolve this issue, at least in this case.
print_object_config.min_bead_width = ConfigOptionFloatOrPercent(0.66, false);
Arachne::WallToolPaths wallToolPaths(polygons, spacing, spacing, inset_count, 0, 0.4, print_object_config, PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-hole-on-perimeters-change-8528.svg"), polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
}
// This test case was distilled from GitHub issue #8528.
// There is an inconsistency between layers in length of the single perimeters.
TEST_CASE("Arachne - #8555 - Inconsistent single perimeter", "[ArachneInconsistentSinglePerimeter8555]") {
const Polygon poly_0 = {
Point(5527411, -38490007),
Point(11118814, -36631169),
Point(13529600, -36167120),
Point(11300145, -36114514),
Point(10484024, -36113916),
Point(5037323, -37985945),
Point(4097054, -39978866)
};
const Polygon poly_1 = {
Point(5566841, -38517205),
Point(11185208, -36649404),
Point(13462719, -36211009),
Point(11357290, -36161329),
Point(10583855, -36160763),
Point(5105952, -38043516),
Point(4222019, -39917031)
};
const Polygon poly_2 = {
Point(5606269, -38544404),
Point(11251599, -36667638),
Point(13391666, -36255700),
Point(10683552, -36207653),
Point(5174580, -38101085),
Point(4346981, -39855197)
};
const Polygon poly_3 = {
Point(5645699, -38571603),
Point(11317993, -36685873),
Point(13324786, -36299588),
Point(10783383, -36254499),
Point(5243209, -38158655),
Point(4471947, -39793362)
};
const Polygon poly_4 = {
Point(5685128, -38598801),
Point(11384385, -36704108),
Point(13257907, -36343476),
Point(10883211, -36301345),
Point(5311836, -38216224),
Point(4596909, -39731528)
};
const Polygon poly_5 = {
Point(5724558, -38626000),
Point(11450778, -36722343),
Point(13191026, -36387365),
Point(10983042, -36348191),
Point(5380466, -38273795),
Point(4721874, -39669693)
};
Polygons polygons = {poly_0, poly_1, poly_2, poly_3, poly_4, poly_5};
coord_t spacing = 417809;
coord_t inset_count = 2;
for (size_t poly_idx = 0; poly_idx < polygons.size(); ++poly_idx) {
Polygons input_polygons{polygons[poly_idx]};
Arachne::WallToolPaths wallToolPaths(input_polygons, spacing, spacing, inset_count, 0, 0.15, PrintObjectConfig::defaults(), PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-inconsistent-single-perimeter-8555-%d.svg", poly_idx), input_polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
}
}
// This test case was distilled from GitHub issue #8633.
// Open perimeter extrusion is shorter on endpoints in comparison to closed perimeter.
TEST_CASE("Arachne - #8633 - Shorter open perimeter", "[ArachneShorterOpenPerimeter8633]") {
const Polygon poly_0 = {
Point(6507498, 4189461),
Point(6460382, 3601960),
Point(6390896, 3181097),
Point(6294072, 2765838),
Point(6170293, 2357794),
Point(7090581, 2045388),
Point(7232821, 2514293),
Point(7344089, 2991501),
Point(7423910, 3474969),
Point(7471937, 3962592),
Point(7487443, 4436235),
Point(6515575, 4436235),
};
const Polygon poly_1 = {
Point(6507498, 4189461),
Point(6460382, 3601960),
Point(6390896, 3181097),
Point(6294072, 2765838),
Point(6170293, 2357794),
Point(6917958, 1586830),
Point(7090552, 2045398),
Point(7232821, 2514293),
Point(7344089, 2991501),
Point(7423910, 3474969),
Point(7471937, 3962592),
Point(7487443, 4436235),
Point(6515575, 4436235),
};
Polygons polygons = {poly_0, poly_1};
coord_t spacing = 617809;
coord_t inset_count = 1;
PrintObjectConfig print_object_config = PrintObjectConfig::defaults();
print_object_config.min_bead_width = ConfigOptionFloatOrPercent(0.51, false);
print_object_config.min_feature_size = ConfigOptionFloatOrPercent(0.15, false);
print_object_config.wall_transition_length = ConfigOptionFloatOrPercent(0.6, false);
for (size_t poly_idx = 0; poly_idx < polygons.size(); ++poly_idx) {
Polygons input_polygons{polygons[poly_idx]};
Arachne::WallToolPaths wallToolPaths(input_polygons, spacing, spacing, inset_count, 0, 0.15, print_object_config, PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-shorter-open-perimeter-8633-%d.svg", poly_idx), input_polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
}
}
// This test case was distilled from GitHub issue #8597.
// There was just an issue with decrementing std::vector::begin() in a specific case.
TEST_CASE("Arachne - #8597 - removeSmallAreas", "[ArachneRemoveSmallAreas8597]") {
const Polygon poly_0 = {
Point(-38768167, -3636556),
Point(-38763631, -3617883),
Point(-38763925, -3617820),
Point(-38990169, -3919539),
Point(-38928506, -3919539),
};
const Polygon poly_1 = {
Point(-39521732, -4480560),
Point(-39383333, -4398498),
Point(-39119825, -3925307),
Point(-39165608, -3926212),
Point(-39302205, -3959445),
Point(-39578719, -4537002),
};
Polygons polygons = {poly_0, poly_1};
coord_t spacing = 407079;
coord_t inset_count = 2;
Arachne::WallToolPaths wallToolPaths(polygons, spacing, spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-remove-small-areas-8597.svg"), polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
REQUIRE(perimeters.size() == 1);
}
// Test case for missing infill that is probably caused by PolylineStitcher, which produced an open polyline.
TEST_CASE("Arachne - Missing infill", "[ArachneMissingInfill]") {
const Polygon poly_0 = {
Point( 5525881, 3649657),
Point( 452351, -2035297),
Point(-1014702, -2144286),
Point(-5142096, -9101108),
Point( 5525882, -9101108),
};
const Polygon poly_1 = {
Point(1415524, -2217520),
Point(1854189, -2113857),
Point(1566974, -2408538),
};
const Polygon poly_2 = {
Point(-42854, -3771357),
Point(310500, -3783332),
Point( 77735, -4059215),
};
Polygons polygons = {poly_0, poly_1, poly_2};
coord_t spacing = 357079;
coord_t inset_count = 2;
Arachne::WallToolPaths wallToolPaths(polygons, spacing, spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wallToolPaths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-missing-infill.svg"), polygons, perimeters, union_ex(wallToolPaths.getInnerContour()));
#endif
// REQUIRE(wallToolPaths.getInnerContour().size() == 1);
}
// This test case was distilled from GitHub issue #8849.
// Missing part of the model after simplifying generated tool-paths by simplifyToolPaths.
TEST_CASE("Arachne - #8849 - Missing part of model", "[ArachneMissingPart8849]") {
const Polygon poly_0 = {
Point(-29700000, -10600000),
Point(-28200000, -10600000),
Point( 20000000, -10600000),
Point( 20000000, - 9900000),
Point(-28200000, - 9900000),
Point(-28200000, 0),
Point(-29700000, 0),
};
Polygons polygons = {poly_0};
coord_t ext_perimeter_spacing = 449999;
coord_t perimeter_spacing = 757079;
coord_t inset_count = 2;
Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.32, PrintObjectConfig::defaults(), PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-missing-part-8849.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
#endif
int64_t total_extrusion_length = 0;
for (Arachne::VariableWidthLines &perimeter : perimeters)
for (Arachne::ExtrusionLine &extrusion_line : perimeter)
total_extrusion_length += extrusion_line.getLength();
// Total extrusion length should be around 30mm when the part is missing and around 120 when everything is ok.
// REQUIRE(total_extrusion_length >= scaled<int64_t>(120.));
}
// This test case was distilled from GitHub issue #8446.
// Boost Voronoi generator produces non-planar Voronoi diagram with two intersecting linear Voronoi edges.
// Those intersecting edges are causing that perimeters are also generated in places where they shouldn't be.
TEST_CASE("Arachne - #8446 - Degenerated Voronoi diagram - Linear edges", "[ArachneDegeneratedDiagram8446LinearEdges]") {
Polygon poly_0 = {
Point( 42240656, 9020315),
Point( 4474248, 42960681),
Point( -4474248, 42960681),
Point( -4474248, 23193537),
Point( -6677407, 22661038),
Point( -8830542, 21906307),
Point( -9702935, 21539826),
Point(-13110431, 19607811),
Point(-18105334, 15167780),
Point(-20675743, 11422461),
Point(-39475413, 17530840),
Point(-42240653, 9020315)
};
Polygons polygons = {poly_0};
coord_t ext_perimeter_spacing = 407079;
coord_t perimeter_spacing = 407079;
coord_t inset_count = 1;
Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-8446-linear-edges.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
#endif
int64_t total_extrusion_length = 0;
for (Arachne::VariableWidthLines &perimeter : perimeters)
for (Arachne::ExtrusionLine &extrusion_line : perimeter)
total_extrusion_length += extrusion_line.getLength();
// Total extrusion length should be around 211.2mm when the part is ok and 212.1mm when it has perimeters in places where they shouldn't be.
REQUIRE(total_extrusion_length <= scaled<int64_t>(211.5));
}
// This test case was distilled from GitHub issue #8846.
// Boost Voronoi generator produces degenerated Voronoi diagram with one parabolic edge intersecting linear Voronoi edge.
// Those intersecting edges are causing that perimeters are also generated in places where they shouldn't be.
TEST_CASE("Arachne - #8846 - Degenerated Voronoi diagram - One Parabola", "[ArachneDegeneratedDiagram8846OneParabola]") {
const Polygon poly_0 = {
Point(101978540, -41304489), Point(101978540, 41304489),
Point(94709788, 42514051), Point(94709788, 48052315),
Point(93352716, 48052315), Point(93352716, 42514052),
Point(75903540, 42514051), Point(75903540, 48052315),
Point(74546460, 48052315), Point(74546460, 42514052),
Point(69634788, 42514051), Point(69634788, 48052315),
Point(68277708, 48052315), Point(68277708, 42514051),
Point(63366040, 42514051), Point(63366040, 48052315),
Point(62008960, 48052315), Point(62008960, 42514051),
Point(57097292, 42514051), Point(57097292, 48052315),
Point(55740212, 48052315), Point(55740212, 42514052),
Point(50828540, 42514052), Point(50828540, 48052315),
Point(49471460, 48052315), Point(49471460, 42514051),
Point(25753540, 42514051), Point(25753540, 48052315),
Point(24396460, 48052315), Point(24396460, 42514051),
Point(19484790, 42514052), Point(19484790, 48052315),
Point(18127710, 48052315), Point(18127710, 42514051),
Point(-5590210, 42514051), Point(-5590210, 48052315),
Point(-6947290, 48052315), Point(-6947290, 42514051),
Point(-11858960, 42514051), Point(-11858960, 48052315),
Point(-13216040, 48052315), Point(-13216040, 42514051),
Point(-18127710, 42514051), Point(-18127710, 48052315),
Point(-19484790, 48052315), Point(-19484790, 42514052),
Point(-49471460, 42514051), Point(-49471460, 48052315),
Point(-50828540, 48052315), Point(-50828540, 42514052),
Point(-55740212, 42514052), Point(-55740212, 48052315),
Point(-57097292, 48052315), Point(-57097292, 42514051),
Point(-68277708, 42514051), Point(-68277708, 48052315),
Point(-69634788, 48052315), Point(-69634788, 42514051),
Point(-74546460, 42514052), Point(-74546460, 48052315),
Point(-75903540, 48052315), Point(-75903540, 42514051),
Point(-80815204, 42514051), Point(-80815204, 48052315),
Point(-82172292, 48052315), Point(-82172292, 42514051),
Point(-87083956, 42514051), Point(-87083956, 48052315),
Point(-88441044, 48052315), Point(-88441044, 42514051),
Point(-99621460, 42514051), Point(-99621460, 48052315),
Point(-100978540, 48052315), Point(-100978540, 42528248),
Point(-101978540, 41304489), Point(-101978540, -41304489),
Point(-100978540, -48052315), Point(-99621460, -48052315),
};
Polygon poly_1 = {
Point(-100671460, -40092775),
Point(-100671460, 40092775),
Point(100671460, 40092775),
Point(100671460, -40092775),
};
Polygons polygons = {poly_0, poly_1};
coord_t ext_perimeter_spacing = 607079;
coord_t perimeter_spacing = 607079;
coord_t inset_count = 1;
Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-8846-one-parabola.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
#endif
int64_t total_extrusion_length = 0;
for (Arachne::VariableWidthLines &perimeter : perimeters)
for (Arachne::ExtrusionLine &extrusion_line : perimeter)
total_extrusion_length += extrusion_line.getLength();
// Total extrusion length should be around 1335mm when the part is ok and 1347mm when it has perimeters in places where they shouldn't be.
REQUIRE(total_extrusion_length <= scaled<int64_t>(1335.));
}
// This test case was distilled from GitHub issue #9357.
// Boost Voronoi generator produces degenerated Voronoi diagram with two intersecting parabolic Voronoi edges.
// Those intersecting edges are causing that perimeters are also generated in places where they shouldn't be.
TEST_CASE("Arachne - #9357 - Degenerated Voronoi diagram - Two parabolas", "[ArachneDegeneratedDiagram9357TwoParabolas]") {
const Polygon poly_0 = {
Point(78998946, -11733905),
Point(40069507, -7401251),
Point(39983905, -6751055),
Point(39983905, 8251054),
Point(79750000, 10522762),
Point(79983905, 10756667),
Point(79983905, 12248946),
Point(79950248, 12504617),
Point(79709032, 12928156),
Point(79491729, 13102031),
Point(78998946, 13233905),
Point(38501054, 13233905),
Point(37258117, 12901005),
Point(36349000, 11991885),
Point(36100868, 11392844),
Point(36016095, 10748947),
Point(36016095, -6751054),
Point(35930493, -7401249),
Point(4685798, -11733905),
};
Polygons polygons = {poly_0};
coord_t ext_perimeter_spacing = 407079;
coord_t perimeter_spacing = 407079;
coord_t inset_count = 1;
Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-9357-two-parabolas.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
#endif
int64_t total_extrusion_length = 0;
for (Arachne::VariableWidthLines &perimeter : perimeters)
for (Arachne::ExtrusionLine &extrusion_line : perimeter)
total_extrusion_length += extrusion_line.getLength();
// Total extrusion length should be around 256mm when the part is ok and 293mm when it has perimeters in places where they shouldn't be.
REQUIRE(total_extrusion_length <= scaled<int64_t>(256.));
}
// This test case was distilled from GitHub issue #8846.
// Boost Voronoi generator produces degenerated Voronoi diagram with some Voronoi edges intersecting input segments.
// Those Voronoi edges intersecting input segments are causing that perimeters are also generated in places where they shouldn't be.
TEST_CASE("Arachne - #8846 - Degenerated Voronoi diagram - Voronoi edges intersecting input segment", "[ArachneDegeneratedDiagram8846IntersectingInputSegment]") {
const Polygon poly_0 = {
Point( 60000000, 58000000),
Point(-20000000, 53229451),
Point( 49312250, 53229452),
Point( 49443687, 53666225),
Point( 55358348, 50908580),
Point( 53666223, 49443687),
Point( 53229452, 49312250),
Point( 53229452, -49312250),
Point( 53666014, -49443623),
Point(-10000000, -58000000),
Point( 60000000, -58000000),
};
Polygons polygons = {poly_0};
coord_t ext_perimeter_spacing = 407079;
coord_t perimeter_spacing = 407079;
coord_t inset_count = 1;
Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.32, PrintObjectConfig::defaults(), PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-8846-intersecting-input-segment.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
#endif
int64_t total_extrusion_length = 0;
for (Arachne::VariableWidthLines &perimeter : perimeters)
for (Arachne::ExtrusionLine &extrusion_line : perimeter)
total_extrusion_length += extrusion_line.getLength();
// Total extrusion length should be around 500mm when the part is ok and 680mm when it has perimeters in places where they shouldn't be.
REQUIRE(total_extrusion_length <= scaled<int64_t>(500.));
}
// This test case was distilled from GitHub issue #10034.
// In this test case previous rotation by PI / 6 wasn't able to fix non-planar Voronoi diagram.
TEST_CASE("Arachne - #10034 - Degenerated Voronoi diagram - That wasn't fixed by rotation by PI / 6", "[ArachneDegeneratedDiagram10034RotationNotWorks]") {
Polygon poly_0 = {
Point(43612632, -25179766), Point(58456010, 529710), Point(51074898, 17305660), Point(49390982, 21042355),
Point(48102357, 23840161), Point(46769686, 26629546), Point(45835761, 28472742), Point(45205450, 29623133),
Point(45107431, 29878059), Point(45069846, 30174950), Point(45069846, 50759533), Point(-45069846, 50759533),
Point(-45069852, 29630557), Point(-45105780, 29339980), Point(-45179725, 29130704), Point(-46443313, 26398986),
Point(-52272109, 13471493), Point(-58205450, 95724), Point(-29075091, -50359531), Point(29075086, -50359531),
};
Polygon poly_1 = {
Point(-37733905, 45070445), Point(-37813254, 45116257), Point(-39353851, 47784650), Point(-39353851, 47876274),
Point(-38632470, 49125743), Point(-38553121, 49171555), Point(-33833475, 49171555), Point(-33754126, 49125743),
Point(-33032747, 47876277), Point(-33032747, 47784653), Point(-34007855, 46095721), Point(-34573350, 45116257),
Point(-34652699, 45070445),
};
Polygon poly_2 = {
Point(-44016799, 40706401), Point(-44116953, 40806555), Point(-44116953, 46126289), Point(-44016799, 46226443),
Point(-42211438, 46226443), Point(-42132089, 46180631), Point(-40591492, 43512233), Point(-40591492, 43420609),
Point(-41800123, 41327194), Point(-42132089, 40752213), Point(-42211438, 40706401),
};
Polygon poly_3 = {
Point(6218189, 10966609), Point(6138840, 11012421), Point(4598238, 13680817), Point(4598238, 13772441), Point(6138840, 16440843),
Point(6218189, 16486655), Point(9299389, 16486655), Point(9378738, 16440843), Point(10919340, 13772441), Point(10919340, 13680817),
Point(10149039, 12346618), Point(9378738, 11012421), Point(9299389, 10966609),
};
Polygon poly_4 = {
Point(13576879, 6718065), Point(13497530, 6763877), Point(11956926, 9432278), Point(11956926, 9523902),
Point(13497528, 12192302), Point(13576877, 12238114), Point(16658079, 12238112), Point(16737428, 12192300),
Point(18278031, 9523904), Point(18278031, 9432280), Point(17507729, 8098077), Point(16737428, 6763877),
Point(16658079, 6718065),
};
Polygons polygons = {
poly_0, poly_1, poly_2, poly_3, poly_4,
};
coord_t ext_perimeter_spacing = 407079;
coord_t perimeter_spacing = 407079;
coord_t inset_count = 1;
Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults());
wall_tool_paths.generate();
std::vector<Arachne::VariableWidthLines> perimeters = wall_tool_paths.getToolPaths();
#ifdef ARACHNE_DEBUG_OUT
export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-10034-rotation-not-works.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour()));
#endif
}

View File

@@ -0,0 +1,406 @@
#include <catch2/catch.hpp>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/AStar.hpp"
#include "libslic3r/Execution/ExecutionSeq.hpp"
#include "libslic3r/PointGrid.hpp"
using namespace Slic3r;
TEST_CASE("Testing basic invariants of AStar", "[AStar]") {
struct DummyTracer {
using Node = int;
int goal = 0;
float distance(int a, int b) const { return a - b; }
float goal_heuristic(int n) const { return n == goal ? -1.f : 0.f; }
size_t unique_id(int n) const { return n; }
void foreach_reachable(int, std::function<bool(int)>) const {}
};
std::vector<int> out;
SECTION("Output is empty when source is also the destination") {
bool found = astar::search_route(DummyTracer{}, 0, std::back_inserter(out));
REQUIRE(out.empty());
REQUIRE(found);
}
SECTION("Return false when there is no route to destination") {
bool found = astar::search_route(DummyTracer{}, 1, std::back_inserter(out));
REQUIRE(!found);
REQUIRE(out.empty());
}
}
struct PointGridTracer3D {
using Node = size_t;
const PointGrid<float> &grid;
size_t final;
PointGridTracer3D(const PointGrid<float> &g, size_t goal) :
grid{g}, final{goal} {}
template<class Fn>
void foreach_reachable(size_t from, Fn &&fn) const
{
Vec3i from_crd = grid.get_coord(from);
REQUIRE(grid.get_idx(from_crd) == from);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 1, 0, 0}); i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 0, 1, 0}); i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 0, 0, 1}); i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 1, 1, 0}); i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 0, 1, 1}); i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 1, 1, 1}); i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{-1, 0, 0}); from_crd.x() > 0 && i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 0, -1, 0}); from_crd.y() > 0 && i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 0, 0, -1}); from_crd.z() > 0 && i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{-1, -1, 0}); from_crd.x() > 0 && from_crd.y() > 0 && i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{ 0, -1, -1}); from_crd.y() > 0 && from_crd.z() && i < grid.point_count()) fn(i);
if (size_t i = grid.get_idx(from_crd + Vec3i{-1, -1, -1}); from_crd.x() > 0 && from_crd.y() > 0 && from_crd.z() && i < grid.point_count()) fn(i);
}
float distance(size_t a, size_t b) const
{
return (grid.get(a) - grid.get(b)).squaredNorm();
}
float goal_heuristic(size_t n) const
{
return n == final ? -1.f : (grid.get(n) - grid.get(final)).squaredNorm();
}
size_t unique_id(size_t n) const { return n; }
};
template<class Node, class Cmp = std::less<Node>>
bool has_duplicates(const std::vector<Node> &res, Cmp cmp = {})
{
auto cpy = res;
std::sort(cpy.begin(), cpy.end(), cmp);
auto it = std::unique(cpy.begin(), cpy.end());
return it != cpy.end();
}
TEST_CASE("astar algorithm test over 3D point grid", "[AStar]") {
auto vol = BoundingBox3Base<Vec3f>{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}};
auto pgrid = point_grid(ex_seq, vol, {0.1f, 0.1f, 0.1f});
size_t target = pgrid.point_count() - 1;
PointGridTracer3D pgt{pgrid, target};
std::vector<size_t> out;
bool found = astar::search_route(pgt, 0, std::back_inserter(out));
REQUIRE(found);
REQUIRE(!out.empty());
REQUIRE(out.front() == target);
#ifndef NDEBUG
std::cout << "Route taken: ";
for (auto it = out.rbegin(); it != out.rend(); ++it) {
std::cout << "(" << pgrid.get_coord(*it).transpose() << ") ";
}
std::cout << std::endl;
#endif
REQUIRE(!has_duplicates(out)); // No duplicates in output
}
enum CellValue {ON, OFF};
struct CellGridTracer2D_AllDirs {
using Node = Vec2i;
static constexpr auto Cols = size_t(5);
static constexpr auto Rows = size_t(8);
static constexpr size_t GridSize = Cols * Rows;
const std::array<std::array<CellValue, Cols>, Rows> &grid;
Vec2i goal;
CellGridTracer2D_AllDirs(const std::array<std::array<CellValue, Cols>, Rows> &g,
const Vec2i &goal_)
: grid{g}, goal{goal_}
{}
template<class Fn>
void foreach_reachable(const Vec2i &src, Fn &&fn) const
{
auto is_inside = [](const Vec2i& v) { return v.x() >= 0 && v.x() < int(Cols) && v.y() >= 0 && v.y() < int(Rows); };
if (Vec2i crd = src + Vec2i{0, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{1, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{0, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{-1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{-1, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{1, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{-1, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
}
float distance(const Vec2i & a, const Vec2i & b) const { return (a - b).squaredNorm(); }
float goal_heuristic(const Vec2i & n) const { return n == goal ? -1.f : (n - goal).squaredNorm(); }
size_t unique_id(const Vec2i & n) const { return n.y() * Cols + n.x(); }
};
struct CellGridTracer2D_Axis {
using Node = Vec2i;
static constexpr auto Cols = size_t(5);
static constexpr auto Rows = size_t(8);
static constexpr size_t GridSize = Cols * Rows;
const std::array<std::array<CellValue, Cols>, Rows> &grid;
Vec2i goal;
CellGridTracer2D_Axis(
const std::array<std::array<CellValue, Cols>, Rows> &g,
const Vec2i &goal_)
: grid{g}, goal{goal_}
{}
template<class Fn>
void foreach_reachable(const Vec2i &src, Fn &&fn) const
{
auto is_inside = [](const Vec2i& v) { return v.x() >= 0 && v.x() < int(Cols) && v.y() >= 0 && v.y() < int(Rows); };
if (Vec2i crd = src + Vec2i{0, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{0, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
if (Vec2i crd = src + Vec2i{-1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd);
}
float distance(const Vec2i & a, const Vec2i & b) const { return (a - b).squaredNorm(); }
float goal_heuristic(const Vec2i &n) const
{
int manhattan_dst = std::abs(n.x() - goal.x()) +
std::abs(n.y() - goal.y());
return n == goal ? -1.f : manhattan_dst;
}
size_t unique_id(const Vec2i & n) const { return n.y() * Cols + n.x(); }
};
using TestClasses = std::tuple< CellGridTracer2D_AllDirs, CellGridTracer2D_Axis >;
TEMPLATE_LIST_TEST_CASE("Astar should avoid simple barrier", "[AStar]", TestClasses) {
std::array<std::array<CellValue, 5>, 8> grid = {{
{ON , ON , ON , ON , ON},
{ON , ON , ON , ON , ON},
{ON , ON , ON , ON , ON},
{ON , ON , ON , ON , ON},
{ON , ON , ON , ON , ON},
{ON , OFF, OFF, OFF, ON},
{ON , ON , ON , ON , ON},
{ON , ON , ON , ON , ON}
}};
Vec2i dst = {2, 0};
TestType cgt{grid, dst};
std::vector<Vec2i> out;
bool found = astar::search_route(cgt, {2, 7}, std::back_inserter(out));
REQUIRE(found);
REQUIRE(!out.empty());
REQUIRE(out.front() == dst);
REQUIRE(!has_duplicates(out, [](const Vec2i &a, const Vec2i &b) {
return a.x() == b.x() ? a.y() < b.y() : a.x() < b.x();
}));
#ifndef NDEBUG
std::cout << "Route taken: ";
for (auto it = out.rbegin(); it != out.rend(); ++it) {
std::cout << "(" << it->transpose() << ") ";
}
std::cout << std::endl;
#endif
}
TEMPLATE_LIST_TEST_CASE("Astar should manage to avoid arbitrary barriers", "[AStar]", TestClasses) {
std::array<std::array<CellValue, 5>, 8> grid = {{
{ON , ON , ON , ON , ON},
{ON , ON , ON , OFF, ON},
{OFF, OFF, ON , OFF, ON},
{ON , ON , ON , OFF, ON},
{ON , OFF, ON , OFF, ON},
{ON , OFF, ON , ON , ON},
{ON , OFF, ON , OFF, ON},
{ON , ON , ON , ON , ON}
}};
Vec2i dst = {0, 0};
TestType cgt{grid, dst};
std::vector<Vec2i> out;
bool found = astar::search_route(cgt, {0, 7}, std::back_inserter(out));
REQUIRE(found);
REQUIRE(!out.empty());
REQUIRE(out.front() == dst);
REQUIRE(!has_duplicates(out, [](const Vec2i &a, const Vec2i &b) {
return a.x() == b.x() ? a.y() < b.y() : a.x() < b.x();
}));
#ifndef NDEBUG
std::cout << "Route taken: ";
for (auto it = out.rbegin(); it != out.rend(); ++it) {
std::cout << "(" << it->transpose() << ") ";
}
std::cout << std::endl;
#endif
}
TEMPLATE_LIST_TEST_CASE("Astar should find the way out of a labyrinth", "[AStar]", TestClasses) {
std::array<std::array<CellValue, 5>, 8> grid = {{
{ON , ON , ON , ON , ON },
{ON , OFF, OFF, OFF, OFF},
{ON , ON , ON , ON , ON },
{OFF, OFF, OFF, OFF, ON },
{ON , ON , ON , ON , ON },
{ON , OFF, OFF, OFF, OFF},
{ON , ON , ON , ON , ON },
{OFF, OFF, OFF, OFF, ON }
}};
Vec2i dst = {4, 0};
TestType cgt{grid, dst};
std::vector<Vec2i> out;
bool found = astar::search_route(cgt, {4, 7}, std::back_inserter(out));
REQUIRE(found);
REQUIRE(!out.empty());
REQUIRE(out.front() == dst);
REQUIRE(!has_duplicates(out, [](const Vec2i &a, const Vec2i &b) {
return a.x() == b.x() ? a.y() < b.y() : a.x() < b.x();
}));
#ifndef NDEBUG
std::cout << "Route taken: ";
for (auto it = out.rbegin(); it != out.rend(); ++it) {
std::cout << "(" << it->transpose() << ") ";
}
std::cout << std::endl;
#endif
}
TEST_CASE("Zero heuristic function should result in dijsktra's algo", "[AStar]")
{
struct GraphTracer {
using Node = size_t;
using QNode = astar::QNode<GraphTracer>;
struct Edge
{
size_t to_id = size_t(-1);
float cost = 0.f;
bool operator <(const Edge &e) const { return to_id < e.to_id; }
};
struct ENode: public QNode {
std::vector<Edge> edges;
ENode(size_t node_id, std::initializer_list<Edge> edgelist)
: QNode{node_id}, edges(edgelist)
{}
ENode &operator=(const QNode &q)
{
assert(node == q.node);
g = q.g;
h = q.h;
parent = q.parent;
queue_id = q.queue_id;
return *this;
}
};
// Example graph from
// https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/?ref=lbp
std::vector<ENode> nodes = {
{0, {{1, 4.f}, {7, 8.f}}},
{1, {{0, 4.f}, {2, 8.f}, {7, 11.f}}},
{2, {{1, 8.f}, {3, 7.f}, {5, 4.f}, {8, 2.f}}},
{3, {{2, 7.f}, {4, 9.f}, {5, 14.f}}},
{4, {{3, 9.f}, {5, 10.f}}},
{5, {{2, 4.f}, {3, 14.f}, {4, 10.f}, {6, 2.f}}},
{6, {{5, 2.f}, {7, 1.f}, {8, 6.f}}},
{7, {{0, 8.f}, {1, 11.f}, {6, 1.f}, {8, 7.f}}},
{8, {{2, 2.f}, {6, 6.f}, {7, 7.f}}}
};
float distance(size_t a, size_t b) const {
float ret = std::numeric_limits<float>::infinity();
if (a < nodes.size()) {
auto it = std::lower_bound(nodes[a].edges.begin(),
nodes[a].edges.end(),
Edge{b, 0.f});
if (it != nodes[a].edges.end()) {
ret = it->cost;
}
}
return ret;
}
float goal_heuristic(size_t) const { return 0.f; }
size_t unique_id(size_t n) const { return n; }
void foreach_reachable(size_t n, std::function<bool(int)> fn) const
{
if (n < nodes.size()) {
for (const Edge &e : nodes[n].edges)
fn(e.to_id);
}
}
} graph;
std::vector<size_t> out;
// 'graph.nodes' is able to be a node cache (it simulates an associative container)
bool found = astar::search_route(graph, size_t(0), std::back_inserter(out), graph.nodes);
// But should not crash or loop infinitely.
REQUIRE(!found);
// Without a destination, there is no output. But the algorithm should halt.
REQUIRE(out.empty());
// Source node should have it's parent unset
REQUIRE(graph.nodes[0].parent == astar::Unassigned);
// All other nodes should have their parents set
for (size_t i = 1; i < graph.nodes.size(); ++i)
REQUIRE(graph.nodes[i].parent != astar::Unassigned);
std::array<float, 9> ref_distances = {0.f, 4.f, 12.f, 19.f, 21.f,
11.f, 9.f, 8.f, 14.f};
// Try to trace each node back to the source node. Each of them should
// arrive to the source within less hops than the full number of nodes.
for (size_t i = 0, k = 0; i < graph.nodes.size(); ++i, k = 0) {
GraphTracer::QNode *q = &graph.nodes[i];
REQUIRE(q->g == Approx(ref_distances[i]));
while (k++ < graph.nodes.size() && q->parent != astar::Unassigned)
q = &graph.nodes[q->parent];
REQUIRE(q->parent == astar::Unassigned);
}
}

View File

@@ -0,0 +1,214 @@
#include <catch2/catch.hpp>
#include <iostream>
#include <boost/filesystem.hpp>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/SVG.hpp"
using namespace Slic3r;
// #define TESTS_EXPORT_SVGS
SCENARIO("Constant offset", "[ClipperUtils]") {
coord_t s = 1000000;
GIVEN("20mm box") {
ExPolygon box20mm;
box20mm.contour.points = { Vec2crd{ 0, 0 }, Vec2crd{ 20 * s, 0 }, Vec2crd{ 20 * s, 20 * s}, Vec2crd{ 0, 20 * s} };
std::vector<float> deltas_plus(box20mm.contour.points.size(), 1. * s);
std::vector<float> deltas_minus(box20mm.contour.points.size(), - 1. * s);
Polygons output;
WHEN("Slic3r::offset()") {
for (double miter : { 2.0, 1.5, 1.2 }) {
DYNAMIC_SECTION("plus 1mm, miter " << miter << "x") {
output = Slic3r::offset(box20mm, 1. * s, ClipperLib::jtMiter, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("constant_offset_box20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 22^2mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx(22. * 22. * s * s));
}
}
DYNAMIC_SECTION("minus 1mm, miter " << miter << "x") {
output = Slic3r::offset(box20mm, - 1. * s, ClipperLib::jtMiter, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("constant_offset_box20mm_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 18^2mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx(18. * 18. * s * s));
}
}
}
}
WHEN("Slic3r::variable_offset_outer/inner") {
for (double miter : { 2.0, 1.5, 1.2 }) {
DYNAMIC_SECTION("plus 1mm, miter " << miter << "x") {
output = Slic3r::variable_offset_outer(box20mm, { deltas_plus }, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("variable_offset_box20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 22^2mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx(22. * 22. * s * s));
}
}
DYNAMIC_SECTION("minus 1mm, miter " << miter << "x") {
output = Slic3r::variable_offset_inner(box20mm, { deltas_minus }, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("variable_offset_box20mm_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 18^2mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx(18. * 18. * s * s));
}
}
}
}
}
GIVEN("20mm box with 10mm hole") {
ExPolygon box20mm;
box20mm.contour.points = { Vec2crd{ 0, 0 }, Vec2crd{ 20 * s, 0 }, Vec2crd{ 20 * s, 20 * s}, Vec2crd{ 0, 20 * s} };
box20mm.holes.emplace_back(Slic3r::Polygon({ Vec2crd{ 5 * s, 5 * s }, Vec2crd{ 5 * s, 15 * s}, Vec2crd{ 15 * s, 15 * s}, Vec2crd{ 15 * s, 5 * s } }));
std::vector<float> deltas_plus(box20mm.contour.points.size(), 1. * s);
std::vector<float> deltas_minus(box20mm.contour.points.size(), -1. * s);
ExPolygons output;
SECTION("Slic3r::offset()") {
for (double miter : { 2.0, 1.5, 1.2 }) {
DYNAMIC_SECTION("miter " << miter << "x") {
WHEN("plus 1mm") {
output = Slic3r::offset_ex(box20mm, 1. * s, ClipperLib::jtMiter, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("constant_offset_box20mm_10mm_hole_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 22^2-8^2 mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx((22. * 22. - 8. * 8.) * s * s));
}
}
WHEN("minus 1mm") {
output = Slic3r::offset_ex(box20mm, - 1. * s, ClipperLib::jtMiter, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("constant_offset_box20mm_10mm_hole_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 18^2-12^2 mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx((18. * 18. - 12. * 12.) * s * s));
}
}
}
}
}
SECTION("Slic3r::variable_offset_outer()") {
for (double miter : { 2.0, 1.5, 1.2 }) {
DYNAMIC_SECTION("miter " << miter << "x") {
WHEN("plus 1mm") {
output = Slic3r::variable_offset_outer_ex(box20mm, { deltas_plus, deltas_plus }, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("variable_offset_box20mm_10mm_hole_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 22^2-8^2 mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx((22. * 22. - 8. * 8.) * s * s));
}
}
WHEN("minus 1mm") {
output = Slic3r::variable_offset_inner_ex(box20mm, { deltas_minus, deltas_minus }, miter);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("variable_offset_box20mm_10mm_hole_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(box20mm, "blue");
svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area is 18^2-12^2 mm2") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx((18. * 18. - 12. * 12.) * s * s));
}
}
}
}
}
}
GIVEN("20mm right angle triangle") {
ExPolygon triangle20mm;
triangle20mm.contour.points = { Vec2crd{ 0, 0 }, Vec2crd{ 20 * s, 0 }, Vec2crd{ 0, 20 * s } };
Polygons output;
double offset = 1.;
// Angle of the sharp corner bisector.
double angle_bisector = M_PI / 8.;
// Area tapered by mitering one sharp corner.
double area_tapered = pow(offset * (1. / sin(angle_bisector) - 1.), 2.) * tan(angle_bisector);
double l_triangle_side_offsetted = 20. + offset * (1. + 1. / tan(angle_bisector));
double area_offsetted = (0.5 * l_triangle_side_offsetted * l_triangle_side_offsetted - 2. * area_tapered) * s * s;
SECTION("Slic3r::offset()") {
for (double miter : { 2.0, 1.5, 1.2 }) {
DYNAMIC_SECTION("Outer offset 1mm, miter " << miter << "x") {
output = Slic3r::offset(triangle20mm, offset * s, ClipperLib::jtMiter, 2.0);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("constant_offset_triangle20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(triangle20mm, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area matches") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx(area_offsetted));
}
}
}
}
SECTION("Slic3r::variable_offset_outer()") {
std::vector<float> deltas(triangle20mm.contour.points.size(), 1. * s);
for (double miter : { 2.0, 1.5, 1.2 }) {
DYNAMIC_SECTION("Outer offset 1mm, miter " << miter << "x") {
output = Slic3r::variable_offset_outer(triangle20mm, { deltas }, 2.0);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("variable_offset_triangle20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output));
svg.draw(triangle20mm, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif
THEN("Area matches") {
REQUIRE(output.size() == 1);
REQUIRE(output.front().area() == Approx(area_offsetted));
}
}
}
}
}
}

View File

@@ -0,0 +1,384 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <iostream>
#include <boost/filesystem.hpp>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/SVG.hpp"
using namespace Slic3r;
SCENARIO("Various Clipper operations - xs/t/11_clipper.t", "[ClipperUtils]") {
// CCW oriented contour
Slic3r::Polygon square{ { 200, 100 }, {200, 200}, {100, 200}, {100, 100} };
// CW oriented contour
Slic3r::Polygon hole_in_square{ { 160, 140 }, { 140, 140 }, { 140, 160 }, { 160, 160 } };
Slic3r::ExPolygon square_with_hole(square, hole_in_square);
GIVEN("square_with_hole") {
WHEN("offset") {
Polygons result = Slic3r::offset(square_with_hole, 5.f);
THEN("offset matches") {
REQUIRE(result == Polygons {
{ { 205, 205 }, { 95, 205 }, { 95, 95 }, { 205, 95 }, },
{ { 155, 145 }, { 145, 145 }, { 145, 155 }, { 155, 155 } } });
}
}
WHEN("offset_ex") {
ExPolygons result = Slic3r::offset_ex(square_with_hole, 5.f);
THEN("offset matches") {
REQUIRE(result == ExPolygons { {
{ { 205, 205 }, { 95, 205 }, { 95, 95 }, { 205, 95 }, },
{ { 145, 145 }, { 145, 155 }, { 155, 155 }, { 155, 145 } } } } );
}
}
WHEN("offset2_ex") {
ExPolygons result = Slic3r::offset2_ex({ square_with_hole }, 5.f, -2.f);
THEN("offset matches") {
REQUIRE(result == ExPolygons { {
{ { 203, 203 }, { 97, 203 }, { 97, 97 }, { 203, 97 } },
{ { 143, 143 }, { 143, 157 }, { 157, 157 }, { 157, 143 } } } } );
}
}
}
GIVEN("square_with_hole 2") {
Slic3r::ExPolygon square_with_hole(
{ { 20000000, 20000000 }, { 0, 20000000 }, { 0, 0 }, { 20000000, 0 } },
{ { 5000000, 15000000 }, { 15000000, 15000000 }, { 15000000, 5000000 }, { 5000000, 5000000 } });
WHEN("offset2_ex") {
Slic3r::ExPolygons result = Slic3r::offset2_ex(ExPolygons { square_with_hole }, -1.f, 1.f);
THEN("offset matches") {
REQUIRE(result.size() == 1);
REQUIRE(square_with_hole.area() == result.front().area());
}
}
}
GIVEN("square and hole") {
WHEN("diff_ex") {
ExPolygons result = Slic3r::diff_ex(Polygons{ square }, Polygons{ hole_in_square });
THEN("hole is created") {
REQUIRE(result.size() == 1);
REQUIRE(square_with_hole.area() == result.front().area());
}
}
}
GIVEN("polyline") {
Polyline polyline { { 50, 150 }, { 300, 150 } };
WHEN("intersection_pl") {
Polylines result = Slic3r::intersection_pl(polyline, ExPolygon{ square, hole_in_square });
THEN("correct number of result lines") {
REQUIRE(result.size() == 2);
}
THEN("result lines have correct length") {
// results are in no particular order
REQUIRE(result[0].length() == 40);
REQUIRE(result[1].length() == 40);
}
}
WHEN("diff_pl") {
Polylines result = Slic3r::diff_pl({ polyline }, Polygons{ square, hole_in_square });
THEN("correct number of result lines") {
REQUIRE(result.size() == 3);
}
// results are in no particular order
THEN("the left result line has correct length") {
REQUIRE(std::count_if(result.begin(), result.end(), [](const Polyline &pl) { return pl.length() == 50; }) == 1);
}
THEN("the right result line has correct length") {
REQUIRE(std::count_if(result.begin(), result.end(), [](const Polyline &pl) { return pl.length() == 100; }) == 1);
}
THEN("the central result line has correct length") {
REQUIRE(std::count_if(result.begin(), result.end(), [](const Polyline &pl) { return pl.length() == 20; }) == 1);
}
}
}
GIVEN("Clipper bug #96 / Slic3r issue #2028") {
Slic3r::Polyline subject{
{ 44735000, 31936670 }, { 55270000, 31936670 }, { 55270000, 25270000 }, { 74730000, 25270000 }, { 74730000, 44730000 }, { 68063296, 44730000 }, { 68063296, 55270000 }, { 74730000, 55270000 },
{ 74730000, 74730000 }, { 55270000, 74730000 }, { 55270000, 68063296 }, { 44730000, 68063296 }, { 44730000, 74730000 }, { 25270000, 74730000 }, { 25270000, 55270000 }, { 31936670, 55270000 },
{ 31936670, 44730000 }, { 25270000, 44730000 }, { 25270000, 25270000 }, { 44730000, 25270000 }, { 44730000, 31936670 } };
Slic3r::Polygon clip { {75200000, 45200000}, {54800000, 45200000}, {54800000, 24800000}, {75200000, 24800000} };
Slic3r::Polylines result = Slic3r::intersection_pl(subject, ExPolygon{ clip });
THEN("intersection_pl - result is not empty") {
REQUIRE(result.size() == 1);
}
}
GIVEN("Clipper bug #122") {
Slic3r::Polyline subject { { 1975, 1975 }, { 25, 1975 }, { 25, 25 }, { 1975, 25 }, { 1975, 1975 } };
Slic3r::Polygons clip { { { 2025, 2025 }, { -25, 2025 } , { -25, -25 }, { 2025, -25 } },
{ { 525, 525 }, { 525, 1475 }, { 1475, 1475 }, { 1475, 525 } } };
Slic3r::Polylines result = Slic3r::intersection_pl({ subject }, clip);
THEN("intersection_pl - result is not empty") {
REQUIRE(result.size() == 1);
REQUIRE(result.front().points.size() == 5);
}
}
GIVEN("Clipper bug #126") {
Slic3r::Polyline subject { { 200000, 19799999 }, { 200000, 200000 }, { 24304692, 200000 }, { 15102879, 17506106 }, { 13883200, 19799999 }, { 200000, 19799999 } };
Slic3r::Polygon clip { { 15257205, 18493894 }, { 14350057, 20200000 }, { -200000, 20200000 }, { -200000, -200000 }, { 25196917, -200000 } };
Slic3r::Polylines result = Slic3r::intersection_pl(subject, ExPolygon{ clip });
THEN("intersection_pl - result is not empty") {
REQUIRE(result.size() == 1);
}
THEN("intersection_pl - result has same length as subject polyline") {
REQUIRE(result.front().length() == Approx(subject.length()));
}
}
#if 0
{
# Clipper does not preserve polyline orientation
my $polyline = Slic3r::Polyline->new([50, 150], [300, 150]);
my $result = Slic3r::Geometry::Clipper::intersection_pl([$polyline], [$square]);
is scalar(@$result), 1, 'intersection_pl - correct number of result lines';
is_deeply $result->[0]->pp, [[100, 150], [200, 150]], 'clipped line orientation is preserved';
}
{
# Clipper does not preserve polyline orientation
my $polyline = Slic3r::Polyline->new([300, 150], [50, 150]);
my $result = Slic3r::Geometry::Clipper::intersection_pl([$polyline], [$square]);
is scalar(@$result), 1, 'intersection_pl - correct number of result lines';
is_deeply $result->[0]->pp, [[200, 150], [100, 150]], 'clipped line orientation is preserved';
}
{
# Disabled until Clipper bug #127 is fixed
my $subject = [
Slic3r::Polyline->new([-90000000, -100000000], [-90000000, 100000000]), # vertical
Slic3r::Polyline->new([-100000000, -10000000], [100000000, -10000000]), # horizontal
Slic3r::Polyline->new([-100000000, 0], [100000000, 0]), # horizontal
Slic3r::Polyline->new([-100000000, 10000000], [100000000, 10000000]), # horizontal
];
my $clip = Slic3r::Polygon->new(# a circular, convex, polygon
[99452190, 10452846], [97814760, 20791169], [95105652, 30901699], [91354546, 40673664], [86602540, 50000000],
[80901699, 58778525], [74314483, 66913061], [66913061, 74314483], [58778525, 80901699], [50000000, 86602540],
[40673664, 91354546], [30901699, 95105652], [20791169, 97814760], [10452846, 99452190], [0, 100000000],
[-10452846, 99452190], [-20791169, 97814760], [-30901699, 95105652], [-40673664, 91354546],
[-50000000, 86602540], [-58778525, 80901699], [-66913061, 74314483], [-74314483, 66913061],
[-80901699, 58778525], [-86602540, 50000000], [-91354546, 40673664], [-95105652, 30901699],
[-97814760, 20791169], [-99452190, 10452846], [-100000000, 0], [-99452190, -10452846],
[-97814760, -20791169], [-95105652, -30901699], [-91354546, -40673664], [-86602540, -50000000],
[-80901699, -58778525], [-74314483, -66913061], [-66913061, -74314483], [-58778525, -80901699],
[-50000000, -86602540], [-40673664, -91354546], [-30901699, -95105652], [-20791169, -97814760],
[-10452846, -99452190], [0, -100000000], [10452846, -99452190], [20791169, -97814760],
[30901699, -95105652], [40673664, -91354546], [50000000, -86602540], [58778525, -80901699],
[66913061, -74314483], [74314483, -66913061], [80901699, -58778525], [86602540, -50000000],
[91354546, -40673664], [95105652, -30901699], [97814760, -20791169], [99452190, -10452846], [100000000, 0]
);
my $result = Slic3r::Geometry::Clipper::intersection_pl($subject, [$clip]);
is scalar(@$result), scalar(@$subject), 'intersection_pl - expected number of polylines';
is sum(map scalar(@$_), @$result), scalar(@$subject) * 2, 'intersection_pl - expected number of points in polylines';
}
#endif
}
SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") {
GIVEN("square with hole") {
// CCW oriented contour
Slic3r::Polygon square { { 10, 10 }, { 20, 10 }, { 20, 20 }, { 10, 20 } };
Slic3r::Polygon square2 { { 5, 12 }, { 25, 12 }, { 25, 18 }, { 5, 18 } };
// CW oriented contour
Slic3r::Polygon hole_in_square { { 14, 14 }, { 14, 16 }, { 16, 16 }, { 16, 14 } };
WHEN("intersection_ex with another square") {
ExPolygons intersection = Slic3r::intersection_ex(Polygons{ square, hole_in_square }, Polygons{ square2 });
THEN("intersection area matches (hole is preserved)") {
ExPolygon match({ { 20, 18 }, { 10, 18 }, { 10, 12 }, { 20, 12 } },
{ { 14, 16 }, { 16, 16 }, { 16, 14 }, { 14, 14 } });
REQUIRE(intersection.size() == 1);
REQUIRE(intersection.front().area() == Approx(match.area()));
}
}
ExPolygons expolygons { ExPolygon { square, hole_in_square } };
WHEN("Clipping line 1") {
Polylines intersection = intersection_pl({ Polyline { { 15, 18 }, { 15, 15 } } }, expolygons);
THEN("line is clipped to square with hole") {
REQUIRE((Vec2f(15, 18) - Vec2f(15, 16)).norm() == Approx(intersection.front().length()));
}
}
WHEN("Clipping line 2") {
Polylines intersection = intersection_pl({ Polyline { { 15, 15 }, { 15, 12 } } }, expolygons);
THEN("line is clipped to square with hole") {
REQUIRE((Vec2f(15, 14) - Vec2f(15, 12)).norm() == Approx(intersection.front().length()));
}
}
WHEN("Clipping line 3") {
Polylines intersection = intersection_pl({ Polyline { { 12, 18 }, { 18, 18 } } }, expolygons);
THEN("line is clipped to square with hole") {
REQUIRE((Vec2f(18, 18) - Vec2f(12, 18)).norm() == Approx(intersection.front().length()));
}
}
WHEN("Clipping line 4") {
Polylines intersection = intersection_pl({ Polyline { { 5, 15 }, { 30, 15 } } }, expolygons);
THEN("line is clipped to square with hole") {
REQUIRE((Vec2f(14, 15) - Vec2f(10, 15)).norm() == Approx(intersection.front().length()));
REQUIRE((Vec2f(20, 15) - Vec2f(16, 15)).norm() == Approx(intersection[1].length()));
}
}
WHEN("Clipping line 5") {
Polylines intersection = intersection_pl({ Polyline { { 30, 15 }, { 5, 15 } } }, expolygons);
THEN("reverse line is clipped to square with hole") {
REQUIRE((Vec2f(20, 15) - Vec2f(16, 15)).norm() == Approx(intersection.front().length()));
REQUIRE((Vec2f(14, 15) - Vec2f(10, 15)).norm() == Approx(intersection[1].length()));
}
}
WHEN("Clipping line 6") {
Polylines intersection = intersection_pl({ Polyline { { 10, 18 }, { 20, 18 } } }, expolygons);
THEN("tangent line is clipped to square with hole") {
REQUIRE((Vec2f(20, 18) - Vec2f(10, 18)).norm() == Approx(intersection.front().length()));
}
}
}
GIVEN("square with hole 2") {
// CCW oriented contour
Slic3r::Polygon square { { 0, 0 }, { 40, 0 }, { 40, 40 }, { 0, 40 } };
Slic3r::Polygon square2 { { 10, 10 }, { 30, 10 }, { 30, 30 }, { 10, 30 } };
// CW oriented contour
Slic3r::Polygon hole { { 15, 15 }, { 15, 25 }, { 25, 25 }, {25, 15 } };
WHEN("union_ex with another square") {
ExPolygons union_ = Slic3r::union_ex({ square, square2, hole });
THEN("union of two ccw and one cw is a contour with no holes") {
REQUIRE(union_.size() == 1);
REQUIRE(union_.front() == ExPolygon { { 40, 40 }, { 0, 40 }, { 0, 0 }, { 40, 0 } } );
}
}
WHEN("diff_ex with another square") {
ExPolygons diff = Slic3r::diff_ex(Polygons{ square, square2 }, Polygons{ hole });
THEN("difference of a cw from two ccw is a contour with one hole") {
REQUIRE(diff.size() == 1);
REQUIRE(diff.front().area() == Approx(ExPolygon({ {40, 40}, {0, 40}, {0, 0}, {40, 0} }, { {15, 25}, {25, 25}, {25, 15}, {15, 15} }).area()));
}
}
}
GIVEN("yet another square") {
Slic3r::Polygon square { { 10, 10 }, { 20, 10 }, { 20, 20 }, { 10, 20 } };
Slic3r::Polyline square_pl = square.split_at_first_point();
WHEN("no-op diff_pl") {
Slic3r::Polylines res = Slic3r::diff_pl({ square_pl }, Polygons{});
THEN("returns the right number of polylines") {
REQUIRE(res.size() == 1);
}
THEN("returns the unmodified input polyline") {
REQUIRE(res.front().points.size() == square_pl.points.size());
}
}
}
GIVEN("circle") {
Slic3r::ExPolygon circle_with_hole { Polygon::new_scale({
{ 151.8639,288.1192 }, {133.2778,284.6011}, { 115.0091,279.6997 }, { 98.2859,270.8606 }, { 82.2734,260.7933 },
{ 68.8974,247.4181 }, { 56.5622,233.0777 }, { 47.7228,216.3558 }, { 40.1617,199.0172 }, { 36.6431,180.4328 },
{ 34.932,165.2312 }, { 37.5567,165.1101 }, { 41.0547,142.9903 }, { 36.9056,141.4295 }, { 40.199,124.1277 },
{ 47.7776,106.7972 }, { 56.6335,90.084 }, { 68.9831,75.7557 }, { 82.3712,62.3948 }, { 98.395,52.3429 },
{ 115.1281,43.5199 }, { 133.4004,38.6374 }, { 151.9884,35.1378 }, { 170.8905,35.8571 }, { 189.6847,37.991 },
{ 207.5349,44.2488 }, { 224.8662,51.8273 }, { 240.0786,63.067 }, { 254.407,75.4169 }, { 265.6311,90.6406 },
{ 275.6832,106.6636 }, { 281.9225,124.52 }, { 286.8064,142.795 }, { 287.5061,161.696 }, { 286.7874,180.5972 },
{ 281.8856,198.8664 }, { 275.6283,216.7169 }, { 265.5604,232.7294 }, { 254.3211,247.942 }, { 239.9802,260.2776 },
{ 224.757,271.5022 }, { 207.4179,279.0635 }, { 189.5605,285.3035 }, { 170.7649,287.4188 }
}) };
circle_with_hole.holes = { Polygon::new_scale({
{ 158.227,215.9007 }, { 164.5136,215.9007 }, { 175.15,214.5007 }, { 184.5576,210.6044 }, { 190.2268,207.8743 },
{ 199.1462,201.0306 }, { 209.0146,188.346 }, { 213.5135,177.4829 }, { 214.6979,168.4866 }, { 216.1025,162.3325 },
{ 214.6463,151.2703 }, { 213.2471,145.1399 }, { 209.0146,134.9203 }, { 199.1462,122.2357 }, { 189.8944,115.1366 },
{ 181.2504,111.5567 }, { 175.5684,108.8205 }, { 164.5136,107.3655 }, { 158.2269,107.3655 }, { 147.5907,108.7656 },
{ 138.183,112.6616 }, { 132.5135,115.3919 }, { 123.5943,122.2357 }, { 113.7259,134.92 }, { 109.2269,145.7834 },
{ 108.0426,154.7799 }, { 106.638,160.9339 }, { 108.0941,171.9957 }, { 109.4933,178.1264 }, { 113.7259,188.3463 },
{ 123.5943,201.0306 }, { 132.8461,208.1296 }, { 141.4901,211.7094 }, { 147.172,214.4458 }
}) };
THEN("contour is counter-clockwise") {
REQUIRE(circle_with_hole.contour.is_counter_clockwise());
}
THEN("hole is counter-clockwise") {
REQUIRE(circle_with_hole.holes.size() == 1);
REQUIRE(circle_with_hole.holes.front().is_clockwise());
}
WHEN("clipping a line") {
auto line = Polyline::new_scale({ { 152.742,288.086671142818 }, { 152.742,34.166466971035 } });
Polylines intersection = intersection_pl(line, to_polygons(circle_with_hole));
THEN("clipped to two pieces") {
REQUIRE(intersection.front().length() == Approx((Vec2d(152742000, 215178843) - Vec2d(152742000, 288086661)).norm()));
REQUIRE(intersection[1].length() == Approx((Vec2d(152742000, 35166477) - Vec2d(152742000, 108087507)).norm()));
}
}
}
GIVEN("line") {
THEN("expand by 5") {
REQUIRE(offset(Polyline({10,10}, {20,10}), 5).front().area() == Polygon({ {10,5}, {20,5}, {20,15}, {10,15} }).area());
}
}
}
template<e_ordering o = e_ordering::OFF, class P, class P_Alloc, class Tree>
double polytree_area(const Tree &tree, std::vector<P, P_Alloc> *out)
{
traverse_pt<o>(tree, out);
return std::accumulate(out->begin(), out->end(), 0.0,
[](double a, const P &p) { return a + p.area(); });
}
size_t count_polys(const ExPolygons& expolys)
{
size_t c = 0;
for (auto &ep : expolys) c += ep.holes.size() + 1;
return c;
}
TEST_CASE("Traversing Clipper PolyTree", "[ClipperUtils]") {
// Create a polygon representing unit box
Polygon unitbox;
const auto UNIT = coord_t(1. / SCALING_FACTOR);
unitbox.points = { Vec2crd{0, 0}, Vec2crd{UNIT, 0}, Vec2crd{UNIT, UNIT}, Vec2crd{0, UNIT}};
Polygon box_frame = unitbox;
box_frame.scale(20, 10);
Polygon hole_left = unitbox;
hole_left.scale(8);
hole_left.translate(UNIT, UNIT);
hole_left.reverse();
Polygon hole_right = hole_left;
hole_right.translate(UNIT * 10, 0);
Polygon inner_left = unitbox;
inner_left.scale(4);
inner_left.translate(UNIT * 3, UNIT * 3);
Polygon inner_right = inner_left;
inner_right.translate(UNIT * 10, 0);
Polygons reference = union_({box_frame, hole_left, hole_right, inner_left, inner_right});
ClipperLib::PolyTree tree = union_pt(reference);
double area_sum = box_frame.area() + hole_left.area() +
hole_right.area() + inner_left.area() +
inner_right.area();
REQUIRE(area_sum > 0);
SECTION("Traverse into Polygons WITHOUT spatial ordering") {
Polygons output;
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
REQUIRE(output.size() == reference.size());
}
SECTION("Traverse into ExPolygons WITHOUT spatial ordering") {
ExPolygons output;
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
REQUIRE(count_polys(output) == reference.size());
}
SECTION("Traverse into Polygons WITH spatial ordering") {
Polygons output;
REQUIRE(area_sum == Approx(polytree_area<e_ordering::ON>(tree.GetFirst(), &output)));
REQUIRE(output.size() == reference.size());
}
SECTION("Traverse into ExPolygons WITH spatial ordering") {
ExPolygons output;
REQUIRE(area_sum == Approx(polytree_area<e_ordering::ON>(tree.GetFirst(), &output)));
REQUIRE(count_polys(output) == reference.size());
}
}

View File

@@ -0,0 +1,23 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Color.hpp"
using namespace Slic3r;
SCENARIO("Color encoding/decoding cycle", "[Color]") {
GIVEN("Color") {
const ColorRGB src_rgb(static_cast<unsigned char>(255), static_cast<unsigned char>(127), static_cast<unsigned char>(63));
WHEN("apply encode/decode cycle") {
const std::string encoded = encode_color(src_rgb);
ColorRGB res_rgb;
decode_color(encoded, res_rgb);
const bool ret = res_rgb.r_uchar() == src_rgb.r_uchar() && res_rgb.g_uchar() == src_rgb.g_uchar() && res_rgb.b_uchar() == src_rgb.b_uchar();
THEN("result matches source") {
REQUIRE(ret);
}
}
}
}

View File

@@ -0,0 +1,247 @@
#include <catch2/catch.hpp>
#include "libslic3r/Config.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/LocalesUtils.hpp"
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include <cereal/archives/binary.hpp>
using namespace Slic3r;
SCENARIO("Generic config validation performs as expected.", "[Config]") {
GIVEN("A config generated from default options") {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
WHEN("perimeter_extrusion_width is set to 250%, a valid value") {
config.set_deserialize_strict("perimeter_extrusion_width", "250%");
THEN( "The config is read as valid.") {
REQUIRE(config.validate().empty());
}
}
WHEN("perimeter_extrusion_width is set to -10, an invalid value") {
config.set("perimeter_extrusion_width", -10);
THEN( "Validate returns error") {
REQUIRE(! config.validate().empty());
}
}
WHEN("perimeters is set to -10, an invalid value") {
config.set("perimeters", -10);
THEN( "Validate returns error") {
REQUIRE(! config.validate().empty());
}
}
}
}
SCENARIO("Config accessor functions perform as expected.", "[Config]") {
auto test = [](ConfigBase &config) {
WHEN("A boolean option is set to a boolean value") {
REQUIRE_NOTHROW(config.set("gcode_comments", true));
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionBool>("gcode_comments")->getBool() == true);
}
}
WHEN("A boolean option is set to a string value representing a 0 or 1") {
CHECK_NOTHROW(config.set_deserialize_strict("gcode_comments", "1"));
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionBool>("gcode_comments")->getBool() == true);
}
}
WHEN("A boolean option is set to a string value representing something other than 0 or 1") {
THEN("A BadOptionTypeException exception is thrown.") {
REQUIRE_THROWS_AS(config.set("gcode_comments", "Z"), BadOptionTypeException);
}
AND_THEN("Value is unchanged.") {
REQUIRE(config.opt<ConfigOptionBool>("gcode_comments")->getBool() == false);
}
}
WHEN("A boolean option is set to an int value") {
THEN("A BadOptionTypeException exception is thrown.") {
REQUIRE_THROWS_AS(config.set("gcode_comments", 1), BadOptionTypeException);
}
}
WHEN("A numeric option is set from serialized string") {
config.set_deserialize_strict("bed_temperature", "100");
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionInts>("bed_temperature")->get_at(0) == 100);
}
}
#if 0
//FIXME better design accessors for vector elements.
WHEN("An integer-based option is set through the integer interface") {
config.set("bed_temperature", 100);
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionInts>("bed_temperature")->get_at(0) == 100);
}
}
#endif
WHEN("An floating-point option is set through the integer interface") {
config.set("perimeter_speed", 10);
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionFloat>("perimeter_speed")->getFloat() == 10.0);
}
}
WHEN("A floating-point option is set through the double interface") {
config.set("perimeter_speed", 5.5);
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionFloat>("perimeter_speed")->getFloat() == 5.5);
}
}
WHEN("An integer-based option is set through the double interface") {
THEN("A BadOptionTypeException exception is thrown.") {
REQUIRE_THROWS_AS(config.set("bed_temperature", 5.5), BadOptionTypeException);
}
}
WHEN("A numeric option is set to a non-numeric value.") {
THEN("A BadOptionTypeException exception is thown.") {
REQUIRE_THROWS_AS(config.set_deserialize_strict("perimeter_speed", "zzzz"), BadOptionValueException);
}
THEN("The value does not change.") {
REQUIRE(config.opt<ConfigOptionFloat>("perimeter_speed")->getFloat() == 60.0);
}
}
WHEN("A string option is set through the string interface") {
config.set("end_gcode", "100");
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionString>("end_gcode")->value == "100");
}
}
WHEN("A string option is set through the integer interface") {
config.set("end_gcode", 100);
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionString>("end_gcode")->value == "100");
}
}
WHEN("A string option is set through the double interface") {
config.set("end_gcode", 100.5);
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionString>("end_gcode")->value == float_to_string_decimal_point(100.5));
}
}
WHEN("A float or percent is set as a percent through the string interface.") {
config.set_deserialize_strict("first_layer_extrusion_width", "100%");
THEN("Value and percent flag are 100/true") {
auto tmp = config.opt<ConfigOptionFloatOrPercent>("first_layer_extrusion_width");
REQUIRE(tmp->percent == true);
REQUIRE(tmp->value == 100);
}
}
WHEN("A float or percent is set as a float through the string interface.") {
config.set_deserialize_strict("first_layer_extrusion_width", "100");
THEN("Value and percent flag are 100/false") {
auto tmp = config.opt<ConfigOptionFloatOrPercent>("first_layer_extrusion_width");
REQUIRE(tmp->percent == false);
REQUIRE(tmp->value == 100);
}
}
WHEN("A float or percent is set as a float through the int interface.") {
config.set("first_layer_extrusion_width", 100);
THEN("Value and percent flag are 100/false") {
auto tmp = config.opt<ConfigOptionFloatOrPercent>("first_layer_extrusion_width");
REQUIRE(tmp->percent == false);
REQUIRE(tmp->value == 100);
}
}
WHEN("A float or percent is set as a float through the double interface.") {
config.set("first_layer_extrusion_width", 100.5);
THEN("Value and percent flag are 100.5/false") {
auto tmp = config.opt<ConfigOptionFloatOrPercent>("first_layer_extrusion_width");
REQUIRE(tmp->percent == false);
REQUIRE(tmp->value == 100.5);
}
}
WHEN("An invalid option is requested during set.") {
THEN("A BadOptionTypeException exception is thrown.") {
REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", 1), UnknownOptionException);
REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", 1.0), UnknownOptionException);
REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", "1"), UnknownOptionException);
REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", true), UnknownOptionException);
}
}
WHEN("An invalid option is requested during get.") {
THEN("A UnknownOptionException exception is thrown.") {
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionString>("deadbeef_invalid_option", false), UnknownOptionException);
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionFloat>("deadbeef_invalid_option", false), UnknownOptionException);
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionInt>("deadbeef_invalid_option", false), UnknownOptionException);
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionBool>("deadbeef_invalid_option", false), UnknownOptionException);
}
}
WHEN("An invalid option is requested during opt.") {
THEN("A UnknownOptionException exception is thrown.") {
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionString>("deadbeef_invalid_option", false), UnknownOptionException);
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionFloat>("deadbeef_invalid_option", false), UnknownOptionException);
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionInt>("deadbeef_invalid_option", false), UnknownOptionException);
REQUIRE_THROWS_AS(config.option_throw<ConfigOptionBool>("deadbeef_invalid_option", false), UnknownOptionException);
}
}
WHEN("getX called on an unset option.") {
THEN("The default is returned.") {
REQUIRE(config.opt_float("layer_height") == 0.3);
REQUIRE(config.opt_int("raft_layers") == 0);
REQUIRE(config.opt_bool("support_material") == false);
}
}
WHEN("getFloat called on an option that has been set.") {
config.set("layer_height", 0.5);
THEN("The set value is returned.") {
REQUIRE(config.opt_float("layer_height") == 0.5);
}
}
};
GIVEN("DynamicPrintConfig generated from default options") {
auto config = Slic3r::DynamicPrintConfig::full_print_config();
test(config);
}
GIVEN("FullPrintConfig generated from default options") {
Slic3r::FullPrintConfig config;
test(config);
}
}
SCENARIO("Config ini load/save interface", "[Config]") {
WHEN("new_from_ini is called") {
Slic3r::DynamicPrintConfig config;
std::string path = std::string(TEST_DATA_DIR) + "/test_config/new_from_ini.ini";
config.load_from_ini(path, ForwardCompatibilitySubstitutionRule::Disable);
THEN("Config object contains ini file options.") {
REQUIRE(config.option_throw<ConfigOptionStrings>("filament_colour", false)->values.size() == 1);
REQUIRE(config.option_throw<ConfigOptionStrings>("filament_colour", false)->values.front() == "#ABCD");
}
}
}
SCENARIO("DynamicPrintConfig serialization", "[Config]") {
WHEN("DynamicPrintConfig is serialized and deserialized") {
FullPrintConfig full_print_config;
DynamicPrintConfig cfg;
cfg.apply(full_print_config, false);
std::string serialized;
try {
std::ostringstream ss;
cereal::BinaryOutputArchive oarchive(ss);
oarchive(cfg);
serialized = ss.str();
} catch (const std::runtime_error & /* e */) {
// e.what();
}
THEN("Config object contains ini file options.") {
DynamicPrintConfig cfg2;
try {
std::stringstream ss(serialized);
cereal::BinaryInputArchive iarchive(ss);
iarchive(cfg2);
} catch (const std::runtime_error & /* e */) {
// e.what();
}
REQUIRE(cfg == cfg2);
}
}
}

View File

@@ -0,0 +1,118 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/Geometry/Curves.hpp>
#include <libslic3r/Utils.hpp>
#include <libslic3r/SVG.hpp>
TEST_CASE("Curves: cubic b spline fit test", "[Curves]") {
using namespace Slic3r;
using namespace Slic3r::Geometry;
auto fx = [&](size_t index) {
return float(index) / 200.0f;
};
auto fy = [&](size_t index) {
return 1.0f;
};
std::vector<Vec<1, float>> observations { };
std::vector<float> observation_points { };
std::vector<float> weights { };
for (size_t index = 0; index < 200; ++index) {
observations.push_back(Vec<1, float> { fy(index) });
observation_points.push_back(fx(index));
weights.push_back(1);
}
Vec2f fmin { fx(0), fy(0) };
Vec2f fmax { fx(200), fy(200) };
auto bspline = fit_cubic_bspline(observations, observation_points, weights, 1);
Approx ap(1.0f);
ap.epsilon(0.1f);
for (int p = 0; p < 200; ++p) {
float fitted_val = bspline.get_fitted_value(fx(p))(0);
float expected = fy(p);
REQUIRE(fitted_val == ap(expected));
}
}
TEST_CASE("Curves: quadratic f cubic b spline fit test", "[Curves]") {
using namespace Slic3r;
using namespace Slic3r::Geometry;
auto fx = [&](size_t index) {
return float(index) / 100.0f;
};
auto fy = [&](size_t index) {
return (fx(index) - 1) * (fx(index) - 1);
};
std::vector<Vec<1, float>> observations { };
std::vector<float> observation_points { };
std::vector<float> weights { };
for (size_t index = 0; index < 200; ++index) {
observations.push_back(Vec<1, float> { fy(index) });
observation_points.push_back(fx(index));
weights.push_back(1);
}
Vec2f fmin { fx(0), fy(0) };
Vec2f fmax { fx(200), fy(200) };
auto bspline = fit_cubic_bspline(observations, observation_points, weights, 10);
for (int p = 0; p < 200; ++p) {
float fitted_val = bspline.get_fitted_value(fx(p))(0);
float expected = fy(p);
auto check = [](float a, float b) {
return abs(a - b) < 0.2f;
};
//Note: checking is problematic, splines will not perfectly align
REQUIRE(check(fitted_val, expected));
}
}
TEST_CASE("Curves: polynomial fit test", "[Curves]") {
using namespace Slic3r;
using namespace Slic3r::Geometry;
auto fx = [&](size_t index) {
return float(index) / 100.0f;
};
auto fy = [&](size_t index) {
return (fx(index) - 1) * (fx(index) - 1);
};
std::vector<Vec<1, float>> observations { };
std::vector<float> observation_points { };
std::vector<float> weights { };
for (size_t index = 0; index < 200; ++index) {
observations.push_back(Vec<1, float> { fy(index) });
observation_points.push_back(fx(index));
weights.push_back(1);
}
Vec2f fmin { fx(0), fy(0) };
Vec2f fmax { fx(200), fy(200) };
Approx ap(1.0f);
ap.epsilon(0.1f);
auto poly = fit_polynomial(observations, observation_points, weights, 2);
REQUIRE(poly.coefficients(0, 0) == ap(1));
REQUIRE(poly.coefficients(0, 1) == ap(-2));
REQUIRE(poly.coefficients(0, 2) == ap(1));
}

View File

@@ -0,0 +1,171 @@
#include <catch2/catch.hpp>
#include <libslic3r/CutSurface.hpp>
#include <libslic3r/TriangleMesh.hpp> // its_make_cube + its_merge
using namespace Slic3r;
TEST_CASE("Cut character from surface", "[]")
{
std::string font_path = std::string(TEST_DATA_DIR) +
"/../../resources/fonts/NotoSans-Regular.ttf";
char letter = '%';
float flatness = 2.;
unsigned int font_index = 0; // collection
double z_depth = 50.f; // projection size
auto font = Emboss::create_font_file(font_path.c_str());
REQUIRE(font != nullptr);
std::optional<Emboss::Glyph> glyph =
Emboss::letter2glyph(*font, font_index, letter, flatness);
REQUIRE(glyph.has_value());
ExPolygons shapes = glyph->shape;
REQUIRE(!shapes.empty());
Transform3d tr = Transform3d::Identity();
tr.translate(Vec3d(0., 0., -z_depth));
tr.scale(Emboss::SHAPE_SCALE);
Emboss::OrthoProject cut_projection(tr, Vec3d(0., 0., z_depth));
auto object = its_make_cube(782 - 49 + 50, 724 + 10 + 50, 5);
its_translate(object, Vec3f(49 - 25, -10 - 25, -40));
auto cube2 = object; // copy
its_translate(cube2, Vec3f(100, -40, 7.5));
its_merge(object, std::move(cube2));
std::vector<indexed_triangle_set> objects{object};
// Call core function for cut surface
auto surfaces = cut_surface(shapes, objects, cut_projection, 0.5);
CHECK(!surfaces.empty());
Emboss::OrthoProject projection(Transform3d::Identity(),
Vec3d(0.f, 0.f, 10.f));
its_translate(surfaces, Vec3f(0.f, 0.f, 10));
indexed_triangle_set its = cut2model(surfaces, projection);
CHECK(!its.empty());
// its_write_obj(its, "C:/data/temp/projected.obj");
}
//#define DEBUG_3MF
#ifdef DEBUG_3MF
// Test load of 3mf
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/Model.hpp"
static std::vector<indexed_triangle_set> transform_volumes(ModelVolume *mv) {
const auto &volumes = mv->get_object()->volumes;
std::vector<indexed_triangle_set> results;
results.reserve(volumes.size());
// Improve create object from part or use gl_volume
// Get first model part in object
for (const ModelVolume *v : volumes) {
if (v->id() == mv->id()) continue;
if (!v->is_model_part()) continue;
const TriangleMesh &tm = v->mesh();
if (tm.empty()) continue;
if (tm.its.empty()) continue;
results.push_back(tm.its); // copy: indexed_triangle_set
indexed_triangle_set& its = results.back();
its_transform(its,v->get_matrix());
}
return results;
}
static Emboss::OrthoProject create_projection_for_cut(
Transform3d tr,
double shape_scale,
const BoundingBox &shape_bb,
const std::pair<float, float> &z_range)
{
// create sure that emboss object is bigger than source object
const float safe_extension = 1.0f;
double min_z = z_range.first - safe_extension;
double max_z = z_range.second + safe_extension;
assert(min_z < max_z);
// range between min and max value
double projection_size = max_z - min_z;
Matrix3d transformation_for_vector = tr.linear();
// Projection must be negative value.
// System of text coordinate
// X .. from left to right
// Y .. from bottom to top
// Z .. from text to eye
Vec3d untransformed_direction(0., 0., projection_size);
Vec3d project_direction = transformation_for_vector * untransformed_direction;
// Projection is in direction from far plane
tr.translate(Vec3d(0., 0., min_z));
tr.scale(shape_scale);
// Text alignemnt to center 2D
Vec2d move = -(shape_bb.max + shape_bb.min).cast<double>() / 2.;
// Vec2d move = -shape_bb.center().cast<double>(); // not precisse
tr.translate(Vec3d(move.x(), move.y(), 0.));
return Emboss::OrthoProject(tr, project_direction);
}
TEST_CASE("CutSurface in 3mf", "[Emboss]")
{
//std::string path_to_3mf = "C:/Users/Filip Sykala/Downloads/EmbossFromMultiVolumes.3mf";
//int object_id = 0;
//int text_volume_id = 2;
//std::string path_to_3mf = "C:/Users/Filip Sykala/Downloads/treefrog.3mf";
//int object_id = 0;
//int text_volume_id = 1;
std::string path_to_3mf = "C:/Users/Filip Sykala/Downloads/cube_test.3mf";
int object_id = 1;
int text_volume_id = 2;
Model model;
DynamicPrintConfig config;
ConfigSubstitutionContext ctxt{ForwardCompatibilitySubstitutionRule::Disable};
CHECK(load_3mf(path_to_3mf.c_str(), config, ctxt, &model, false));
CHECK(object_id >= 0);
CHECK((size_t)object_id < model.objects.size());
ModelObject* mo = model.objects[object_id];
CHECK(mo != nullptr);
CHECK(text_volume_id >= 0);
CHECK((size_t)text_volume_id < mo->volumes.size());
ModelVolume *mv_text = mo->volumes[text_volume_id];
CHECK(mv_text != nullptr);
CHECK(mv_text->text_configuration.has_value());
TextConfiguration &tc = *mv_text->text_configuration;
/* // Need GUI to load font by wx
std::optional<wxFont> wx_font = GUI::WxFontUtils::load_wxFont(tc.style.path);
CHECK(wx_font.has_value());
Emboss::FontFileWithCache ff(GUI::WxFontUtils::create_font_file(*wx_font));
CHECK(ff.font_file != nullptr);
/*/ // end use GUI
// start use fake font
std::string font_path = std::string(TEST_DATA_DIR) +
"/../../resources/fonts/NotoSans-Regular.ttf";
Emboss::FontFileWithCache ff(Emboss::create_font_file(font_path.c_str()));
// */ // end use fake font
CHECK(ff.has_value());
std::vector<indexed_triangle_set> its = transform_volumes(mv_text);
BoundingBoxf3 bb;
for (auto &i : its) bb.merge(Slic3r::bounding_box(i));
Transform3d cut_projection_tr = mv_text->get_matrix() * tc.fix_3mf_tr->inverse();
Transform3d emboss_tr = cut_projection_tr.inverse();
BoundingBoxf3 mesh_bb_tr = bb.transformed(emboss_tr);
std::pair<float, float> z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()};
FontProp fp = tc.style.prop;
ExPolygons shapes = Emboss::text2shapes(ff, tc.text.c_str(), fp);
double shape_scale = Emboss::get_shape_scale(fp, *ff.font_file);
Emboss::OrthoProject projection = create_projection_for_cut(
cut_projection_tr, shape_scale, get_extents(shapes), z_range);
float projection_ratio = -z_range.first / (z_range.second - z_range.first);
SurfaceCut cut = cut_surface(shapes, its, projection, projection_ratio);
its_write_obj(cut, "C:/data/temp/cutSurface/result_cut.obj");
}
#endif // DEBUG_3MF

View File

@@ -0,0 +1,610 @@
#include <catch2/catch.hpp>
#include <iostream>
#include <boost/filesystem.hpp>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ElephantFootCompensation.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/SVG.hpp"
using namespace Slic3r;
// #define TESTS_EXPORT_SVGS
namespace Slic3r {
ClipperLib::Path mittered_offset_path_scaled(const Points& contour, const std::vector<float>& deltas, double miter_limit);
}
static ExPolygon spirograph_gear_1mm()
{
ExPolygon out;
out.contour.points = { { 8989059, 1015976 }, { 9012502, 1051010 }, { 9224741, 1786512 }, { 9232060, 1811874 }, { 9222459, 2132217 }, { 10263301, 2241715 }, { 10318693, 1936696 }, { 10320603, 1926178 }, { 10680972, 1250945 }, { 10693399, 1227661 }, { 10723616, 1198273 }, { 11599898, 346008 }, { 11616108, 351267 }, { 12086183, 503769 }, { 12293780, 1708518 }, { 12300939, 1750061 }, { 12195899, 2508234 }, { 12192277, 2534378 }, { 12053161, 2823089 }, { 12959357, 3346344 }, { 13133980, 3090414 }, { 13140002, 3081589 }, { 13160830, 3065371 }, { 13764842, 2595047 }, { 13804400, 2580484 }, { 14951581, 2158173 }, { 14964243, 2169573 }, { 15331439, 2500198 }, { 15031347, 3685330 }, { 15020999, 3726196 }, { 14616409, 4376044 }, { 14602458, 4398453 }, { 14594311, 4405358 }, { 14358060, 4605591 }, { 14973020, 5452271 }, { 15245662, 5283768 }, { 15271287, 5277427 }, { 16014420, 5093552 }, { 16056481, 5096336 }, { 17276242, 5177094 }, { 17477040, 5628611 }, { 17483964, 5644181 }, { 16727991, 6604475 }, { 16701923, 6637589 }, { 16680060, 6652386 }, { 16046043, 7081528 }, { 16035789, 7084529 }, { 15738421, 7171570 }, { 15955998, 8195191 }, { 16273777, 8152008 }, { 16299760, 8156636 }, { 17053280, 8290848 }, { 17090572, 8310500 }, { 18172024, 8880417 }, { 18172024, 9391815 }, { 18134732, 9411467 }, { 17053280, 9981369 }, { 17027297, 9985997 }, { 16273777, 10120209 }, { 16263184, 10118770 }, { 15955998, 10077026 }, { 15738421, 11100647 }, { 16046043, 11190704 }, { 16067906, 11205502 }, { 16701923, 11634644 }, { 17457896, 12594938 }, { 17483964, 12628052 }, { 17283166, 13079569 }, { 17276242, 13095139 }, { 17234181, 13097923 }, { 16014420, 13178665 }, { 15988795, 13172324 }, { 15245662, 12988449 }, { 15236574, 12982832 }, { 14973020, 12819946 }, { 14358060, 13666641 }, { 14602458, 13873764 }, { 15007048, 14523627 }, { 15020999, 14546036 }, { 15321091, 15731152 }, { 15331439, 15772018 }, { 15318777, 15783419 }, { 14951581, 16114059 }, { 14912023, 16099496 }, { 13764842, 15677170 }, { 13744014, 15660952 }, { 13140002, 15190628 }, { 12959357, 14925887 }, { 12053161, 15449127 }, { 12187640, 15728230 }, { 12192277, 15737854 }, { 12297317, 16496013 }, { 12300939, 16522156 }, { 12093342, 17726920 }, { 12086183, 17768464 }, { 12069973, 17773722 }, { 11599898, 17926208 }, { 11569681, 17896820 }, { 10693399, 17044556 }, { 10333030, 16369337 }, { 10320603, 16346054 }, { 10263301, 16030502 }, { 9222459, 16140015 }, { 9231740, 16449664 }, { 9232060, 16460342 }, { 9019821, 17195859 }, { 9012502, 17221222 }, { 8332646, 18237183 }, { 8309203, 18272216 }, { 8292260, 18270438 }, { 7800922, 18218872 }, { 7347225, 17083700 }, { 7331580, 17044556 }, { 7276730, 16280940 }, { 7274839, 16254608 }, { 7350758, 15943314 }, { 6355663, 15619904 }, { 6238078, 15906603 }, { 6234023, 15916489 }, { 5741039, 16501967 }, { 5724040, 16522156 }, { 5688377, 16544630 }, { 4654144, 17196380 }, { 4639383, 17187857 }, { 4211318, 16940704 }, { 4258533, 15719288 }, { 4260162, 15677170 }, { 4520697, 14957214 }, { 4529681, 14932388 }, { 4725821, 14678955 }, { 3948022, 13978775 }, { 3716296, 14200317 }, { 3692552, 14211841 }, { 3003980, 14546036 }, { 2962267, 14552057 }, { 1752599, 14726654 }, { 1462059, 14326969 }, { 1452041, 14313187 }, { 1991943, 13216482 }, { 2010560, 13178665 }, { 2541453, 12627039 }, { 2559760, 12608017 }, { 2569167, 12602956 }, { 2841979, 12456177 }, { 2416404, 11500290 }, { 2114701, 11608368 }, { 2088313, 11609244 }, { 1323058, 11634644 }, { 1282503, 11623175 }, { 106399, 11290588 }, { 3546, 10807167 }, { -1, 10790497 }, { 32389, 10763526 }, { 971700, 9981369 }, { 996159, 9971434 }, { 1705482, 9683334 }, { 1716131, 9682534 }, { 2024962, 9659348 }, { 2024962, 8612869 }, { 1705482, 8588898 }, { 1681022, 8578963 }, { 971700, 8290848 }, { 939310, 8263878 }, { -1, 7481735 }, { 102852, 6998299 }, { 106399, 6981629 }, { 146954, 6970160 }, { 1323058, 6637589 }, { 1349446, 6638464 }, { 2114701, 6663849 }, { 2124758, 6667452 }, { 2416404, 6771927 }, { 2841979, 5816056 }, { 2559760, 5664200 }, { 2028867, 5112573 }, { 2010560, 5093552 }, { 1470658, 3996848 }, { 1452041, 3959030 }, { 1742580, 3559360 }, { 1752599, 3545578 }, { 1794312, 3551599 }, { 3003980, 3726196 }, { 3027724, 3737720 }, { 3716296, 4071915 }, { 3724020, 4079299 }, { 3948022, 4293442 }, { 4725822, 3593262 }, { 4536219, 3348276 }, { 4529681, 3339829 }, { 4269146, 2619873 }, { 4260162, 2595047 }, { 4212946, 1373645 }, { 4211318, 1331528 }, { 4226079, 1323005 }, { 4654144, 1075852 }, { 4689807, 1098325 }, { 5724040, 1750061 }, { 6217024, 2335539 }, { 6234023, 2355728 }, { 6355663, 2652329 }, { 7350759, 2328903 }, { 7277369, 2027985 }, { 7274839, 2017609 }, { 7329689, 1253993 }, { 7331580, 1227661 }, { 7785277, 92503 }, { 7800922, 53360 }, { 7817864, 51581 }, { 8309203, 0 } };
out.holes.emplace_back(Slic3r::Points({ {8982039, 9119734}, {8675233, 9160126}, {8654832, 9168577}, {8368934, 9287003}, {8351415, 9300446}, {8105907, 9488831}, {7917523, 9734328}, {7904081, 9751846}, {7785658, 10037750}, {7777208, 10058151}, {7736814, 10364949}, {7733932, 10386841}, {7774325, 10693653}, {7777208, 10715546}, {7895630, 11001450}, {7904081, 11021851}, {8092464, 11267363}, {8105907, 11284882}, {8123425, 11298325}, {8368934, 11486710}, {8389335, 11495160}, {8675233, 11613571}, {8697126, 11616453}, {9003932, 11656845}, {9025825, 11653963}, {9332633, 11613571}, {9353034, 11605121}, {9638932, 11486710}, {9656451, 11473267}, {9901958, 11284882}, {10090343, 11039370}, {10103786, 11021851}, {10222209, 10735947}, {10230659, 10715546}, {10271050, 10408734}, {10273932, 10386841}, {10233541, 10080043}, {10230659, 10058151}, {10112236, 9772247}, {10103786, 9751846}, {9915401, 9506349}, {9901958, 9488831}, {9884439, 9475388}, {9638932, 9287003}, {9618531, 9278552}, {9332633, 9160126}, {9310740, 9157244}, {9003932, 9116852} }));
out.holes.emplace_back(Slic3r::Points({ {5301863, 6863631}, {4995055, 6904022}, {4974654, 6912473}, {4688756, 7030899}, {4671237, 7044342}, {4425731, 7232727}, {4237345, 7478225}, {4223903, 7495743}, {4105480, 7781646}, {4097030, 7802048}, {4056638, 8108859}, {4053756, 8130753}, {4094147, 8437550}, {4097030, 8459442}, {4215452, 8745346}, {4223903, 8765747}, {4412288, 9011259}, {4425731, 9028778}, {4443249, 9042221}, {4688756, 9230606}, {4709157, 9239057}, {4995055, 9357483}, {5016948, 9360365}, {5323756, 9400757}, {5345649, 9397875}, {5652456, 9357483}, {5672856, 9349032}, {5958755, 9230606}, {5976273, 9217163}, {6221782, 9028778}, {6410165, 8783266}, {6423608, 8765747}, {6542031, 8479843}, {6550481, 8459442}, {6590874, 8152645}, {6593757, 8130753}, {6553363, 7823941}, {6550481, 7802048}, {6432058, 7516144}, {6423608, 7495743}, {6235224, 7250245}, {6221782, 7232727}, {6204263, 7219284}, {5958755, 7030899}, {5938354, 7022448}, {5652456, 6904022}, {5630563, 6901140}, {5323756, 6860749} }));
out.holes.emplace_back(Slic3r::Points({ {10306044, 5682112}, {9999236, 5722504}, {9978835, 5730953}, {9692937, 5849365}, {9675418, 5862808}, {9429912, 6051194}, {9241527, 6296691}, {9228084, 6314209}, {9109661, 6600113}, {9101211, 6620514}, {9060819, 6927326}, {9057937, 6949219}, {9098329, 7256016}, {9101211, 7277909}, {9219634, 7563812}, {9228084, 7584214}, {9416469, 7829725}, {9429912, 7847245}, {9447431, 7860687}, {9692937, 8049073}, {9713338, 8057523}, {9999236, 8175949}, {10021129, 8178831}, {10327937, 8219223}, {10349830, 8216341}, {10656638, 8175949}, {10677039, 8167498}, {10962937, 8049073}, {10980456, 8035630}, {11225963, 7847245}, {11414346, 7601733}, {11427789, 7584214}, {11546212, 7298310}, {11554662, 7277909}, {11595056, 6971111}, {11597938, 6949219}, {11557544, 6642407}, {11554662, 6620514}, {11436239, 6334610}, {11427789, 6314209}, {11239406, 6068712}, {11225963, 6051194}, {11208444, 6037751}, {10962937, 5849365}, {10942536, 5840915}, {10656638, 5722504}, {10634745, 5719621}, {10327937, 5679230} }));
return out;
}
// Contour from GH issue #2998.
static ExPolygon box_with_hole_close_to_wall()
{
ExPolygon out;
out.contour.points = { { 20000000, 20000000}, { 0, 20000000}, { 0, 0}, { 20000000, 0} };
out.holes.emplace_back(Slic3r::Points( {
{ 9905173, 501406}, { 9895707, 501967}, { 9715853, 512640}, { 9706437, 513762}, { 9527531, 535071}, { 9518198, 536749}, { 9340868, 568619}, { 9331651, 570846}, { 9156521, 613166},
{ 9147452, 615935}, { 8975137, 668555}, { 8966248, 671857}, { 8797352, 734593}, { 8788674, 738416}, { 8623792, 811047}, { 8615356, 815377}, { 8455065, 897648}, { 8446900, 902470},
{ 8291765, 994093}, { 8283900, 999390}, { 8134465, 1100042}, { 8126928, 1105796}, { 7983719, 1215124}, { 7976536, 1221315}, { 7840055, 1338934}, { 7833251, 1345539}, { 7703977, 1471037},
{ 7697576, 1478034}, { 7575964, 1610970}, { 7569989, 1618333}, { 7456466, 1758240}, { 7450937, 1765944}, { 7345902, 1912331}, { 7340840, 1920349}, { 7244661, 2072701}, { 7240082, 2081005},
{ 7153097, 2238787}, { 7149019, 2247348}, { 7071534, 2410005}, { 7067970, 2418793}, { 7000257, 2585755}, { 6997220, 2594738}, { 6939517, 2765418}, { 6937018, 2774565},
{ 6889527, 2948365}, { 6887574, 2957644}, { 6850462, 3133951}, { 6849062, 3143330}, { 6822461, 3321526}, { 6821618, 3330971}, { 6805620, 3510430}, { 6805339, 3519909},
{ 6800000, 3700000}, { 6800281, 3709478}, { 6805620, 3889570}, { 6806462, 3899015}, { 6822461, 4078474}, { 6823861, 4087853}, { 6850462, 4266049}, { 6852415, 4275328},
{ 6889527, 4451636}, { 6892027, 4460783}, { 6939517, 4634582}, { 6942554, 4643565}, { 7000257, 4814245}, { 7003821, 4823033}, { 7071534, 4989995}, { 7075612, 4998556},
{ 7153097, 5161214}, { 7157675, 5169518}, { 7244661, 5327300}, { 7249723, 5335318}, { 7345902, 5487670}, { 7351430, 5495374}, { 7456466, 5641761}, { 7462440, 5649124},
{ 7575964, 5789031}, { 7582365, 5796027}, { 7703977, 5928963}, { 7710780, 5935568}, { 7840055, 6061067}, { 7847238, 6067257}, { 7983719, 6184877}, { 7991256, 6190631},
{ 8134465, 6299958}, { 8142330, 6305255}, { 8291765, 6405907}, { 8299930, 6410729}, { 8455065, 6502352}, { 8463501, 6506682}, { 8623792, 6588953}, { 8632470, 6592776},
{ 8797352, 6665407}, { 8806241, 6668708}, { 8975137, 6731445}, { 8984206, 6734214}, { 9156521, 6786834}, { 9165738, 6789061}, { 9340868, 6831381}, { 9350201, 6833058},
{ 9527531, 6864929}, { 9536947, 6866050}, { 9715853, 6887360}, { 9725319, 6887921}, { 9905173, 6898595}, { 10094827, 6898595}, { 10104293, 6898033}, { 10284147, 6887360},
{ 10293563, 6886238}, { 10472469, 6864929}, { 10481802, 6863251}, { 10659132, 6831381}, { 10668349, 6829154}, { 10843479, 6786834}, { 10852548, 6784065}, { 11024863, 6731445},
{ 11033752, 6728143}, { 11202648, 6665407}, { 11211326, 6661584}, { 11376208, 6588953}, { 11384644, 6584623}, { 11544935, 6502352}, { 11553100, 6497530}, { 11708235, 6405907},
{ 11716100, 6400610}, { 11865535, 6299958}, { 11873072, 6294204}, { 12016281, 6184877}, { 12023464, 6178686}, { 12159946, 6061067}, { 12166750, 6054461}, { 12296023, 5928963},
{ 12302424, 5921966}, { 12424036, 5789031}, { 12430011, 5781667}, { 12543534, 5641761}, { 12549062, 5634056}, { 12654099, 5487670}, { 12659161, 5479651}, { 12755340, 5327300},
{ 12759918, 5318995}, { 12846903, 5161214}, { 12850981, 5152653}, { 12928466, 4989995}, { 12932030, 4981208}, { 12999743, 4814245}, { 13002780, 4805262}, { 13060483, 4634582},
{ 13062983, 4625434}, { 13110474, 4451636}, { 13112427, 4442356}, { 13149538, 4266049}, { 13150938, 4256670}, { 13177540, 4078474}, { 13178382, 4069029}, { 13194380, 3889570},
{ 13194661, 3880092}, { 13200000, 3700000}, { 13199719, 3690521}, { 13194380, 3510430}, { 13193538, 3500985}, { 13177540, 3321526}, { 13176140, 3312147}, { 13149538, 3133951},
{ 13147585, 3124672}, { 13110474, 2948365}, { 13107974, 2939217}, { 13060483, 2765418}, { 13057446, 2756435}, { 12999743, 2585755}, { 12996179, 2576968}, { 12928466, 2410005},
{ 12924388, 2401444}, { 12846903, 2238787}, { 12842325, 2230482}, { 12755340, 2072701}, { 12750278, 2064682}, { 12654099, 1912331}, { 12648571, 1904626}, { 12543534, 1758240},
{ 12537559, 1750876}, { 12424036, 1610970}, { 12417635, 1603973}, { 12296023, 1471037}, { 12289219, 1464432}, { 12159946, 1338934}, { 12152763, 1332744}, { 12016281, 1215124},
{ 12008744, 1209370}, { 11865535, 1100042}, { 11857670, 1094745}, { 11708235, 994093}, { 11700070, 989271}, { 11544935, 897648}, { 11536499, 893318}, { 11376208, 811047},
{ 11367530, 807224}, { 11202648, 734593}, { 11193759, 731291}, { 11024863, 668555}, { 11015794, 665786}, { 10843479, 613166}, { 10834262, 610939}, { 10659132, 568619},
{ 10649799, 566941}, { 10472469, 535071}, { 10463053, 533950}, { 10284147, 512640}, { 10274681, 512078}, { 10094827, 501406}
}));
return out;
}
// Contour from GH issue #2085.
static ExPolygon thin_ring()
{
ExPolygon out;
out.contour.points = {
{ 7805980, 147}, { 8182728, 9400}, { 8188694, 9840}, { 8564533, 37560}, { 8570470, 38292}, { 8944500, 84420}, { 8950394, 85443}, { 9321700, 149880},
{ 9327537, 151191}, { 9695240, 233760}, { 9701005, 235356}, { 10064220, 335870}, { 10069900, 337747}, { 10427740, 455960}, { 10433321, 458113}, { 10784930, 593740},
{ 10790399, 596164}, { 11134930, 748880}, { 11140273, 751570}, { 11476891, 921010}, { 11482096, 923959}, { 11810000, 1109720}, { 11815054, 1112921}, { 12133450, 1314540},
{ 12138341, 1317985}, { 12446450, 1534980}, { 12451166, 1538661}, { 12748270, 1770520}, { 12752800, 1774427}, { 13038160, 2020580}, { 13042492, 2024705}, { 13315430, 2284570},
{ 13575295, 2557508}, { 13579420, 2561840}, { 13825573, 2847201}, { 13829480, 2851730}, { 14061340, 3148834}, { 14065020, 3153550}, { 14282016, 3461660}, { 14285460, 3466550},
{ 14487080, 3784946}, { 14490280, 3790000}, { 14676041, 4117905}, { 14678990, 4123110}, { 14848430, 4459727}, { 14851120, 4465071}, { 15003836, 4809601}, { 15006260, 4815070},
{ 15141887, 5166679}, { 15144040, 5172261}, { 15262254, 5530100}, { 15264130, 5535780}, { 15364645, 5898995}, { 15366240, 5904761}, { 15448809, 6272464}, { 15450120, 6278301},
{ 15514557, 6649607}, { 15515580, 6655501}, { 15561709, 7029530}, { 15562441, 7035467}, { 15590160, 7411306}, { 15590600, 7417272}, { 15599853, 7794020}, { 15600000, 7800000},
{ 15590747, 8176748}, { 15590600, 8182728}, { 15562881, 8558567}, { 15562441, 8564533}, { 15516312, 8938563}, { 15515580, 8944500}, { 15451143, 9315806}, { 15450120, 9321700},
{ 15367551, 9689403}, { 15366240, 9695240}, { 15265725, 10058455}, { 15264130, 10064220}, { 15145916, 10422060}, { 15144040, 10427740}, { 15008413, 10779349}, { 15006260, 10784930},
{ 14853544, 11129461}, { 14851120, 11134930}, { 14681680, 11471548}, { 14678990, 11476891}, { 14493229, 11804795}, { 14490280, 11810000}, { 14288660, 12128396}, { 14285460, 12133450},
{ 14068464, 12441559}, { 14065020, 12446450}, { 13833160, 12743554}, { 13829480, 12748270}, { 13583327, 13033630}, { 13579420, 13038160}, { 13319555, 13311098}, { 13315430, 13315430},
{ 13311098, 13319555}, { 13038160, 13579420}, { 13033630, 13583327}, { 12748270, 13829480}, { 12743554, 13833160}, { 12446450, 14065020}, { 12441559, 14068464}, { 12133450, 14285460},
{ 12128396, 14288660}, { 11810000, 14490280}, { 11804795, 14493229}, { 11476891, 14678990}, { 11471548, 14681680}, { 11134930, 14851120}, { 11129461, 14853544}, { 10784930, 15006260},
{ 10779349, 15008413}, { 10427740, 15144040}, { 10422060, 15145916}, { 10064220, 15264130}, { 10058455, 15265725}, { 9695240, 15366240}, { 9689403, 15367551}, { 9321700, 15450120},
{ 9315806, 15451143}, { 8944500, 15515580}, { 8938563, 15516312}, { 8564533, 15562441}, { 8558567, 15562881}, { 8182728, 15590600}, { 8176748, 15590747}, { 7800000, 15600000},
{ 7794020, 15599853}, { 7417272, 15590600}, { 7411306, 15590160}, { 7035467, 15562441}, { 7029530, 15561709}, { 6655501, 15515580}, { 6649607, 15514557}, { 6278301, 15450120},
{ 6272464, 15448809}, { 5904761, 15366240}, { 5898995, 15364645}, { 5535780, 15264130}, { 5530100, 15262254}, { 5172261, 15144040}, { 5166679, 15141887}, { 4815070, 15006260},
{ 4809601, 15003836}, { 4465071, 14851120}, { 4459727, 14848430}, { 4123110, 14678990}, { 4117905, 14676041}, { 3790000, 14490280}, { 3784946, 14487080}, { 3466550, 14285460},
{ 3461660, 14282016}, { 3153550, 14065020}, { 3148834, 14061340}, { 2851730, 13829480}, { 2847201, 13825573}, { 2561840, 13579420}, { 2557508, 13575295}, { 2284570, 13315430},
{ 2024705, 13042492}, { 2020580, 13038160}, { 1774427, 12752800}, { 1770520, 12748270}, { 1538661, 12451166}, { 1534980, 12446450}, { 1317985, 12138341}, { 1314540, 12133450},
{ 1112921, 11815054}, { 1109720, 11810000}, { 923959, 11482096}, { 921010, 11476891}, { 751570, 11140273}, { 748880, 11134930}, { 596164, 10790399}, { 593740, 10784930},
{ 458113, 10433321}, { 455960, 10427740}, { 337747, 10069900}, { 335870, 10064220}, { 235356, 9701005}, { 233760, 9695240}, { 151191, 9327537}, { 149880, 9321700}, { 85443, 8950394},
{ 84420, 8944500}, { 38292, 8570470}, { 37560, 8564533}, { 9840, 8188694}, { 9400, 8182728}, { 147, 7805980}, { 0, 7800000}, { 9253, 7423252}, { 9400, 7417272}, { 37120, 7041433},
{ 37560, 7035467}, { 83688, 6661437}, { 84420, 6655501}, { 148858, 6284194}, { 149880, 6278301}, { 232450, 5910597}, { 233760, 5904761}, { 334275, 5541545}, { 335870, 5535780},
{ 454084, 5177940}, { 455960, 5172261}, { 591587, 4820651}, { 593740, 4815070}, { 746456, 4470539}, { 748880, 4465071}, { 918320, 4128453}, { 921010, 4123110}, { 1106772, 3795205},
{ 1109720, 3790000}, { 1311340, 3471604}, { 1314540, 3466550}, { 1531536, 3158441}, { 1534980, 3153550}, { 1766840, 2856446}, { 1770520, 2851730}, { 2016673, 2566370}, { 2020580, 2561840},
{ 2280445, 2288903}, { 2284570, 2284570}, { 2288903, 2280445}, { 2561840, 2020580}, { 2566370, 2016673}, { 2851730, 1770520}, { 2856446, 1766840}, { 3153550, 1534980}, { 3158441, 1531536},
{ 3466550, 1314540}, { 3471604, 1311340}, { 3790000, 1109720}, { 3795205, 1106772}, { 4123110, 921010}, { 4128453, 918320}, { 4465071, 748880}, { 4470539, 746456}, { 4815070, 593740},
{ 4820651, 591587}, { 5172261, 455960}, { 5177940, 454084}, { 5535780, 335870}, { 5541545, 334275}, { 5904761, 233760}, { 5910597, 232450}, { 6278301, 149880}, { 6284194, 148858},
{ 6655501, 84420}, { 6661437, 83688}, { 7035467, 37560}, { 7041433, 37120}, { 7417272, 9400}, { 7423252, 9253}, { 7800000, 0}
};
out.holes.emplace_back(Slic3r::Points( {
{ 7794921, 1002175}, { 7466441, 1010240}, { 7461374, 1010614}, { 7133685, 1034780}, { 7128642, 1035402}, { 6802534, 1075630}, { 6797528, 1076499}, { 6473790, 1132670},
{ 6468832, 1133784}, { 6148230, 1205780}, { 6143333, 1207135}, { 5826660, 1294770}, { 5821835, 1296364}, { 5509840, 1399430}, { 5505100, 1401259}, { 5198540, 1519510},
{ 5193895, 1521569}, { 4893501, 1654720}, { 4888962, 1657005}, { 4595471, 1804740}, { 4591050, 1807245}, { 4305150, 1969200}, { 4300857, 1971918}, { 4023260, 2147710},
{ 4019106, 2150636}, { 3750470, 2339831}, { 3746465, 2342956}, { 3487430, 2545110}, { 3483583, 2548429}, { 3234780, 2763050}, { 3231100, 2766553}, { 2993120, 2993120},
{ 2766553, 3231100}, { 2763050, 3234780}, { 2548429, 3483583}, { 2545110, 3487430}, { 2342956, 3746465}, { 2339831, 3750470}, { 2150636, 4019106}, { 2147710, 4023260},
{ 1971918, 4300857}, { 1969200, 4305150}, { 1807245, 4591050}, { 1804740, 4595471}, { 1657005, 4888962}, { 1654720, 4893501}, { 1521569, 5193895}, { 1519510, 5198540},
{ 1401259, 5505100}, { 1399430, 5509840}, { 1296364, 5821835}, { 1294770, 5826660}, { 1207135, 6143333}, { 1205780, 6148230}, { 1133784, 6468832}, { 1132670, 6473790},
{ 1076499, 6797528}, { 1075630, 6802534}, { 1035402, 7128642}, { 1034780, 7133685}, { 1010614, 7461374}, { 1010240, 7466441}, { 1002175, 7794921}, { 1002050, 7800000},
{ 1010115, 8128480}, { 1010240, 8133559}, { 1034406, 8461248}, { 1034780, 8466315}, { 1075008, 8792423}, { 1075630, 8797466}, { 1131802, 9121204}, { 1132670, 9126210},
{ 1204667, 9446812}, { 1205780, 9451770}, { 1293415, 9768443}, { 1294770, 9773340}, { 1397836, 10085335}, { 1399430, 10090160}, { 1517682, 10396721}, { 1519510, 10401461},
{ 1652661, 10701855}, { 1654720, 10706500}, { 1802456, 10999992}, { 1804740, 11004530}, { 1966696, 11290429}, { 1969200, 11294850}, { 2144992, 11572447}, { 2147710, 11576740},
{ 2336905, 11845376}, { 2339831, 11849530}, { 2541984, 12108564}, { 2545110, 12112570}, { 2759731, 12361373}, { 2763050, 12365220}, { 2989617, 12603200}, { 2993120, 12606880},
{ 2996800, 12610383}, { 3234780, 12836950}, { 3238628, 12840269}, { 3487430, 13054890}, { 3491436, 13058016}, { 3750470, 13260170}, { 3754624, 13263096}, { 4023260, 13452290},
{ 4027553, 13455008}, { 4305150, 13630800}, { 4309571, 13633304}, { 4595471, 13795260}, { 4600009, 13797544}, { 4893501, 13945280}, { 4898146, 13947339}, { 5198540, 14080490},
{ 5203280, 14082319}, { 5509840, 14200570}, { 5514665, 14202164}, { 5826660, 14305230}, { 5831557, 14306585}, { 6148230, 14394220}, { 6153188, 14395333}, { 6473790, 14467330},
{ 6478796, 14468199}, { 6802534, 14524370}, { 6807577, 14524992}, { 7133685, 14565220}, { 7138752, 14565594}, { 7466441, 14589760}, { 7471520, 14589885}, { 7800000, 14597950},
{ 7805079, 14597825}, { 8133559, 14589760}, { 8138626, 14589386}, { 8466315, 14565220}, { 8471358, 14564598}, { 8797466, 14524370}, { 8802472, 14523501}, { 9126210, 14467330},
{ 9131168, 14466217}, { 9451770, 14394220}, { 9456667, 14392865}, { 9773340, 14305230}, { 9778165, 14303636}, { 10090160, 14200570}, { 10094900, 14198741}, { 10401461, 14080490},
{ 10406106, 14078431}, { 10706500, 13945280}, { 10711038, 13942996}, { 11004530, 13795260}, { 11008951, 13792756}, { 11294850, 13630800}, { 11299143, 13628082}, { 11576740, 13452290},
{ 11580894, 13449364}, { 11849530, 13260170}, { 11853536, 13257044}, { 12112570, 13054890}, { 12116417, 13051571}, { 12365220, 12836950}, { 12368900, 12833447}, { 12606880, 12606880},
{ 12833447, 12368900}, { 12836950, 12365220}, { 13051571, 12116417}, { 13054890, 12112570}, { 13257044, 11853536}, { 13260170, 11849530}, { 13449364, 11580894}, { 13452290, 11576740},
{ 13628082, 11299143}, { 13630800, 11294850}, { 13792756, 11008951}, { 13795260, 11004530}, { 13942996, 10711038}, { 13945280, 10706500}, { 14078431, 10406106}, { 14080490, 10401461},
{ 14198741, 10094900}, { 14200570, 10090160}, { 14303636, 9778165}, { 14305230, 9773340}, { 14392865, 9456667}, { 14394220, 9451770}, { 14466217, 9131168}, { 14467330, 9126210},
{ 14523501, 8802472}, { 14524370, 8797466}, { 14564598, 8471358}, { 14565220, 8466315}, { 14589386, 8138626}, { 14589760, 8133559}, { 14597825, 7805079}, { 14597950, 7800000},
{ 14589885, 7471520}, { 14589760, 7466441}, { 14565594, 7138752}, { 14565220, 7133685}, { 14524992, 6807577}, { 14524370, 6802534}, { 14468199, 6478796}, { 14467330, 6473790},
{ 14395333, 6153188}, { 14394220, 6148230}, { 14306585, 5831557}, { 14305230, 5826660}, { 14202164, 5514665}, { 14200570, 5509840}, { 14082319, 5203280}, { 14080490, 5198540},
{ 13947339, 4898146}, { 13945280, 4893501}, { 13797544, 4600009}, { 13795260, 4595471}, { 13633304, 4309571}, { 13630800, 4305150}, { 13455008, 4027553}, { 13452290, 4023260},
{ 13263096, 3754624}, { 13260170, 3750470}, { 13058016, 3491436}, { 13054890, 3487430}, { 12840269, 3238628}, { 12836950, 3234780}, { 12610383, 2996800}, { 12606880, 2993120},
{ 12603200, 2989617}, { 12365220, 2763050}, { 12361373, 2759731}, { 12112570, 2545110}, { 12108564, 2541984}, { 11849530, 2339831}, { 11845376, 2336905}, { 11576740, 2147710},
{ 11572447, 2144992}, { 11294850, 1969200}, { 11290429, 1966696}, { 11004530, 1804740}, { 10999992, 1802456}, { 10706500, 1654720}, { 10701855, 1652661}, { 10401461, 1519510},
{ 10396721, 1517682}, { 10090160, 1399430}, { 10085335, 1397836}, { 9773340, 1294770}, { 9768443, 1293415}, { 9451770, 1205780}, { 9446812, 1204667}, { 9126210, 1132670},
{ 9121204, 1131802}, { 8797466, 1075630}, { 8792423, 1075008}, { 8466315, 1034780}, { 8461248, 1034406}, { 8133559, 1010240}, { 8128480, 1010115}, { 7800000, 1002050}
} ));
return out;
}
static ExPolygon vase_with_fins()
{
ExPolygon out;
out.contour.points = {
{27431106, 489754}, {27436907, 489850}, {27457500, 489724}, {27457500, 5510510}, {28343327, 5565859}, {28351400, 5566288}, {28389945, 5568336}, {28394790, 5568765}, {28420177, 5571613}, {28901163, 5629918},
{29903776, 5750412}, {30416384, 2513976}, {30682801, 831878}, {30688548, 795593}, {31507808, 939183}, {31513523, 940185}, {31533883, 943282}, {30775577, 5731079}, {30768824, 5773720}, {30748466, 5902252},
{31614726, 6095505}, {31622633, 6097191}, {31660382, 6105244}, {31665100, 6106426}, {31689729, 6113210}, {32155671, 6246039}, {33127094, 6521893}, {34139670, 3405493}, {34665944, 1785782}, {34677296, 1750843},
{35464012, 2020824}, {35469500, 2022707}, {35489124, 2028950}, {33991170, 6639179}, {33977829, 6680238}, {33937615, 6804003}, {34762987, 7130382}, {34770532, 7133285}, {34806557, 7147144}, {34811033, 7149049},
{34834297, 7159603}, {35273721, 7363683}, {36190026, 7788101}, {37677657, 4868472}, {38450834, 3351031}, {38467513, 3318298}, {39202308, 3708028}, {39207434, 3710747}, {39225840, 3719984}, {37025125, 8039112},
{37005525, 8077579}, {36946446, 8193529}, {37710592, 8645011}, {37717591, 8649059}, {37751004, 8668383}, {37755126, 8670965}, {37776453, 8685028}, {38178545, 8955338}, {39017176, 9517879}, {40943217, 6866906},
{41944249, 5489097}, {41965843, 5459376}, {42630625, 5959265}, {42635262, 5962752}, {42651996, 5974755}, {39802725, 9896448}, {39777349, 9931375}, {39700858, 10036656}, {40384973, 10602104}, {40391252, 10607196},
{40421232, 10631509}, {40424899, 10634704}, {40443764, 10651931}, {40798616, 10981815}, {41538921, 11668622}, {43855948, 9351592}, {45060194, 8147345}, {45086172, 8121368}, {45664563, 8719082}, {45668598, 8723251},
{45683249, 8737724}, {42255579, 12165422}, {42225051, 12195949}, {42133032, 12287968}, {42720262, 12953467}, {42725667, 12959479}, {42751474, 12988183}, {42754596, 12991912}, {42770534, 13011877}, {43069412, 13393211},
{43693167, 14187377}, {46344137, 12261333}, {47721948, 11260299}, {47751670, 11238705}, {48229435, 11919543}, {48232767, 11924292}, {48244974, 11940879}, {44323286, 14790155}, {44288359, 14815531}, {44183078, 14892022},
{44658973, 15641210}, {44663371, 15647994}, {44684370, 15680381}, {44686871, 15684553}, {44699489, 15706766}, {44935035, 16130156}, {45426863, 17012121}, {48346505, 15524481}, {49863946, 14751306}, {49896680, 14734627},
{50262068, 15481841}, {50264616, 15487053}, {50274078, 15505344}, {45954933, 17706046}, {45916466, 17725646}, {45800515, 17784726}, {46153358, 18599135}, {46156641, 18606523}, {46172315, 18641796}, {46174132, 18646308},
{46183120, 18670221}, {46349534, 19125250}, {46697342, 20073284}, {49813754, 19060715}, {51433464, 18534440}, {51468404, 18523087}, {51712400, 19318239}, {51714102, 19323786}, {51720585, 19343332}, {47110355, 20841293},
{47069295, 20854634}, {46945530, 20894847}, {47166614, 21754409}, {47168701, 21762220}, {47178664, 21799510}, {47179753, 21804251}, {47184889, 21829276}, {47278074, 22304738}, {47473309, 23295520}, {50709741, 22782917},
{52391837, 22516497}, {52428122, 22510750}, {52544737, 23334291}, {52545550, 23340036}, {52548897, 23360356}, {47761090, 24118668}, {47718449, 24125422}, {47589917, 24145780}, {47673812, 25029360}, {47674651, 25037401},
{47678657, 25075792}, {47678992, 25080644}, {47680151, 25106164}, {47697809, 25590347}, {47735642, 26599468}, {52752230, 26599468}, {52738564, 27431106}, {52738469, 27436907}, {52738595, 27457500}, {47717808, 27457500},
{47662461, 28343321}, {47662032, 28351394}, {47659983, 28389938}, {47659554, 28394784}, {47656706, 28420171}, {47598401, 28901157}, {47477907, 29903774}, {50714338, 30416378}, {52396434, 30682795}, {52432719, 30688542},
{52289144, 31507800}, {52288143, 31513515}, {52285046, 31533875}, {47497239, 30775569}, {47454598, 30768816}, {47326067, 30748458}, {47132809, 31614720}, {47131122, 31622626}, {47123069, 31660376}, {47121887, 31665094},
{47115103, 31689724}, {46982279, 32155664}, {46706424, 33127087}, {49822834, 34139662}, {51442545, 34665936}, {51477485, 34677289}, {51207490, 35464012}, {51205607, 35469500}, {51199363, 35489124}, {46589140, 33991162},
{46548081, 33977821}, {46424316, 33937607}, {46097945, 34762979}, {46095042, 34770524}, {46081183, 34806549}, {46079278, 34811025}, {46068724, 34834289}, {45864641, 35273715}, {45440218, 36190023}, {48359847, 37677651},
{49877288, 38450826}, {49910022, 38467505}, {49520291, 39202300}, {49517572, 39207426}, {49508336, 39225832}, {45189199, 37025117}, {45150732, 37005517}, {45034781, 36946438}, {44583309, 37710592}, {44579262, 37717591},
{44559938, 37751004}, {44557356, 37755126}, {44543292, 37776453}, {44272982, 38178543}, {43710441, 39017170}, {46361413, 40943214}, {47739222, 41944249}, {47768943, 41965843}, {47269053, 42630624}, {47265566, 42635262},
{47253564, 42651996}, {43331872, 39802717}, {43296945, 39777341}, {43191664, 39700850}, {42626221, 40384973}, {42621129, 40391252}, {42596816, 40421232}, {42593621, 40424899}, {42576394, 40443764}, {42246510, 40798616},
{41559699, 41538918}, {43876735, 43855948}, {45080983, 45060194}, {45106960, 45086172}, {44509231, 45664571}, {44505061, 45668605}, {44490589, 45683256}, {41062903, 42255578}, {40940357, 42133032}, {40274856, 42720258},
{40268844, 42725663}, {40240140, 42751470}, {40236411, 42754592}, {40216446, 42770530}, {39835112, 43069407}, {39040953, 43693161}, {40966991, 46344124}, {41968025, 47721932}, {41989619, 47751654}, {41308783, 48229434},
{41304034, 48232767}, {41287447, 48244973}, {38438168, 44323278}, {38412792, 44288351}, {38336302, 44183071}, {37587122, 44658973}, {37580338, 44663371}, {37547951, 44684370}, {37543779, 44686871}, {37521566, 44699489},
{37098171, 44935029}, {36216213, 45426864}, {37703841, 48346500}, {38477019, 49863946}, {38493698, 49896680}, {37746484, 50262052}, {37741272, 50264600}, {37722981, 50274062}, {35522285, 45954933}, {35502686, 45916466},
{35443606, 45800515}, {34629191, 46153350}, {34621803, 46156633}, {34586530, 46172307}, {34582018, 46174124}, {34558105, 46183112}, {34103078, 46349526}, {33155041, 46697341}, {34167619, 49813746}, {34693894, 51433456},
{34705246, 51468395}, {33910086, 51712399}, {33904540, 51714102}, {33884994, 51720585}, {32387039, 47110355}, {32373698, 47069295}, {32333485, 46945530}, {31473915, 47166622}, {31466104, 47168709}, {31428813, 47178672},
{31424073, 47179760}, {31399048, 47184897}, {30923586, 47278079}, {29932800, 47473310}, {30445407, 50709741}, {30711827, 52391837}, {30717574, 52428122}, {29894033, 52544729}, {29888288, 52545543}, {29867968, 52548889},
{29109657, 47761082}, {29102904, 47718441}, {29082546, 47589909}, {28198964, 47673827}, {28190923, 47674666}, {28152532, 47678673}, {28147680, 47679007}, {28122160, 47680166}, {27637977, 47697820}, {26628861, 47735648},
{26628861, 51012422}, {26628864, 52715485}, {26628864, 52752222}, {25797210, 52738556}, {25791409, 52738461}, {25770816, 52738587}, {25770816, 47717800}, {24884998, 47662453}, {24876924, 47662024}, {24838380, 47659975},
{24833534, 47659546}, {24808147, 47656698}, {24327161, 47598396}, {23324548, 47477901}, {22811940, 50714338}, {22545523, 52396434}, {22539776, 52432719}, {21720525, 52289129}, {21714811, 52288127}, {21694451, 52285030},
{22452755, 47497223}, {22459508, 47454583}, {22479866, 47326051}, {21613606, 47132816}, {21605699, 47131129}, {21567950, 47123077}, {21563232, 47121895}, {21538602, 47115110}, {21072662, 46982279}, {20101239, 46706425},
{19088664, 49822824}, {18562390, 51442538}, {18551037, 51477477}, {17764314, 51207498}, {17758826, 51205614}, {17739202, 51199371}, {19237154, 46589140}, {19250495, 46548081}, {19290709, 46424316}, {18465339, 46097937},
{18457794, 46095035}, {18421769, 46081175}, {18417293, 46079270}, {18394029, 46068716}, {17954603, 45864634}, {17038299, 45440211}, {15550671, 48359845}, {14777498, 49877288}, {14760820, 49910022}, {14026023, 49520291},
{14020897, 49517572}, {14002491, 49508335}, {16203201, 45189191}, {16222801, 45150724}, {16281880, 45034773}, {15517740, 44583309}, {15510741, 44579261}, {15477328, 44559938}, {15473206, 44557356}, {15451878, 44543292},
{15049787, 44272982}, {14211153, 43710440}, {12285115, 46361403}, {11284082, 47739206}, {11262488, 47768928}, {10597703, 47269053}, {10593066, 47265566}, {10576332, 47253563}, {13425609, 43331872}, {13450985, 43296945},
{13527476, 43191664}, {12843352, 42626213}, {12837073, 42621121}, {12807093, 42596808}, {12803426, 42593613}, {12784561, 42576386}, {12429709, 42246502}, {11689410, 41559693}, {9372373, 43876727}, {8168126, 45080975},
{8142148, 45106952}, {7563757, 44509222}, {7559722, 44505053}, {7545071, 44490581}, {10972747, 41062911}, {11003274, 41032383}, {11095293, 40940365}, {10508063, 40274848}, {10502658, 40268836}, {10476851, 40240132},
{10473729, 40236403}, {10457791, 40216438}, {10158911, 39835107}, {9535160, 39040950}, {6884192, 40966991}, {5506386, 41968025}, {5476665, 41989618}, {4998885, 41308775}, {4995553, 41304026}, {4983346, 41287439},
{8905039, 38438168}, {8939966, 38412792}, {9045247, 38336301}, {8569356, 37587114}, {8564958, 37580330}, {8543959, 37547943}, {8541458, 37543771}, {8528840, 37521558}, {8293293, 37098166}, {7801454, 36216208},
{4881822, 37703836}, {3364381, 38477011}, {3331647, 38493690}, {2966260, 37746484}, {2963712, 37741272}, {2954250, 37722981}, {7273379, 35522270}, {7311845, 35502670}, {7427796, 35443590}, {7074968, 34629191},
{7071686, 34621803}, {7056012, 34586530}, {7054194, 34582018}, {7045206, 34558105}, {6878792, 34103076}, {6530980, 33155036}, {3414573, 34167611}, {1794864, 34693885}, {1759924, 34705238}, {1515921, 33910079},
{1514219, 33904532}, {1507735, 33884986}, {6117964, 32387033}, {6159023, 32373692}, {6282789, 32333479}, {6061704, 31473909}, {6059617, 31466099}, {6049654, 31428807}, {6048565, 31424067}, {6043429, 31399042},
{5950245, 30923582}, {5755014, 29932799}, {2518579, 30445403}, {836483, 30711821}, {800198, 30717568}, {683591, 29894033}, {682777, 29888288}, {679431, 29867968}, {5467236, 29109657}, {5509877, 29102904}, {5638409, 29082546},
{5554499, 28198964}, {5553660, 28190923}, {5549653, 28152532}, {5549319, 28147680}, {5548160, 28122159}, {5530507, 27637975}, {5492679, 26628853}, {2215900, 26628853}, {512834, 26628856}, {476096, 26628856}, {489754, 25797218},
{489850, 25791417}, {489724, 25770824}, {5510510, 25770824}, {5565867, 24884990}, {5566296, 24876916}, {5568344, 24838372}, {5568773, 24833527}, {5571621, 24808139}, {5629923, 24327156}, {5750418, 23324543}, {2513981, 22811940},
{831886, 22545523}, {795600, 22539776}, {939191, 21720518}, {940192, 21714803}, {943289, 21694443}, {5731087, 22452754}, {5773728, 22459508}, {5902260, 22479865}, {6095512, 21613598}, {6097199, 21605691}, {6105252, 21567942},
{6106434, 21563224}, {6113218, 21538594}, {6246044, 21072654}, {6521898, 20101231}, {3405493, 19088662}, {1785783, 18562390}, {1750843, 18551037}, {2020831, 17764306}, {2022714, 17758819}, {2028958, 17739194}, {6639187, 19237147},
{6680246, 19250488}, {6804011, 19290701}, {7130382, 18465339}, {7133285, 18457794}, {7147144, 18421769}, {7149049, 18417293}, {7159603, 18394029}, {7363683, 17954605}, {7788110, 17038301}, {4868477, 15550669}, {3351039, 14777491},
{3318305, 14760812}, {3708029, 14026016}, {3710747, 14020890}, {3719984, 14002484}, {8039120, 16203201}, {8077586, 16222801}, {8193537, 16281881}, {8645019, 15517733}, {8649067, 15510734}, {8668391, 15477321}, {8670973, 15473199},
{8685036, 15451871}, {8955346, 15049780}, {9517887, 14211149}, {6866919, 12285108}, {5489112, 11284075}, {5459391, 11262481}, {5959259, 10597695}, {5962745, 10593058}, {5974747, 10576324}, {9896454, 13425601}, {9931382, 13450977},
{10036663, 13527468}, {10602111, 12843352}, {10607203, 12837073}, {10631516, 12807093}, {10634711, 12803426}, {10651937, 12784561}, {10981820, 12429709}, {11668626, 11689407}, {8147345, 8168126}, {8121368, 8142148}, {8719089, 7563749},
{8723258, 7559715}, {8737731, 7545064}, {12165414, 10972746}, {12195941, 11003274}, {12287960, 11095293}, {12953467, 10508056}, {12959479, 10502650}, {12988183, 10476843}, {12991912, 10473721}, {13011878, 10457783}, {13393211, 10158903},
{14187378, 9535150}, {12261338, 6884179}, {11260306, 5506371}, {11238712, 5476650}, {11919550, 4998885}, {11924299, 4995552}, {11940886, 4983346}, {14790161, 8905032}, {14815537, 8939959}, {14892028, 9045240}, {15641210, 8569348},
{15647994, 8564950}, {15680381, 8543951}, {15684553, 8541450}, {15706766, 8528832}, {16130159, 8293285}, {17012123, 7801449}, {15524489, 4881814}, {14751314, 3364373}, {14734635, 3331640}, {15481841, 2966253}, {15487053, 2963704},
{15505344, 2954242}, {17706054, 7273386}, {17725654, 7311852}, {17784734, 7427803}, {18599135, 7074961}, {18606523, 7071678}, {18641796, 7056004}, {18646308, 7054187}, {18670222, 7045199}, {19125250, 6878787}, {20073289, 6530975},
{19060715, 3414573}, {18534440, 1794864}, {18523088, 1759924}, {19318247, 1515921}, {19323794, 1514219}, {19343340, 1507736}, {20841293, 6117964}, {20854634, 6159023}, {20894848, 6282789}, {21754417, 6061696}, {21762228, 6059609},
{21799518, 6049647}, {21804259, 6048557}, {21829284, 6043421}, {22304743, 5950237}, {23295525, 5755007}, {22782917, 2518572}, {22516497, 836476}, {22510750, 800190}, {23334299, 683591}, {23340043, 682777}, {23360363, 679431},
{24118676, 5467229}, {24125430, 5509869}, {24145787, 5638402}, {25029368, 5554507}, {25037409, 5553668}, {25075799, 5549661}, {25080652, 5549327}, {25106172, 5548168}, {25590355, 5530509}, {26599476, 5492671}, {26599476, 476096}
};
return out;
}
static ExPolygon contour_with_hole()
{
ExPolygon out;
out.contour.points = {
{ 23302819, 108248}, { 23410179, 157624}, { 23451825, 176777}, { 24106418, 478750}, { 24704172, 811512}, { 24883849, 911534}, { 25980045, 1530217}, { 26591038, 1897423}, { 26829981, 2041022}, { 27158523, 2249848}, { 27618921, 2584465},
{ 27896903, 2786507}, { 28144524, 2978990}, { 28815685, 3551061}, { 28909975, 3628821}, { 29371498, 4009409}, { 29402087, 4037084}, { 29493584, 4119861}, { 29765627, 4382947}, { 30607836, 5197449}, { 30934687, 5508413}, { 31019374, 5593546},
{ 31075807, 5655861}, { 31235879, 5823254}, { 31667505, 6274618}, { 31976596, 6656087}, { 32328364, 7055603}, { 32440973, 7183484}, { 32491346, 7249288}, { 33179667, 8148478}, { 33575401, 8717521}, { 33835875, 9075811}, { 34010014, 9315332},
{ 34304500, 9781688}, { 34369165, 9898535}, { 34397842, 9950359}, { 34494651, 10316439}, { 34501993, 10344190}, { 34385828, 10617514}, { 34331252, 10651174}, { 34084812, 10803186}, { 33894353, 10899665}, { 33398927, 11326583},
{ 33183121, 11494200}, { 32195826, 12261037}, { 31686925, 12719913}, { 31571718, 12807396}, { 31250995, 13050935}, { 31207108, 13086856}, { 31130381, 13149671}, { 31070741, 13206732}, { 30967095, 13305896}, { 30228082, 14071658},
{ 30116771, 14212337}, { 30044101, 14304176}, { 29567520, 14906137}, { 29043350, 15664879}, { 28911161, 15871189}, { 28855871, 15957479}, { 28714334, 16227582}, { 28650159, 16350050}, { 28364584, 16899765}, { 28240857, 17235607},
{ 28151371, 17509658}, { 28114198, 17623503}, { 28309361, 17730441}, { 28370394, 17763884}, { 28488974, 17847025}, { 28525745, 17872806}, { 29082248, 18281292}, { 29152930, 18376480}, { 29168058, 18396855}, { 29173722, 18656366},
{ 29176206, 18770149}, { 29167406, 18857292}, { 29104337, 19029141}, { 29049428, 19178752}, { 28907061, 19434701}, { 28857790, 19523283}, { 28715480, 19775043}, { 28630622, 20043684}, { 28609342, 20111052}, { 28573760, 20267045},
{ 28403454, 21103762}, { 28370165, 21230085}, { 28332310, 21373746}, { 28315057, 21418891}, { 28294569, 21472487}, { 28334157, 21579715}, { 28561468, 21814880}, { 28854906, 22118451}, { 29225599, 22499341}, { 29285205, 22617454},
{ 29324833, 22695983}, { 29313473, 22800767}, { 29312583, 22808982}, { 29272380, 22876835}, { 28829469, 23460472}, { 28817999, 23488286}, { 28796393, 23540675}, { 28775618, 23627381}, { 28732328, 23808034}, { 28661140, 24177335},
{ 28645731, 24834289}, { 28625222, 25202417}, { 28579034, 26031478}, { 28586310, 26420529}, { 28633240, 26560504}, { 28664456, 26653603}, { 28740916, 26788014}, { 28797005, 26886614}, { 28812464, 26950783}, { 28858428, 27009579},
{ 28975940, 26859631}, { 29022419, 26805440}, { 29115451, 26696972}, { 29135739, 26685915}, { 29155135, 26675346}, { 29408332, 26616458}, { 29592642, 26573591}, { 29614928, 26568091}, { 29711634, 26559197}, { 30723503, 26466299},
{ 31183646, 26470661}, { 31550568, 26550771}, { 31777556, 26600329}, { 32014697, 26671604}, { 32334931, 26854665}, { 32449353, 26920987}, { 32657873, 27041843}, { 32701539, 27084927}, { 32750872, 27133602}, { 33434549, 27790306},
{ 33487600, 27817659}, { 33548673, 27849142}, { 33793150, 28109624}, { 33877574, 28164293}, { 33965395, 28221161}, { 33999067, 28249986}, { 34024398, 28271673}, { 34059690, 28329572}, { 34087359, 28374972}, { 34181544, 28710471},
{ 34170186, 28732578}, { 34134947, 28801161}, { 34092867, 29064916}, { 33950784, 29233310}, { 33878646, 29318804}, { 33721956, 29672399}, { 33660358, 29727949}, { 33620108, 29764243}, { 33393624, 30270577}, { 33094597, 30771032},
{ 33063116, 30812704}, { 32973928, 30930779}, { 32608081, 31341847}, { 32393317, 31544017}, { 32206520, 31719862}, { 31997581, 31894374}, { 31972538, 31942583}, { 32059002, 32025240}, { 32171917, 32133182}, { 32501317, 32311025},
{ 32715593, 32426714}, { 32802065, 32479231}, { 32956210, 32574312}, { 33249042, 32770899}, { 33946833, 33239350}, { 34445301, 33680139}, { 34778020, 33974357}, { 35230994, 34391224}, { 35341113, 34460366}, { 35450459, 34529022},
{ 35625170, 34673345}, { 35764733, 34757179}, { 35775747, 34633947}, { 35846476, 34564107}, { 35965365, 34446723}, { 36038088, 34379954}, { 36151170, 34276133}, { 36426218, 34106680}, { 36531666, 34187969}, { 36695885, 34314565},
{ 37011093, 34586835}, { 37067557, 34150814}, { 37052506, 33989541}, { 37037043, 33823855}, { 37069574, 33661923}, { 37083653, 33591851}, { 37186706, 33497192}, { 37521634, 33288703}, { 37617140, 33275082}, { 37684699, 33219614},
{ 37821418, 33228393}, { 37938489, 33235910}, { 38091617, 33138918}, { 38155158, 33060873}, { 38213556, 32989142}, { 38727086, 32659362}, { 38746459, 32654507}, { 38809135, 32638806}, { 38820634, 32624462}, { 38855007, 32581573},
{ 39134002, 32235481}, { 39392850, 32163442}, { 39569189, 32115608}, { 39686862, 32083692}, { 39744314, 32146839}, { 39840707, 31963655}, { 39973169, 31711932}, { 40025735, 31592644}, { 40157184, 31465080}, { 40313010, 31313863},
{ 40390192, 31223588}, { 40418596, 31230809}, { 40594404, 31186692}, { 40732045, 31068306}, { 40746151, 30846139}, { 40761255, 30608300}, { 40853394, 30223426}, { 40876768, 30095588}, { 40895496, 29993166}, { 40968240, 29949606},
{ 41197066, 29989787}, { 41412367, 30027591}, { 41472384, 29977101}, { 41695297, 29659954}, { 41890516, 29382211}, { 42157410, 28987811}, { 42408947, 28616097}, { 42669462, 28292349}, { 42683144, 28275345}, { 42919982, 27924149},
{ 43162781, 27628506}, { 43527344, 27260325}, { 43847191, 27036250}, { 44057061, 26922424}, { 44231096, 26828037}, { 44301999, 26795490}, { 44327421, 26804561}, { 44319287, 26913761}, { 44143507, 27648484}, { 44107324, 27729499},
{ 44074236, 27803580}, { 44025541, 27932083}, { 43944121, 28146941}, { 43877811, 28710269}, { 43895199, 28764671}, { 43933238, 28883702}, { 43919165, 29004140}, { 43888109, 29269841}, { 43825852, 29576752}, { 43811824, 29609468},
{ 43748820, 29756420}, { 43763658, 29837769}, { 43832567, 30215488}, { 44075125, 29807258}, { 44209233, 29804204}, { 44310228, 29813855}, { 44365586, 29958259}, { 43873534, 30271247}, { 44003187, 30330249}, { 44617279, 30687869},
{ 44694113, 31070182}, { 44941015, 31257544}, { 45130334, 31171398}, { 45147836, 31132029}, { 45242053, 31070592}, { 45345637, 31033061}, { 45565937, 30953238}, { 45609517, 30857448}, { 45651888, 30764320}, { 45660681, 30754094},
{ 45822750, 30772646}, { 45944979, 30753042}, { 45964326, 30749938}, { 46054945, 30795588}, { 46577640, 31130668}, { 46870296, 31313313}, { 46976414, 31379541}, { 46998128, 31406087}, { 47008874, 31439291}, { 47031018, 31569281},
{ 47031214, 31576854}, { 47036334, 31774677}, { 47193705, 31889293}, { 47353245, 32029772}, { 47484683, 32145510}, { 47534251, 32233847}, { 47538509, 32241438}, { 47602626, 32453825}, { 47622648, 32465115}, { 47701707, 32575250},
{ 47776955, 33122018}, { 47677092, 33345574}, { 47630772, 33380015}, { 47572757, 33423150}, { 47328653, 33537512}, { 47343826, 33612940}, { 47462219, 33617810}, { 47578431, 33622591}, { 47808035, 33604884}, { 47842258, 33885890},
{ 47847000, 34154765}, { 47852298, 34455418}, { 47806556, 34798342}, { 47804979, 34803470}, { 47795265, 34835122}, { 47811501, 34879922}, { 47843100, 35247684}, { 47839663, 35481904}, { 47833503, 35902474}, { 47803910, 36044010},
{ 47819598, 36077879}, { 47841934, 36100587}, { 47854870, 36165755}, { 47911856, 36452861}, { 47927332, 36616382}, { 47936929, 36717785}, { 47770423, 36987292}, { 47699764, 37101659}, { 47671115, 37157488}, { 47423375, 37424772},
{ 47616349, 37518717}, { 47680621, 37550006}, { 47836151, 37632587}, { 47811936, 37777743}, { 47716954, 38113916}, { 47654340, 38250491}, { 47533407, 38514290}, { 47431515, 38674036}, { 47367427, 38987733}, { 47348164, 39043625},
{ 47298533, 39187606}, { 47279676, 39231940}, { 47252411, 39296047}, { 47246894, 39304927}, { 47238746, 39318037}, { 47232029, 39335258}, { 47220194, 39365593}, { 47196053, 39429922}, { 47159408, 39527585}, { 47041654, 39691835},
{ 47002148, 39908798}, { 46964248, 39997937}, { 46895728, 40159083}, { 46826610, 40301043}, { 46763479, 40430710}, { 46514929, 40884923}, { 46474179, 40918994}, { 46440818, 40946888}, { 46433233, 40992821}, { 46426528, 41033401},
{ 46108271, 41626808}, { 46056215, 41723876}, { 45997871, 41855066}, { 45755987, 42227269}, { 45653183, 42385466}, { 45444848, 42652871}, { 45380966, 42654262}, { 45336326, 42655238}, { 45326382, 42763461}, { 45318953, 42844333},
{ 45175146, 43086382}, { 45086585, 43235443}, { 45055897, 43281060}, { 44968051, 43418247}, { 44470500, 44195272}, { 44413430, 44364401}, { 44390221, 44433179}, { 44309502, 44528273}, { 44199667, 44604532}, { 43887229, 44833256},
{ 43815081, 44886070}, { 43726552, 44932547}, { 43689058, 44928887}, { 43686137, 44927822}, { 43280111, 44871367}, { 43249704, 44937548}, { 43324977, 45004000}, { 43046101, 45224515}, { 42898716, 45341059}, { 42838343, 45382240},
{ 42721108, 45493632}, { 42470119, 45669357}, { 42359756, 45746630}, { 42073412, 45910212}, { 42022050, 45926905}, { 41907133, 46027394}, { 41144940, 46559849}, { 40902566, 46683907}, { 40884989, 46688481}, { 40811763, 46707548},
{ 40768612, 46786655}, { 40675645, 46871372}, { 40548269, 46985681}, { 40382460, 47085920}, { 40082094, 47267510}, { 39768380, 47413990}, { 39734614, 47420931}, { 39586801, 47437916}, { 39408498, 47458403}, { 39355630, 47574767},
{ 39281498, 47737937}, { 39251009, 47783502}, { 39152882, 47890727}, { 39013408, 48043132}, { 38921577, 48100514}, { 38896008, 48108330}, { 38727116, 48102492}, { 38692428, 48101294}, { 38425261, 48075982}, { 38342344, 48047392},
{ 38336010, 48154957}, { 38151978, 48395628}, { 37811687, 48488990}, { 37804084, 48490379}, { 37674998, 48513979}, { 37674196, 48513196}, { 37658712, 48498074}, { 37592273, 48482371}, { 37336907, 48659173}, { 37140701, 48741338},
{ 37129466, 48764064}, { 37075599, 48873013}, { 36739574, 48838715}, { 36721697, 48864552}, { 36456161, 49171298}, { 36442740, 49184060}, { 36436660, 49212679}, { 36300951, 49585030}, { 36223897, 49727927}, { 36150156, 49864671},
{ 35924446, 50245885}, { 35769083, 50508275}, { 35750118, 50514284}, { 35323137, 50653609}, { 34050908, 50703703}, { 33864494, 50706292}, { 33666152, 50709051}, { 33813201, 50839130}, { 33884905, 50893350}, { 33912037, 50913867},
{ 34282238, 51132740}, { 35016181, 51605972}, { 35027459, 51615787}, { 35030754, 51618656}, { 35108803, 51693454}, { 35137469, 51720927}, { 34948522, 51872654}, { 34658613, 52064227}, { 34464997, 52192175}, { 34289189, 52285353},
{ 34219119, 52312637}, { 33847969, 52428212}, { 33681538, 52480036}, { 33407178, 52510887}, { 33421683, 52685666}, { 33428342, 52765908}, { 33392094, 53146294}, { 33371466, 53362761}, { 33253040, 54291767}, { 33196142, 54612534},
{ 33128154, 54815569}, { 33095559, 54912904}, { 32570427, 55111061}, { 32525706, 55125923}, { 32458612, 55148214}, { 32385063, 55163161}, { 32282016, 55184108}, { 32241393, 55188603}, { 32190544, 55194226}, { 32027959, 55217259},
{ 32011561, 56072729}, { 32003567, 57064095}, { 31997637, 57799631}, { 32015577, 60287161}, { 32014290, 61201940}, { 32012996, 62120667}, { 32007630, 62197246}, { 32002828, 62265761}, { 32003310, 62373952}, { 32003630, 62444825},
{ 31951202, 63100419}, { 31935103, 63301732}, { 31937490, 63354807}, { 31968533, 64124669}, { 32071989, 64767136}, { 32091323, 64947492}, { 32101518, 65042609}, { 32140486, 65216353}, { 32159835, 65302616}, { 32422071, 66001036},
{ 32441049, 66056128}, { 32463003, 66119864}, { 32483582, 66164217}, { 32504016, 66208251}, { 32702117, 66557895}, { 32734168, 66611648}, { 32759723, 66654509}, { 32985249, 66546464}, { 33208649, 66439436}, { 33424955, 66330151},
{ 33554797, 66263457}, { 33891385, 66090564}, { 34622897, 65616004}, { 34819546, 65471063}, { 34988926, 65346218}, { 35739513, 64794843}, { 36421629, 64150515}, { 36944662, 63656452}, { 36959929, 63643292}, { 36964174, 63639854},
{ 36973615, 63630686}, { 37023366, 63597643}, { 37652255, 63172287}, { 37804320, 63100590}, { 37939211, 63174238}, { 37949998, 63178562}, { 38147664, 63257792}, { 38147652, 63269386}, { 38147521, 63403665}, { 38150429, 63418056},
{ 38177182, 63550576}, { 38159827, 64298859}, { 38153585, 64520174}, { 38146482, 64771937}, { 38142126, 64820836}, { 38138239, 64839298}, { 38115242, 65010431}, { 38113231, 65025393}, { 37912271, 66372984}, { 37841830, 66687479},
{ 37674277, 67228175}, { 37551047, 67593509}, { 37497098, 67727333}, { 37392268, 67951311}, { 36986640, 68817980}, { 36604483, 69575518}, { 36479686, 69769345}, { 36265058, 70102690}, { 36332308, 70163400}, { 36398395, 70223058},
{ 36718105, 70645723}, { 36714573, 70708131}, { 36707947, 70825274}, { 36665865, 71083146}, { 36295751, 71910509}, { 36243731, 72020144}, { 36010145, 72512434}, { 35364761, 74115820}, { 35327445, 74206370}, { 35287332, 74303707},
{ 35262905, 74370595}, { 35235816, 74444782}, { 35006275, 75142899}, { 34758612, 75896141}, { 34609479, 76324076}, { 34534936, 76598593}, { 34419529, 77019735}, { 34125782, 78091675}, { 34270135, 78023153}, { 34366481, 77977415},
{ 34669421, 77827427}, { 35532698, 77282412}, { 35875762, 77065829}, { 35924952, 77041288}, { 35981906, 77004141}, { 36227708, 76899428}, { 36700108, 76693284}, { 36835857, 76657801}, { 36942059, 76731654}, { 36959107, 76741135},
{ 37155031, 76850094}, { 37152161, 76868751}, { 37133420, 76990662}, { 37135224, 77014721}, { 37144331, 77136260}, { 37029215, 77783623}, { 36994547, 77984972}, { 36957442, 78200506}, { 36949745, 78231593}, { 36945059, 78243379},
{ 36909925, 78358183}, { 36908693, 78362210}, { 36517584, 79569608}, { 36400200, 79852238}, { 36160758, 80317591}, { 36001388, 80606724}, { 35929263, 80720331}, { 35803937, 80894237}, { 35313741, 81574455}, { 34810829, 82211118},
{ 34636165, 82398130}, { 34424143, 82625140}, { 34177218, 82875584}, { 34001320, 83053991}, { 33330876, 83686990}, { 33313615, 83940131}, { 33257889, 84757318}, { 33154596, 86125618}, { 33050414, 87930914}, { 33037323, 88157771},
{ 32996151, 88791902}, { 33122354, 88720953}, { 34042644, 88195810}, { 34854618, 87571171}, { 35217422, 87292077}, { 35240201, 87279017}, { 35256654, 87268145}, { 35304044, 87230648}, { 35465557, 87154377}, { 35979874, 86886707},
{ 36162994, 86833255}, { 36213131, 86859811}, { 36328089, 86920714}, { 36446386, 87103899}, { 36444792, 87129675}, { 36435583, 87278561}, { 36439166, 87306042}, { 36455346, 87430153}, { 36439626, 87577638}, { 36363937, 88287781},
{ 36334385, 88516418}, { 36324472, 88550288}, { 36266923, 88831775}, { 36258817, 88871412}, { 36009099, 90001153}, { 35925390, 90278389}, { 35742522, 90743063}, { 35584494, 91114154}, { 35511233, 91260521}, { 35378328, 91493626},
{ 34896978, 92337857}, { 34592698, 92829175}, { 34534101, 92906355}, { 34379904, 93109443}, { 34292029, 93224277}, { 34181322, 93368951}, { 33996695, 93594059}, { 33791238, 93844563}, { 33350304, 94350448}, { 32679061, 95059135},
{ 32663276, 95383974}, { 32630835, 96051559}, { 32623715, 96162432}, { 32625261, 96184173}, { 32631760, 96253789}, { 32637940, 96319986}, { 32671334, 96831435}, { 32555999, 97073603}, { 32552956, 97110111}, { 32549772, 97148299},
{ 32339278, 100576678}, { 32333722, 100685777}, { 32330348, 100752035}, { 32315766, 101012907}, { 32604225, 100816839}, { 32833219, 100661190}, { 33690734, 100037568}, { 33947721, 99810841}, { 34263306, 99532414}, { 34709871, 99161136},
{ 35458100, 98470566}, { 35535202, 98409290}, { 35673889, 98299068}, { 35825183, 98230993}, { 36031300, 98138241}, { 36364183, 98058757}, { 36389853, 98099020}, { 36443213, 98182736}, { 36495776, 98421334}, { 36464592, 98534766},
{ 36403262, 98757832}, { 36433188, 98786253}, { 36468201, 98819516}, { 36427877, 99135414}, { 36380139, 99509425}, { 36367327, 99566653}, { 36130997, 100458902}, { 36092849, 100736616}, { 35993189, 101207413}, { 35961980, 101354843},
{ 35901824, 101565944}, { 35599001, 102436249}, { 35598486, 102437494}, { 35525627, 102612717}, { 35498238, 102672427}, { 35179093, 103368204}, { 34902420, 103873765}, { 34074371, 105280790}, { 33796945, 105666257}, { 33430747, 106175067},
{ 32757675, 107021332}, { 32288404, 107611357}, { 32147333, 107782229}, { 32045181, 107903768}, { 32013865, 108446053}, { 32004365, 108597331}, { 31933356, 109727991}, { 31929556, 109801743}, { 31921205, 109963885}, { 31919950, 109998202},
{ 31917378, 110068478}, { 31935487, 110174763}, { 31962352, 110332410}, { 31868759, 110776536}, { 31779274, 112901692}, { 31772558, 113008639}, { 31763520, 113152580}, { 31760914, 113226796}, { 31757613, 113320828}, { 31878079, 113245898},
{ 32056600, 113134847}, { 32205325, 113028281}, { 32417383, 112876331}, { 32791706, 112611586}, { 33374891, 112199137}, { 34043729, 111739447}, { 34299836, 111533282}, { 34686259, 111194925}, { 35041202, 110899316}, { 36153161, 109973245},
{ 36489565, 109732139}, { 36935134, 109547251}, { 36998142, 109523782}, { 37285208, 109416845}, { 37303575, 109443686}, { 37380657, 109556352}, { 37429339, 109768662}, { 37389406, 109896075}, { 37312708, 110140778}, { 37330397, 110173101},
{ 37358669, 110224762}, { 37347970, 110508588}, { 37343682, 110622428}, { 37233824, 111039422}, { 36974286, 111866215}, { 36941457, 112104350}, { 36810462, 112600390}, { 36763361, 112778757}, { 36685333, 113003686}, { 36304140, 113929965},
{ 36303227, 113931942}, { 36219925, 114112998}, { 36185254, 114177524}, { 35766113, 114957538}, { 35699185, 115058398}, { 35271549, 115739102}, { 34529522, 116832154}, { 34230604, 117226448}, { 34152175, 117323267}, { 33753453, 117815498},
{ 33688745, 117896887}, { 33515149, 118115220}, { 33167360, 118505862}, { 32252839, 119533076}, { 31951224, 119865885}, { 31856676, 119967574}, { 31811772, 120013039}, { 31820788, 120150853}, { 31837447, 120637820}, { 31884548, 122014628},
{ 31884879, 122025348}, { 31884889, 122025915}, { 31884859, 122030715}, { 31853727, 124752378}, { 31852710, 125798379}, { 32040109, 125687330}, { 32336721, 125511560}, { 33050838, 125039566}, { 33741293, 124531865}, { 34004877, 124347492},
{ 34706008, 123857040}, { 34900746, 123695124}, { 35248769, 123405773}, { 35958009, 122839178}, { 36752647, 122139217}, { 36794698, 122103853}, { 36926183, 121993279}, { 37041929, 121900209}, { 37364281, 121641009}, { 37506782, 121535931},
{ 37599623, 121475276}, { 37805210, 121390600}, { 38274450, 121197339}, { 38429386, 121137935}, { 38611951, 121409191}, { 38647554, 121490884}, { 38558179, 121763354}, { 38544345, 121816126}, { 38504735, 121967178}, { 38540287, 122025777},
{ 38533522, 122225637}, { 38527834, 122393821}, { 38490015, 122574939}, { 38335371, 123023448}, { 38226910, 123422167}, { 38128017, 123785706}, { 38110062, 123913558}, { 38039445, 124196782}, { 37811751, 125109983}, { 37795287, 125159401},
{ 37789856, 125175267}, { 37747302, 125281671}, { 37678378, 125454008}, { 37326009, 126304036}, { 37280379, 126403545}, { 36723741, 127438116}, { 36607591, 127622339}, { 35307172, 129556108}, { 34960577, 130042788}, { 34625146, 130457962},
{ 34244496, 130929114}, { 33616736, 131638592}, { 33126427, 132192717}, { 32289044, 133098400}, { 32128210, 133254928}, { 32114672, 133265860}, { 32051379, 133303244}, { 31973610, 133349175}, { 32021296, 134019482}, { 32078232, 134927829},
{ 32192915, 136757391}, { 32250210, 137342897}, { 32301584, 137908848}, { 32406571, 139065434}, { 32456488, 139422175}, { 32511513, 139855909}, { 32587723, 140456611}, { 33065481, 140191593}, { 33323332, 140063408}, { 33766028, 139843340},
{ 33978698, 139717429}, { 34224999, 139571601}, { 35090288, 139002456}, { 36098536, 138270161}, { 36204726, 138196467}, { 36870073, 137734734}, { 36937868, 137678015}, { 37269439, 137400662}, { 38224552, 136636721}, { 39248462, 135736109},
{ 39262231, 135724978}, { 39431206, 135588270}, { 39558286, 135491389}, { 40066663, 135103831}, { 40597978, 134876486}, { 40913397, 134752602}, { 41009750, 134730971}, { 41033440, 134769160}, { 41137853, 134937472}, { 41236776, 135135656},
{ 41185372, 135392011}, { 41170368, 135466840}, { 41117848, 135629223}, { 41128977, 135726643}, { 41112112, 135925316}, { 41028443, 136275112}, { 40892177, 136737346}, { 40715316, 137337282}, { 40625973, 137862286}, { 40571054, 138077826},
{ 40413004, 138698127}, { 40307787, 139028628}, { 40280705, 139108396}, { 40108570, 139542037}, { 39781168, 140366808}, { 39776747, 140377453}, { 39771298, 140388940}, { 39694209, 140532631}, { 39126953, 141589960}, { 39112976, 141613526},
{ 38864787, 141998169}, { 38780359, 142124163}, { 37534211, 143983846}, { 36837998, 144898691}, { 36749607, 145008489}, { 36437049, 145396720}, { 36308895, 145540735}, { 35926199, 145970826}, { 35104551, 146848709}, { 34756762, 147234955},
{ 34428436, 147599589}, { 34120556, 147908106}, { 34059694, 147944671}, { 33992021, 147971830}, { 33888925, 148013197}, { 33994002, 148234139}, { 34102871, 148463060}, { 34260406, 148815390}, { 34505558, 149252538}, { 34649150, 149539737},
{ 34875213, 149991894}, { 34913367, 150060689}, { 34939834, 150108425}, { 35009188, 150222655}, { 35057146, 150301638}, { 35531716, 151039155}, { 35961908, 151607166}, { 36198106, 151919026}, { 37112008, 151466356}, { 37122527, 151461129},
{ 37143274, 151448455}, { 37793852, 151104327}, { 38753278, 150462096}, { 39057095, 150265965}, { 39387132, 150052914}, { 39992757, 149578233}, { 40209373, 149410006}, { 40448656, 149224173}, { 41648972, 148150708}, { 41827582, 147994189},
{ 42089284, 147764870}, { 42281920, 147557241}, { 42535672, 147283737}, { 43211137, 146606344}, { 43969650, 145734949}, { 44008274, 145690567}, { 44434382, 145256367}, { 44673165, 145036231}, { 44753304, 144976343}, { 44941707, 144886575},
{ 45449136, 144644796}, { 45533221, 144617860}, { 45594657, 144672684}, { 45686988, 144755077}, { 45821054, 144894151}, { 45845698, 144928928}, { 45802394, 145256827}, { 45801968, 145263145}, { 45793099, 145396327}, { 45826083, 145436911},
{ 45827387, 145448733}, { 45852550, 145676686}, { 45846396, 146183080}, { 45801072, 146729105}, { 45751200, 147329993}, { 45765306, 147565974}, { 45765766, 147690105}, { 45758629, 147823920}, { 45717918, 148587045}, { 45669293, 148998256},
{ 45657164, 149090109}, { 45565455, 149517107}, { 45390903, 150329829}, { 45380310, 150370709}, { 45303883, 150599765}, { 45049477, 151362234}, { 45041081, 151384892}, { 44988127, 151512567}, { 44899898, 151709940}, { 44188361, 153301702},
{ 43960091, 153807492}, { 43687530, 154326968}, { 43680264, 154339888}, { 43428400, 154787836}, { 43418419, 154804941}, { 43222756, 155140257}, { 43211901, 155157187}, { 43019606, 155457042}, { 42439201, 156284412}, { 42742998, 156320854},
{ 42946786, 156345296}, { 43218356, 156408139}, { 43490220, 156548626}, { 43600789, 156605776}, { 43616758, 156616967}, { 43638494, 156675797}, { 43689725, 156874320}, { 43697411, 156939181}, { 43667792, 157194800}, { 43663112, 157219786},
{ 43589483, 157612846}, { 43578259, 157650201}, { 43503908, 157897703}, { 43271842, 158586008}, { 43026656, 159112379}, { 42680049, 159768278}, { 42229097, 160621619}, { 41614538, 161818913}, { 41602009, 161838594}, { 41549009, 161921905},
{ 41366702, 162195210}, { 41089703, 162610457}, { 41051349, 162661598}, { 40028827, 163938879}, { 39981539, 163995316}, { 39859709, 164140726}, { 39557928, 164489623}, { 38840108, 165319487}, { 38817977, 165343409}, { 36508721, 167822791},
{ 35803734, 168527171}, { 35265129, 169065323}, { 35217638, 169111343}, { 35182142, 169143335}, { 34143283, 170051242}, { 34091092, 170092305}, { 33992346, 170169987}, { 32820222, 171015261}, { 32596277, 171172367}, { 32366414, 171333625},
{ 30949741, 172256683}, { 30776429, 172369214}, { 30685231, 172428426}, { 29784929, 172978028}, { 29711510, 173022900}, { 29649347, 173060901}, { 29626880, 173084470}, { 29607989, 173104288}, { 29476620, 173372906}, { 29166644, 173374167},
{ 29105869, 173396269}, { 29066168, 173410694}, { 28480959, 173773359}, { 28318456, 173874074}, { 28236958, 173920336}, { 28053468, 174015451}, { 27663961, 174212865}, { 26444009, 174781179}, { 25128636, 175292014}, { 24833691, 175404475},
{ 24567873, 175499255}, { 23673660, 175815148}, { 23263816, 175959931}, { 22989484, 175996217}, { 22919277, 176005507}, { 22821755, 176011321}, { 22593369, 175931875}, { 22197778, 175796707}, { 20895444, 175329856}, { 20562493, 175210506},
{ 20357518, 175131409}, { 19431901, 174778687}, { 19227774, 174700914}, { 17432818, 173805114}, { 17355249, 173765680}, { 17340552, 173757060}, { 17293649, 173727963}, { 15176003, 172414266}, { 14987901, 172296594}, { 14897452, 172240019},
{ 14730104, 172123866}, { 14649567, 172067971}, { 12604451, 170685425}, { 12582065, 170669040}, { 12501564, 170610143}, { 12483411, 170595498}, { 12418519, 170543227}, { 11146256, 169546467}, { 11131285, 169533173}, { 10973608, 169393198},
{ 10963368, 169383375}, { 10855356, 169279681}, { 9350332, 167783891}, { 9237755, 167663880}, { 9038028, 167450975}, { 7554140, 165846157}, { 6510331, 164717307}, { 6450301, 164645790}, { 4792198, 162599032}, { 4711896, 162499401},
{ 4702892, 162486484}, { 4689884, 162466018}, { 4689721, 162465748}, { 4111625, 161512044}, { 3262811, 159825639}, { 3085907, 159501392}, { 2964224, 159278374}, { 2880198, 159123098}, { 2827825, 159026309}, { 2730830, 158798250},
{ 2662597, 158637824}, { 2461794, 158144454}, { 2258655, 157377436}, { 2232776, 156966420}, { 2227381, 156880727}, { 2229842, 156800001}, { 2404803, 156571898}, { 2502593, 156512353}, { 2571069, 156470646}, { 3012355, 156329121},
{ 3172690, 156317433}, { 3263007, 156310852}, { 3448807, 156270050}, { 2933268, 155537448}, { 2932334, 155536119}, { 2690506, 155171633}, { 2473838, 154800417}, { 2214521, 154335871}, { 1956160, 153843466}, { 1643404, 153150964},
{ 936422, 151585583}, { 886715, 151471650}, { 881872, 151459055}, { 835673, 151315362}, { 420381, 150023686}, { 415543, 150006511}, { 411493, 149986474}, { 371105, 149740432}, { 184472, 148603483}, { 176976, 148544106}, { 143829, 148268525},
{ 141423, 148213179}, { 118798, 147692447}, { 141994, 147109270}, { 96664, 146619882}, { 46940, 146083025}, { 34028, 145778412}, { 32148, 145734124}, { 50580, 145571914}, { 79797, 145477573}, { 59893, 144996644}, { 53607, 144916874},
{ 75632, 144881102}, { 170230, 144783356}, { 367047, 144609349}, { 495089, 144649841}, { 696206, 144748339}, { 861062, 144829070}, { 1202743, 145013350}, { 1665932, 145467720}, { 1738044, 145542186}, { 1871110, 145679584}, { 2233705, 146073631},
{ 2875888, 146771504}, { 2976802, 146887761}, { 3008358, 146918708}, { 3105019, 147016992}, { 3562844, 147482514}, { 3900940, 147829488}, { 3926192, 147851556}, { 5456634, 149216502}, { 5473415, 149229592}, { 5678115, 149389248},
{ 6416516, 149979537}, { 6693887, 150160404}, { 7011978, 150367823}, { 8034093, 151060650}, { 8245822, 151174920}, { 8663509, 151400371}, { 8734568, 151444233}, { 9700825, 151913516}, { 10314440, 151101573}, { 10876143, 150241580},
{ 10937084, 150142918}, { 10989872, 150057455}, { 11012110, 150016058}, { 11029139, 149984364}, { 11202330, 149640866}, { 11331097, 149385460}, { 11601540, 148893595}, { 11801542, 148453984}, { 11867898, 148312015}, { 12006182, 148016156},
{ 11936334, 147987685}, { 11846756, 147951181}, { 11775937, 147908929}, { 11448318, 147578728}, { 10005162, 146006655}, { 9941330, 145934468}, { 9420742, 145345782}, { 9364739, 145276533}, { 9005053, 144831776}, { 8354706, 143947082},
{ 7741954, 143034251}, { 7046776, 141998616}, { 6866979, 141726486}, { 6759755, 141551328}, { 6581042, 141228382}, { 6214827, 140566592}, { 6160332, 140464737}, { 6154984, 140452943}, { 6118667, 140365627}, { 5608006, 139066930},
{ 5576877, 138974353}, { 5449821, 138579522}, { 5423448, 138473840}, { 5269717, 137857830}, { 5183256, 137323221}, { 5051763, 136885377}, { 4900390, 136381329}, { 4788716, 135926420}, { 4778542, 135832434}, { 4751278, 135580592},
{ 4759133, 135551850}, { 4772567, 135502722}, { 4750760, 135428400}, { 4689711, 135220325}, { 4720284, 134950229}, { 4772938, 134876983}, { 4872059, 134739100}, { 4907041, 134734799}, { 4988166, 134747911}, { 5187996, 134827143},
{ 5324282, 134881173}, { 5823633, 135095262}, { 6457261, 135576778}, { 6468046, 135585394}, { 6645027, 135726930}, { 7665807, 136625984}, { 8014871, 136908364}, { 8760642, 137511681}, { 9070115, 137764153}, { 9505207, 138067027},
{ 9692018, 138199840}, { 10866067, 139034528}, { 10974854, 139102654}, { 11199174, 139243162}, { 11980766, 139757269}, { 12820102, 140204762}, { 13013724, 140301821}, { 13307713, 140449197}, { 13339465, 140204984}, { 13387908, 139832384},
{ 13476326, 139229254}, { 13545245, 138464294}, { 13637934, 137435521}, { 13704650, 136750183}, { 13756310, 135946981}, { 13854009, 134427968}, { 13931781, 133352665}, { 13880515, 133326641}, { 13806176, 133288914}, { 13608773, 133095964},
{ 13229938, 132676064}, { 12917088, 132329299}, { 12475540, 131854762}, { 12234438, 131582242}, { 11645945, 130917061}, { 11435343, 130656410}, { 11256705, 130435328}, { 11087956, 130227341}, { 10943531, 130049329}, { 10660547, 129658000},
{ 9884836, 128504691}, { 9363495, 127729593}, { 9183437, 127445707}, { 8613352, 126392173}, { 8569664, 126295529}, { 8233135, 125484892}, { 8100143, 125150567}, { 8091324, 125125230}, { 8068370, 125055541}, { 8047369, 124966573},
{ 7827878, 124036734}, { 7815999, 123941440}, { 7743138, 123689990}, { 7467916, 122740178}, { 7381012, 122383130}, { 7365871, 122250909}, { 7330956, 121946008}, { 7347071, 121910652}, { 7366239, 121868607}, { 7337555, 121775565},
{ 7275180, 121573218}, { 7357784, 121255913}, { 7363162, 121248563}, { 7433561, 121152362}, { 7492882, 121172887}, { 8152120, 121400901}, { 8296078, 121458859}, { 8337642, 121483827}, { 8428744, 121552386}, { 8461373, 121578560},
{ 9113408, 122101612}, { 9968838, 122858025}, { 10418874, 123221408}, { 11203964, 123855334}, { 11236475, 123882487}, { 11264272, 123901717}, { 12869603, 125041315}, { 13619004, 125547677}, { 13833945, 125671552}, { 14049136, 125795572},
{ 14042979, 124631730}, { 14031124, 123791039}, { 14029618, 123425913}, { 14024871, 122275773}, { 14024680, 122240909}, { 14024300, 122172017}, { 14024368, 122132419}, { 14024494, 122058437}, { 14025750, 122003675}, { 14028093, 121901540},
{ 14053706, 121051621}, { 14084937, 120015176}, { 13976495, 119893307}, { 12808105, 118596333}, { 12632795, 118395530}, { 12332420, 118051483}, { 12010936, 117651678}, { 11662489, 117218341}, { 11286185, 116695820}, { 10542401, 115590915},
{ 10484664, 115505145}, { 10085127, 114875400}, { 9677465, 114107097}, { 9676038, 114103997}, { 9587011, 113910478}, { 9572058, 113874387}, { 9221672, 113028545}, { 9132465, 112762183}, { 8929936, 112011523}, { 8896027, 111773355},
{ 8763540, 111338847}, { 8591711, 110775312}, { 8585822, 110750616}, { 8583286, 110726469}, { 8532504, 110242770}, { 8561517, 110201837}, { 8589689, 110162093}, { 8539283, 109999835}, { 8459773, 109743891}, { 8476274, 109635698},
{ 8539247, 109532026}, { 8559299, 109499015}, { 8639538, 109407427}, { 8837219, 109481673}, { 9374636, 109713713}, { 9614985, 109884378}, { 9895885, 110108176}, { 10150796, 110311272}, { 10647433, 110745796}, { 11163900, 111149653},
{ 11435641, 111378216}, { 11952173, 111812662}, { 12063358, 111892355}, { 12195941, 111987389}, { 13754894, 113077948}, { 13965930, 113207021}, { 14143358, 113315534}, { 14095680, 112195851}, { 14075275, 111736247}, { 14031684, 110754424},
{ 13949266, 109698295}, { 13931155, 109374956}, { 13907232, 108947887}, { 13903305, 108820557}, { 13899752, 108705317}, { 13898286, 108692370}, { 13896892, 108680054}, { 13882077, 108455610}, { 13866991, 108227067}, { 13852378, 107897586},
{ 13627196, 107630194}, { 13249326, 107173733}, { 13128837, 107021896}, { 12504668, 106235327}, { 12449045, 106156712}, { 12301165, 105947708}, { 12240927, 105864439}, { 12071292, 105629917}, { 11741182, 105140360}, { 11102902, 104050785},
{ 11009874, 103891983}, { 10724262, 103375048}, { 10370561, 102607103}, { 10302463, 102446702}, { 9995869, 101563023}, { 9933827, 101340326}, { 9788639, 100674614}, { 9761576, 100425516}, { 9620310, 99895785}, { 9572074, 99714909},
{ 9473316, 99261511}, { 9457110, 98860065}, { 9475422, 98813097}, { 9491516, 98771818}, { 9454445, 98628574}, { 9395112, 98399301}, { 9430018, 98201406}, { 9448015, 98172416}, { 9519385, 98057456}, { 9858391, 98155219}, { 10045563, 98209192},
{ 10217386, 98274096}, { 10328458, 98365757}, { 11168922, 99136589}, { 11517095, 99428522}, { 11782963, 99664460}, { 12152171, 99992110}, { 12543518, 100270019}, { 12914813, 100533689}, { 13199749, 100744460}, { 13324020, 100835567},
{ 13585579, 101027330}, { 13575682, 100826649}, { 13569447, 100700201}, { 13562345, 100567361}, { 13559065, 100506021}, { 13429751, 98521010}, { 13371150, 97621467}, { 13343156, 97180710}, { 13333987, 97039073}, { 13207473, 95084673},
{ 13138184, 95005008}, { 13017680, 94866468}, { 12083312, 93848129}, { 12022705, 93771797}, { 11862461, 93569995}, { 11784430, 93470508}, { 11589381, 93221813}, { 11309567, 92840780}, { 10844778, 92098029}, { 10775191, 91976786},
{ 10496881, 91491862}, { 10185086, 90849349}, { 10144137, 90764963}, { 10074833, 90600171}, { 9828579, 89857830}, { 9703614, 89075796}, { 9674971, 88969502}, { 9495272, 88102892}, { 9475468, 87916753}, { 9440640, 87589408}, { 9465676, 87528619},
{ 9487914, 87474617}, { 9465041, 87357340}, { 9428525, 87170118}, { 9490390, 86904119}, { 9512256, 86883153}, { 9574632, 86823334}, { 9727402, 86841642}, { 10166330, 86894255}, { 10190151, 86899193}, { 10198409, 86903284}, { 10249971, 86942095},
{ 10299758, 86980568}, { 11788945, 88131376}, { 11901024, 88196968}, { 12050012, 88284161}, { 12770268, 88697024}, { 12893258, 88767518}, { 12865978, 88340499}, { 12755514, 86383203}, { 12590001, 84209400}, { 12584956, 84143148},
{ 12549052, 83666692}, { 11929877, 83107725}, { 11390770, 82556851}, { 11083660, 82243035}, { 10537284, 81546957}, { 10424674, 81403496}, { 10079867, 80926984}, { 9689286, 80270083}, { 9687616, 80267071}, { 9615613, 80136762},
{ 9601056, 80104215}, { 9309849, 79453353}, { 9259598, 79312241}, { 9118888, 78788995}, { 9088297, 78585733}, { 8994447, 78301695}, { 8881493, 77959840}, { 8828452, 77771748}, { 8812025, 77688113}, { 8733303, 77287329}, { 8744431, 77274990},
{ 8786674, 77228142}, { 8770800, 77179143}, { 8746495, 77100409}, { 8709758, 76981397}, { 8765710, 76725562}, { 8780763, 76703647}, { 8821796, 76643902}, { 9050198, 76667553}, { 9430163, 76706910}, { 9505621, 76721216}, { 9535183, 76740071},
{ 9561982, 76753134}, { 11021709, 77681521}, { 11271938, 77809531}, { 11740477, 78049225}, { 11713323, 77940287}, { 11500961, 77088300}, { 11491445, 77055518}, { 11468672, 76977103}, { 11007894, 75454998}, { 10625858, 74342820},
{ 10581531, 74223613}, { 10547931, 74133250}, { 9872558, 72487409}, { 9785055, 72279833}, { 9735127, 72161397}, { 9661531, 72007947}, { 9614591, 71910070}, { 9234200, 71112437}, { 9147114, 70727104}, { 9142261, 70702843}, { 9138267, 70682891},
{ 9286224, 70443224}, { 9461343, 70247284}, { 9556416, 70162672}, { 9625168, 70101485}, { 9435737, 69811425}, { 9287394, 69584273}, { 8757085, 68530695}, { 8673850, 68365333}, { 8445942, 67877601}, { 8187617, 67187177}, { 8139627, 67039434},
{ 7937861, 66234567}, { 7892565, 65909655}, { 7845288, 65439718}, { 7844011, 65310767}, { 7823103, 65136343}, { 7778117, 64761067}, { 7716333, 64313964}, { 7705694, 64124356}, { 7687717, 63803945}, { 7701643, 63790152}, { 7718011, 63773943},
{ 7758186, 63752036}, { 7729172, 63586572}, { 7697769, 63407488}, { 7779619, 63146399}, { 7790119, 63132497}, { 7857734, 63042993}, { 7899799, 63053366}, { 8115923, 63106666}, { 8464711, 63240292}, { 8677072, 63398904}, { 8767176, 63477143},
{ 8977927, 63660143}, { 9421383, 64100703}, { 9785048, 64413088}, { 9975436, 64589567}, { 10286420, 64877827}, { 11014721, 65410888}, { 11115862, 65482249}, { 11327524, 65631599}, { 11395991, 65675856}, { 11535890, 65766274}, { 12026448, 66109919},
{ 12502690, 66343355}, { 12786634, 66472769}, { 13164960, 66645193}, { 13207596, 66564001}, { 13256756, 66470394}, { 13640736, 65570500}, { 13683003, 65454507}, { 13718537, 65356988}, { 13735231, 65270567}, { 13747424, 65207437},
{ 13863686, 64629409}, { 13875328, 64496043}, { 13887975, 64351165}, { 13957488, 63607260}, { 13950883, 63386188}, { 13943973, 63154947}, { 13895952, 62476120}, { 13876483, 62262044}, { 13859838, 62079009}, { 13859584, 62074662},
{ 13859582, 62065658}, { 13859483, 61971042}, { 13862761, 55222623}, { 13815791, 55212684}, { 13617475, 55174296}, { 13379849, 55128299}, { 13200660, 55067043}, { 13117648, 55038667}, { 12798922, 54907256}, { 12743350, 54730557},
{ 12719703, 54655364}, { 12656225, 54324243}, { 12632418, 53676660}, { 12625539, 53489551}, { 12652785, 53052852}, { 12782795, 52820186}, { 12846930, 52705411}, { 13041220, 52491209}, { 13143647, 52409064}, { 13187810, 52373646},
{ 13354789, 52319639}, { 13381838, 52313108}, { 13407786, 52306845}, { 13609096, 52308186}, { 13798532, 52309451}, { 14794521, 52294618}, { 15549961, 52336594}, { 16050147, 52311338}, { 16209513, 52303295}, { 16312325, 52297439},
{ 16369590, 51869307}, { 16340473, 51576398}, { 16331091, 51482008}, { 16316170, 51377054}, { 16241360, 51186578}, { 16186688, 51047373}, { 16076915, 50725256}, { 16093629, 50461603}, { 16098435, 50385771}, { 16109774, 50333994},
{ 16208639, 50141731}, { 16271132, 50020206}, { 16284775, 49997056}, { 16295310, 49985147}, { 16360397, 49947770}, { 16432796, 49916484}, { 16999910, 49671395}, { 17079341, 49631019}, { 17221011, 49559013}, { 17356128, 49546264},
{ 17369996, 49528116}, { 17426993, 49502498}, { 17530282, 49456075}, { 17342293, 49148088}, { 17284381, 49008875}, { 17254026, 48935905}, { 17357436, 48625105}, { 17422365, 48429965}, { 17423796, 48426977}, { 17601162, 48056939},
{ 17599241, 47980228}, { 17595410, 47827198}, { 17579402, 47751708}, { 17538195, 47557388}, { 17400788, 46598168}, { 17023471, 46464319}, { 16973301, 46446494}, { 16812540, 46389386}, { 16673736, 46329440}, { 16319654, 46176525},
{ 15950663, 46003440}, { 15838028, 45939836}, { 15697899, 45836427}, { 15289766, 45502367}, { 15260072, 45476441}, { 14999104, 45248614}, { 14962927, 45210840}, { 14722491, 44959778}, { 14678301, 44921783}, { 14404868, 44686698},
{ 14020130, 44298671}, { 13905758, 44155324}, { 13566066, 43648328}, { 13163266, 43047144}, { 13102631, 42937239}, { 13070977, 42862998}, { 12945977, 42560557}, { 12902489, 42448510}, { 12696099, 41916758}, { 12684650, 41857975},
{ 12656516, 41713516}, { 12557005, 40938961}, { 12554067, 40837978}, { 12550435, 40713161}, { 12562692, 40535359}, { 12575839, 40344643}, { 12609216, 40034504}, { 12660395, 39915667}, { 12708691, 39803526}, { 12798899, 39599814},
{ 12938906, 39372986}, { 12995589, 39281154}, { 13232289, 39007147}, { 13498241, 38725717}, { 13591444, 38550048}, { 13628611, 38480001}, { 13631794, 38446522}, { 13586786, 38388985}, { 13507530, 38236091}, { 13096257, 38028857},
{ 12821362, 37838492}, { 12551686, 37651741}, { 12445887, 37503612}, { 12369283, 37396362}, { 12264258, 37242462}, { 12195026, 37044172}, { 12148552, 36863589}, { 12101329, 36680088}, { 12142095, 35348959}, { 12144651, 35291418},
{ 12162788, 34883134}, { 12163706, 34850506}, { 12168637, 34675334}, { 12163420, 34644423}, { 12134883, 34475307}, { 12106311, 33932082}, { 12095021, 33476333}, { 12094122, 33057779}, { 12092211, 32168031}, { 12100800, 31962352},
{ 12107580, 31800023}, { 12116077, 31640101}, { 12122543, 31518406}, { 12193613, 31111725}, { 12255946, 30755035}, { 12655685, 28642673}, { 12654000, 28322388}, { 12689137, 28120452}, { 12708722, 28007885}, { 12692342, 27740702},
{ 12770201, 27316837}, { 12810004, 27100162}, { 12822406, 26990057}, { 12840969, 26876333}, { 12930142, 26507364}, { 13006294, 26192274}, { 13140275, 25812749}, { 13171909, 25737294}, { 13213594, 25637871}, { 13513395, 24982223},
{ 13564918, 24904642}, { 13614340, 24830229}, { 13673478, 24765245}, { 13723561, 24710211}, { 13790283, 24595233}, { 13857122, 24480057}, { 14153860, 24116007}, { 14231993, 24020147}, { 14248273, 23981550}, { 14451243, 23786195},
{ 14602942, 23651634}, { 14684407, 23579375}, { 15221344, 23339532}, { 15255414, 23324310}, { 15480802, 23178412}, { 15646843, 23091400}, { 16018697, 22744059}, { 16456749, 22567685}, { 16708674, 22466255}, { 16837697, 22410158},
{ 17154392, 22190832}, { 17069931, 22106918}, { 17007737, 21985244}, { 16978925, 21928875}, { 17036320, 21826992}, { 17212750, 21670157}, { 17298093, 21594293}, { 17451145, 21457485}, { 17530883, 21256458}, { 17541075, 21230767},
{ 17549886, 21207629}, { 17244063, 20372250}, { 17209346, 20248411}, { 17092010, 20089995}, { 17023648, 19955801}, { 16984483, 19912896}, { 16834254, 19784836}, { 16625524, 19606905}, { 16620983, 19603024}, { 16616582, 19597241},
{ 16614255, 19589014}, { 16578856, 19463898}, { 16588025, 19439937}, { 16595650, 19420015}, { 16602627, 19365704}, { 16608518, 19319848}, { 16694764, 19210381}, { 16784442, 19096556}, { 16461235, 17851161}, { 16421291, 17669728},
{ 16359955, 17616552}, { 16304854, 17528237}, { 16266671, 17467038}, { 16001330, 17343372}, { 15927109, 17308781}, { 15828509, 17248124}, { 15766385, 17190011}, { 15678175, 17097940}, { 15629868, 17047518}, { 15678592, 16534350},
{ 15695434, 16356965}, { 15704034, 16303332}, { 15705308, 16269732}, { 15725443, 15743784}, { 15808595, 15332260}, { 15821377, 15312568}, { 15838901, 15285580}, { 15993537, 15201723}, { 16119571, 15175593}, { 16189683, 15163592},
{ 16237347, 15155438}, { 16508759, 15159065}, { 16375757, 14977910}, { 16282021, 14850635}, { 15510709, 13877187}, { 15342882, 13710959}, { 15237532, 13606608}, { 14831965, 13239743}, { 14581428, 13013122}, { 14293147, 12740902},
{ 14190984, 12660109}, { 13669460, 12247703}, { 12564414, 11331695}, { 12487187, 11271158}, { 12046686, 10925925}, { 11871179, 10835479}, { 11582487, 10686699}, { 11523291, 10654160}, { 11396348, 10324251}, { 11575096, 9791088},
{ 11656410, 9657529}, { 11694903, 9594301}, { 12154341, 8957487}, { 12327404, 8717611}, { 12920992, 7861977}, { 13163209, 7541046}, { 13299428, 7360558}, { 13534727, 7094968}, { 13607608, 7012705}, { 14344532, 6120949}, { 15087045, 5393680},
{ 15307430, 5177820}, { 15930737, 4553097}, { 16730116, 3841678}, { 17107544, 3505773}, { 17287251, 3346015}, { 17407773, 3251557}, { 17762201, 2970942}, { 18238970, 2593464}, { 18584923, 2367852}, { 18697829, 2294226}, { 18997703, 2084694},
{ 19253265, 1922140}, { 19413044, 1820512}, { 20082389, 1425058}, { 21018405, 914454}, { 21306702, 757182}, { 21909855, 426548}, { 22232009, 276063}, { 22432844, 180461}, { 22572399, 114027}, { 22900298, 67093}
};
out.holes.emplace_back(Slic3r::Points( {
{ 28812659, 51882256}, { 28813904, 51895244}, { 28807002, 51890550}, { 28850702, 52059657}, { 28856299, 52123368}, { 29045593, 52135332}, { 29004080, 52024610}, { 28932623, 51976002}, { 29332407, 51880142}, { 29334099, 51804647},
{ 29252306, 51781113}, { 29155613, 51753292}, { 28890648, 51728889}, { 28797131, 51720277}
} ));
return out;
}
static bool is_valid_orientation(const ExPolygon &p)
{
bool ret = p.contour.is_counter_clockwise();
for (auto &h : p.holes) ret = ret && h.is_clockwise();
return ret;
}
static bool is_efc_result_smaller(const ExPolygon &efc, const ExPolygon &orig)
{
double efc_area = efc.area();
return efc_area > 0. && efc_area < orig.area() && is_valid_orientation(efc);
}
SCENARIO("Elephant foot compensation", "[ElephantFoot]") {
GIVEN("Contour with hole") {
ExPolygon expoly = contour_with_hole();
WHEN("Compensated") {
// Elephant foot compensation shall not pinch off bits from this contour.
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.419999987f, 0.2f, 0.4f), 0.2f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_with_hole.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
}
GIVEN("Tiny contour") {
ExPolygon expoly({ { 133382606, 94912473 }, { 134232493, 95001115 }, { 133783926, 95159440 }, { 133441897, 95180666 }, { 133408242, 95191984 }, { 133339012, 95166830 }, { 132991642, 95011087 }, { 133206549, 94908304 } });
WHEN("Compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.419999987f, 0.2f, 0.4f), 0.2f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_tiny.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("Tiny contour is not compensated") {
REQUIRE(expoly_compensated == expoly);
}
}
}
GIVEN("Large box") {
ExPolygon expoly( { {50000000, 50000000 }, { 0, 50000000 }, { 0, 0 }, { 50000000, 0 } } );
WHEN("Compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.419999987f, 0.2f, 0.4f), 0.21f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_large_box.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
}
GIVEN("Thin ring (GH issue #2085)") {
ExPolygon expoly = thin_ring();
WHEN("Compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.419999987f, 0.2f, 0.4f), 0.25f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_thin_ring.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
}
#if 0
GIVEN("Varying inner offset") {
ExPolygon input = spirograph_gear_1mm().simplify(SCALED_EPSILON).front();
ExPolygon output;
std::vector<float> deltas(input.contour.points.size(), scale_(1.));
// mittered_offset_path_scaled_points is commented out somewhere above
output.contour.points = Slic3r::mittered_offset_path_scaled_points(input.contour.points, deltas, 2.);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("elephant_foot_compensation_0.svg").c_str(), get_extents(output));
svg.draw(input, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif /* TESTS_EXPORT_SVGS */
for (size_t i = 0; i <= deltas.size() / 2; ++ i)
deltas[i] = deltas[deltas.size() - i - 1] = scale_(1.) * double(i) / (0.5 * double(deltas.size()));
output.contour.points = Slic3r::mittered_offset_path_scaled_points(input.contour.points, deltas, 2.);
#ifdef TESTS_EXPORT_SVGS
{
SVG svg(debug_out_path("elephant_foot_compensation_varying.svg").c_str(), get_extents(output));
svg.draw(input, "blue");
svg.draw_outline(output, "black", coord_t(scale_(0.01)));
}
#endif /* TESTS_EXPORT_SVGS */
}
#endif
GIVEN("Rectangle with a narrow part sticking out") {
// Rectangle
ExPolygon expoly;
coord_t scaled_w = coord_t(scale_(10));
expoly.contour.points = { Vec2crd{ 0, 0 }, Vec2crd{ 0, scaled_w, }, Vec2crd{ scaled_w, scaled_w }, Vec2crd{ scaled_w, 0 } };
// Narrow part
ExPolygon expoly2;
coord_t scaled_h = coord_t(scale_(0.8));
expoly2.contour.points = { { scaled_w - coord_t(SCALED_EPSILON), scaled_w / 2 }, { scaled_w - coord_t(SCALED_EPSILON), scaled_w / 2 + scaled_h, },
{ 2 * scaled_w, scaled_w / 2 + scaled_h }, { 2 * scaled_w, scaled_w / 2 } };
// Rectangle with the narrow part.
expoly = union_ex({ expoly, expoly2 }).front();
WHEN("Partially compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.45f, 0.2f, 0.4f), 0.25f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_0.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
WHEN("Fully compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.35f, 0.2f, 0.4f), 0.17f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_1.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
}
GIVEN("Box with hole close to wall (GH issue #2998)") {
ExPolygon expoly = box_with_hole_close_to_wall();
WHEN("Compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.419999987f, 0.2f, 0.4f), 0.25f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_2.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
}
GIVEN("Spirograph wheel") {
// Rectangle
ExPolygon expoly = spirograph_gear_1mm();
WHEN("Partially compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.45f, 0.2f, 0.4f), 0.25f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_2.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
WHEN("Fully compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.35f, 0.2f, 0.4f), 0.17f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_3.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
WHEN("Brutally compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.45f, 0.2f, 0.4f), 0.6f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_4.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
}
GIVEN("Vase with fins") {
ExPolygon expoly = vase_with_fins();
WHEN("Compensated") {
ExPolygon expoly_compensated = elephant_foot_compensation(expoly, Flow(0.419999987f, 0.2f, 0.4f), 0.41f);
#ifdef TESTS_EXPORT_SVGS
SVG::export_expolygons(debug_out_path("elephant_foot_compensation_vase_with_fins.svg").c_str(),
{ { { expoly }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } },
{ { expoly_compensated }, { "gray", "black", "blue", coord_t(scale_(0.02)), 0.5f, "black", coord_t(scale_(0.05)) } } });
#endif /* TESTS_EXPORT_SVGS */
THEN("area of the compensated polygon is smaller") {
REQUIRE(is_efc_result_smaller(expoly_compensated, expoly));
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
#include <catch2/catch.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/ExPolygon.hpp"
using namespace Slic3r;
static inline bool points_close(const Point &p1, const Point &p2)
{
return (p1 - p2).cast<double>().norm() < SCALED_EPSILON;
}
static bool polygons_close_permuted(const Polygon &poly1, const Polygon &poly2, const std::vector<int> &permutation2)
{
if (poly1.size() != poly2.size() || poly1.size() != permutation2.size())
return false;
for (size_t i = 0; i < poly1.size(); ++ i)
if (poly1[i] != poly2[permutation2[i]])
return false;
return true;
}
SCENARIO("Basics", "[ExPolygon]") {
GIVEN("ccw_square") {
Polygon ccw_square{ { 100, 100 }, { 200, 100 }, { 200, 200 }, { 100, 200 } };
Polygon cw_hole_in_square{ { 140, 140 }, { 140, 160 }, { 160, 160 }, { 160, 140 } };
ExPolygon expolygon { ccw_square, cw_hole_in_square };
THEN("expolygon is valid") {
REQUIRE(expolygon.is_valid());
}
THEN("expolygon area") {
REQUIRE(expolygon.area() == Approx(100*100-20*20));
}
WHEN("Expolygon scaled") {
ExPolygon expolygon2 = expolygon;
expolygon2.scale(2.5);
REQUIRE(expolygon.contour.size() == expolygon2.contour.size());
REQUIRE(expolygon.holes.size() == 1);
REQUIRE(expolygon2.holes.size() == 1);
for (size_t i = 0; i < expolygon.contour.size(); ++ i)
REQUIRE(points_close(expolygon.contour[i] * 2.5, expolygon2.contour[i]));
for (size_t i = 0; i < expolygon.holes.front().size(); ++ i)
REQUIRE(points_close(expolygon.holes.front()[i] * 2.5, expolygon2.holes.front()[i]));
}
WHEN("Expolygon translated") {
ExPolygon expolygon2 = expolygon;
expolygon2.translate(10, -5);
REQUIRE(expolygon.contour.size() == expolygon2.contour.size());
REQUIRE(expolygon.holes.size() == 1);
REQUIRE(expolygon2.holes.size() == 1);
for (size_t i = 0; i < expolygon.contour.size(); ++ i)
REQUIRE(points_close(expolygon.contour[i] + Point(10, -5), expolygon2.contour[i]));
for (size_t i = 0; i < expolygon.holes.front().size(); ++ i)
REQUIRE(points_close(expolygon.holes.front()[i] + Point(10, -5), expolygon2.holes.front()[i]));
}
WHEN("Expolygon rotated around point") {
ExPolygon expolygon2 = expolygon;
expolygon2.rotate(M_PI / 2, Point(150, 150));
REQUIRE(expolygon.contour.size() == expolygon2.contour.size());
REQUIRE(expolygon.holes.size() == 1);
REQUIRE(expolygon2.holes.size() == 1);
REQUIRE(polygons_close_permuted(expolygon2.contour, expolygon.contour, { 1, 2, 3, 0}));
REQUIRE(polygons_close_permuted(expolygon2.holes.front(), expolygon.holes.front(), { 3, 0, 1, 2}));
}
}
}

View File

@@ -0,0 +1,790 @@
#include <catch2/catch.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/Line.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/Circle.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ShortestPath.hpp"
//#include <random>
//#include "libnest2d/tools/benchmark.h"
#include "libslic3r/SVG.hpp"
#include "../libnest2d/printer_parts.hpp"
#include <unordered_set>
using namespace Slic3r;
TEST_CASE("Line::parallel_to", "[Geometry]"){
Line l{ { 100000, 0 }, { 0, 0 } };
Line l2{ { 200000, 0 }, { 0, 0 } };
REQUIRE(l.parallel_to(l));
REQUIRE(l.parallel_to(l2));
Line l3(l2);
l3.rotate(0.9 * EPSILON, { 0, 0 });
REQUIRE(l.parallel_to(l3));
Line l4(l2);
l4.rotate(1.1 * EPSILON, { 0, 0 });
REQUIRE(! l.parallel_to(l4));
// The angle epsilon is so low that vectors shorter than 100um rotated by epsilon radians are not rotated at all.
Line l5{ { 20000, 0 }, { 0, 0 } };
l5.rotate(1.1 * EPSILON, { 0, 0 });
REQUIRE(l.parallel_to(l5));
l.rotate(1., { 0, 0 });
Point offset{ 342876, 97636249 };
l.translate(offset);
l3.rotate(1., { 0, 0 });
l3.translate(offset);
l4.rotate(1., { 0, 0 });
l4.translate(offset);
REQUIRE(l.parallel_to(l3));
REQUIRE(!l.parallel_to(l4));
}
TEST_CASE("Line::perpendicular_to", "[Geometry]") {
Line l{ { 100000, 0 }, { 0, 0 } };
Line l2{ { 0, 200000 }, { 0, 0 } };
REQUIRE(! l.perpendicular_to(l));
REQUIRE(l.perpendicular_to(l2));
Line l3(l2);
l3.rotate(0.9 * EPSILON, { 0, 0 });
REQUIRE(l.perpendicular_to(l3));
Line l4(l2);
l4.rotate(1.1 * EPSILON, { 0, 0 });
REQUIRE(! l.perpendicular_to(l4));
// The angle epsilon is so low that vectors shorter than 100um rotated by epsilon radians are not rotated at all.
Line l5{ { 0, 20000 }, { 0, 0 } };
l5.rotate(1.1 * EPSILON, { 0, 0 });
REQUIRE(l.perpendicular_to(l5));
l.rotate(1., { 0, 0 });
Point offset{ 342876, 97636249 };
l.translate(offset);
l3.rotate(1., { 0, 0 });
l3.translate(offset);
l4.rotate(1., { 0, 0 });
l4.translate(offset);
REQUIRE(l.perpendicular_to(l3));
REQUIRE(! l.perpendicular_to(l4));
}
TEST_CASE("Polygon::contains works properly", "[Geometry]"){
// this test was failing on Windows (GH #1950)
Slic3r::Polygon polygon(Points({
Point(207802834,-57084522),
Point(196528149,-37556190),
Point(173626821,-25420928),
Point(171285751,-21366123),
Point(118673592,-21366123),
Point(116332562,-25420928),
Point(93431208,-37556191),
Point(82156517,-57084523),
Point(129714478,-84542120),
Point(160244873,-84542120)
}));
Point point(95706562, -57294774);
REQUIRE(polygon.contains(point));
}
SCENARIO("Intersections of line segments", "[Geometry]"){
GIVEN("Integer coordinates"){
Line line1(Point(5,15),Point(30,15));
Line line2(Point(10,20), Point(10,10));
THEN("The intersection is valid"){
Point point;
line1.intersection(line2,&point);
REQUIRE(Point(10,15) == point);
}
}
GIVEN("Scaled coordinates"){
Line line1(Point(73.6310778185108 / 0.00001, 371.74239268924 / 0.00001), Point(73.6310778185108 / 0.00001, 501.74239268924 / 0.00001));
Line line2(Point(75/0.00001, 437.9853/0.00001), Point(62.7484/0.00001, 440.4223/0.00001));
THEN("There is still an intersection"){
Point point;
REQUIRE(line1.intersection(line2,&point));
}
}
}
SCENARIO("polygon_is_convex works") {
GIVEN("A square of dimension 10") {
WHEN("Polygon is convex clockwise") {
Polygon cw_square { { {0, 0}, {0,10}, {10,10}, {10,0} } };
THEN("it is not convex") {
REQUIRE(! polygon_is_convex(cw_square));
}
}
WHEN("Polygon is convex counter-clockwise") {
Polygon ccw_square { { {0, 0}, {10,0}, {10,10}, {0,10} } };
THEN("it is convex") {
REQUIRE(polygon_is_convex(ccw_square));
}
}
}
GIVEN("A concave polygon") {
Polygon concave = { {0,0}, {10,0}, {10,10}, {0,10}, {0,6}, {4,6}, {4,4}, {0,4} };
THEN("It is not convex") {
REQUIRE(! polygon_is_convex(concave));
}
}
}
TEST_CASE("Creating a polyline generates the obvious lines", "[Geometry]"){
Slic3r::Polyline polyline;
polyline.points = Points({Point(0, 0), Point(10, 0), Point(20, 0)});
REQUIRE(polyline.lines().at(0).a == Point(0,0));
REQUIRE(polyline.lines().at(0).b == Point(10,0));
REQUIRE(polyline.lines().at(1).a == Point(10,0));
REQUIRE(polyline.lines().at(1).b == Point(20,0));
}
TEST_CASE("Splitting a Polygon generates a polyline correctly", "[Geometry]"){
Slic3r::Polygon polygon(Points({Point(0, 0), Point(10, 0), Point(5, 5)}));
Slic3r::Polyline split = polygon.split_at_index(1);
REQUIRE(split.points[0]==Point(10,0));
REQUIRE(split.points[1]==Point(5,5));
REQUIRE(split.points[2]==Point(0,0));
REQUIRE(split.points[3]==Point(10,0));
}
SCENARIO("BoundingBox", "[Geometry]") {
WHEN("Bounding boxes are scaled") {
BoundingBox bb(Points({Point(0, 1), Point(10, 2), Point(20, 2)}));
bb.scale(2);
REQUIRE(bb.min == Point(0,2));
REQUIRE(bb.max == Point(40,4));
}
WHEN("BoundingBox constructed from points") {
BoundingBox bb(Points{ {100,200}, {100, 200}, {500, -600} });
THEN("minimum is correct") {
REQUIRE(bb.min == Point{100,-600});
}
THEN("maximum is correct") {
REQUIRE(bb.max == Point{500,200});
}
}
WHEN("BoundingBox constructed from a single point") {
BoundingBox bb;
bb.merge({10, 10});
THEN("minimum equals to the only defined point") {
REQUIRE(bb.min == Point{10,10});
}
THEN("maximum equals to the only defined point") {
REQUIRE(bb.max == Point{10,10});
}
}
}
TEST_CASE("Offseting a line generates a polygon correctly", "[Geometry]"){
Slic3r::Polyline tmp = { Point(10,10), Point(20,10) };
Slic3r::Polygon area = offset(tmp,5).at(0);
REQUIRE(area.area() == Slic3r::Polygon(Points({Point(10,5),Point(20,5),Point(20,15),Point(10,15)})).area());
}
SCENARIO("Circle Fit, TaubinFit with Newton's method", "[Geometry]") {
GIVEN("A vector of Vec2ds arranged in a half-circle with approximately the same distance R from some point") {
Vec2d expected_center(-6, 0);
Vec2ds sample {Vec2d(6.0, 0), Vec2d(5.1961524, 3), Vec2d(3 ,5.1961524), Vec2d(0, 6.0), Vec2d(3, 5.1961524), Vec2d(-5.1961524, 3), Vec2d(-6.0, 0)};
std::transform(sample.begin(), sample.end(), sample.begin(), [expected_center] (const Vec2d& a) { return a + expected_center;});
WHEN("Circle fit is called on the entire array") {
Vec2d result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample);
THEN("A center point of -6,0 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
WHEN("Circle fit is called on the first four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample.cbegin(), sample.cbegin()+4);
THEN("A center point of -6,0 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
WHEN("Circle fit is called on the middle four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample.cbegin()+2, sample.cbegin()+6);
THEN("A center point of -6,0 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
}
GIVEN("A vector of Vec2ds arranged in a half-circle with approximately the same distance R from some point") {
Vec2d expected_center(-3, 9);
Vec2ds sample {Vec2d(6.0, 0), Vec2d(5.1961524, 3), Vec2d(3 ,5.1961524),
Vec2d(0, 6.0),
Vec2d(3, 5.1961524), Vec2d(-5.1961524, 3), Vec2d(-6.0, 0)};
std::transform(sample.begin(), sample.end(), sample.begin(), [expected_center] (const Vec2d& a) { return a + expected_center;});
WHEN("Circle fit is called on the entire array") {
Vec2d result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample);
THEN("A center point of 3,9 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
WHEN("Circle fit is called on the first four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample.cbegin(), sample.cbegin()+4);
THEN("A center point of 3,9 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
WHEN("Circle fit is called on the middle four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample.cbegin()+2, sample.cbegin()+6);
THEN("A center point of 3,9 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
}
GIVEN("A vector of Points arranged in a half-circle with approximately the same distance R from some point") {
Point expected_center { Point::new_scale(-3, 9)};
Points sample {Point::new_scale(6.0, 0), Point::new_scale(5.1961524, 3), Point::new_scale(3 ,5.1961524),
Point::new_scale(0, 6.0),
Point::new_scale(3, 5.1961524), Point::new_scale(-5.1961524, 3), Point::new_scale(-6.0, 0)};
std::transform(sample.begin(), sample.end(), sample.begin(), [expected_center] (const Point& a) { return a + expected_center;});
WHEN("Circle fit is called on the entire array") {
Point result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample);
THEN("A center point of scaled 3,9 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
WHEN("Circle fit is called on the first four points") {
Point result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample.cbegin(), sample.cbegin()+4);
THEN("A center point of scaled 3,9 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
WHEN("Circle fit is called on the middle four points") {
Point result_center(0,0);
result_center = Geometry::circle_center_taubin_newton(sample.cbegin()+2, sample.cbegin()+6);
THEN("A center point of scaled 3,9 is returned.") {
REQUIRE(is_approx(result_center, expected_center));
}
}
}
}
TEST_CASE("smallest_enclosing_circle_welzl", "[Geometry]") {
// Some random points in plane.
Points pts {
{ 89243, 4359 }, { 763465, 59687 }, { 3245, 734987 }, { 2459867, 987634 }, { 759866, 67843982 }, { 9754687, 9834658 }, { 87235089, 743984373 },
{ 65874456, 2987546 }, { 98234524, 657654873 }, { 786243598, 287934765 }, { 824356, 734265 }, { 82576449, 7864534 }, { 7826345, 3984765 }
};
const auto c = Slic3r::Geometry::smallest_enclosing_circle_welzl(pts);
// The radius returned is inflated by SCALED_EPSILON, thus all points should be inside.
bool all_inside = std::all_of(pts.begin(), pts.end(), [c](const Point &pt){ return c.contains(pt.cast<double>()); });
auto c2(c);
c2.radius -= SCALED_EPSILON * 2.1;
auto num_on_boundary = std::count_if(pts.begin(), pts.end(), [c2](const Point& pt) { return ! c2.contains(pt.cast<double>(), SCALED_EPSILON); });
REQUIRE(all_inside);
REQUIRE(num_on_boundary == 3);
}
SCENARIO("Path chaining", "[Geometry]") {
GIVEN("A path") {
Points points = { Point(26,26),Point(52,26),Point(0,26),Point(26,52),Point(26,0),Point(0,52),Point(52,52),Point(52,0) };
THEN("Chained with no diagonals (thus 26 units long)") {
std::vector<Points::size_type> indices = chain_points(points);
for (Points::size_type i = 0; i + 1 < indices.size(); ++ i) {
double dist = (points.at(indices.at(i)).cast<double>() - points.at(indices.at(i+1)).cast<double>()).norm();
REQUIRE(std::abs(dist-26) <= EPSILON);
}
}
}
GIVEN("Gyroid infill end points") {
Polylines polylines = {
{ {28122608, 3221037}, {27919139, 56036027} },
{ {33642863, 3400772}, {30875220, 56450360} },
{ {34579315, 3599827}, {35049758, 55971572} },
{ {26483070, 3374004}, {23971830, 55763598} },
{ {38931405, 4678879}, {38740053, 55077714} },
{ {20311895, 5015778}, {20079051, 54551952} },
{ {16463068, 6773342}, {18823514, 53992958} },
{ {44433771, 7424951}, {42629462, 53346059} },
{ {15697614, 7329492}, {15350896, 52089991} },
{ {48085792, 10147132}, {46435427, 50792118} },
{ {48828819, 10972330}, {49126582, 48368374} },
{ {9654526, 12656711}, {10264020, 47691584} },
{ {5726905, 18648632}, {8070762, 45082416} },
{ {54818187, 39579970}, {52974912, 43271272} },
{ {4464342, 37371742}, {5027890, 39106220} },
{ {54139746, 18417661}, {55177987, 38472580} },
{ {56527590, 32058461}, {56316456, 34067185} },
{ {3303988, 29215290}, {3569863, 32985633} },
{ {56255666, 25025857}, {56478310, 27144087} },
{ {4300034, 22805361}, {3667946, 25752601} },
{ {8266122, 14250611}, {6244813, 17751595} },
{ {12177955, 9886741}, {10703348, 11491900} }
};
Polylines chained = chain_polylines(polylines);
THEN("Chained taking the shortest path") {
double connection_length = 0.;
for (size_t i = 1; i < chained.size(); ++i) {
const Polyline &pl1 = chained[i - 1];
const Polyline &pl2 = chained[i];
connection_length += (pl2.first_point() - pl1.last_point()).cast<double>().norm();
}
REQUIRE(connection_length < 85206000.);
}
}
GIVEN("Loop pieces") {
Point a { 2185796, 19058485 };
Point b { 3957902, 18149382 };
Point c { 2912841, 18790564 };
Point d { 2831848, 18832390 };
Point e { 3179601, 18627769 };
Point f { 3137952, 18653370 };
Polylines polylines = { { a, b },
{ c, d },
{ e, f },
{ d, a },
{ f, c },
{ b, e } };
Polylines chained = chain_polylines(polylines, &a);
THEN("Connected without a gap") {
for (size_t i = 0; i < chained.size(); ++i) {
const Polyline &pl1 = (i == 0) ? chained.back() : chained[i - 1];
const Polyline &pl2 = chained[i];
REQUIRE(pl1.points.back() == pl2.points.front());
}
}
}
}
SCENARIO("Line distances", "[Geometry]"){
GIVEN("A line"){
Line line(Point(0, 0), Point(20, 0));
THEN("Points on the line segment have 0 distance"){
REQUIRE(line.distance_to(Point(0, 0)) == 0);
REQUIRE(line.distance_to(Point(20, 0)) == 0);
REQUIRE(line.distance_to(Point(10, 0)) == 0);
}
THEN("Points off the line have the appropriate distance"){
REQUIRE(line.distance_to(Point(10, 10)) == 10);
REQUIRE(line.distance_to(Point(50, 0)) == 30);
}
}
}
SCENARIO("Calculating angles", "[Geometry]")
{
GIVEN(("Vectors 30 degrees apart"))
{
std::vector<std::pair<Point, Point>> pts {
{ {1000, 0}, { 866, 500 } },
{ { 866, 500 }, { 500, 866 } },
{ { 500, 866 }, { 0, 1000 } },
{ { -500, 866 }, { -866, 500 } }
};
THEN("Angle detected is 30 degrees")
{
for (auto &p : pts)
REQUIRE(is_approx(angle(p.first, p.second), M_PI / 6.));
}
}
GIVEN(("Vectors 30 degrees apart"))
{
std::vector<std::pair<Point, Point>> pts {
{ { 866, 500 }, {1000, 0} },
{ { 500, 866 }, { 866, 500 } },
{ { 0, 1000 }, { 500, 866 } },
{ { -866, 500 }, { -500, 866 } }
};
THEN("Angle detected is -30 degrees")
{
for (auto &p : pts)
REQUIRE(is_approx(angle(p.first, p.second), - M_PI / 6.));
}
}
}
SCENARIO("Polygon convex/concave detection", "[Geometry]"){
static constexpr const double angle_threshold = M_PI / 3.;
GIVEN(("A Square with dimension 100")){
auto square = Slic3r::Polygon /*new_scale*/(Points({
Point(100,100),
Point(200,100),
Point(200,200),
Point(100,200)}));
THEN("It has 4 convex points counterclockwise"){
REQUIRE(square.concave_points(angle_threshold).size() == 0);
REQUIRE(square.convex_points(angle_threshold).size() == 4);
}
THEN("It has 4 concave points clockwise"){
square.make_clockwise();
REQUIRE(square.concave_points(angle_threshold).size() == 4);
REQUIRE(square.convex_points(angle_threshold).size() == 0);
}
}
GIVEN("A Square with an extra colinearvertex"){
auto square = Slic3r::Polygon /*new_scale*/(Points({
Point(150,100),
Point(200,100),
Point(200,200),
Point(100,200),
Point(100,100)}));
THEN("It has 4 convex points counterclockwise"){
REQUIRE(square.concave_points(angle_threshold).size() == 0);
REQUIRE(square.convex_points(angle_threshold).size() == 4);
}
}
GIVEN("A Square with an extra collinear vertex in different order"){
auto square = Slic3r::Polygon /*new_scale*/(Points({
Point(200,200),
Point(100,200),
Point(100,100),
Point(150,100),
Point(200,100)}));
THEN("It has 4 convex points counterclockwise"){
REQUIRE(square.concave_points(angle_threshold).size() == 0);
REQUIRE(square.convex_points(angle_threshold).size() == 4);
}
}
GIVEN("A triangle"){
auto triangle = Slic3r::Polygon(Points({
Point(16000170,26257364),
Point(714223,461012),
Point(31286371,461008)
}));
THEN("it has three convex vertices"){
REQUIRE(triangle.concave_points(angle_threshold).size() == 0);
REQUIRE(triangle.convex_points(angle_threshold).size() == 3);
}
}
GIVEN("A triangle with an extra collinear point"){
auto triangle = Slic3r::Polygon(Points({
Point(16000170,26257364),
Point(714223,461012),
Point(20000000,461012),
Point(31286371,461012)
}));
THEN("it has three convex vertices"){
REQUIRE(triangle.concave_points(angle_threshold).size() == 0);
REQUIRE(triangle.convex_points(angle_threshold).size() == 3);
}
}
GIVEN("A polygon with concave vertices with angles of specifically 4/3pi"){
// Two concave vertices of this polygon have angle = PI*4/3, so this test fails
// if epsilon is not used.
auto polygon = Slic3r::Polygon(Points({
Point(60246458,14802768),Point(64477191,12360001),
Point(63727343,11060995),Point(64086449,10853608),
Point(66393722,14850069),Point(66034704,15057334),
Point(65284646,13758387),Point(61053864,16200839),
Point(69200258,30310849),Point(62172547,42483120),
Point(61137680,41850279),Point(67799985,30310848),
Point(51399866,1905506),Point(38092663,1905506),
Point(38092663,692699),Point(52100125,692699)
}));
THEN("the correct number of points are detected"){
REQUIRE(polygon.concave_points(angle_threshold).size() == 6);
REQUIRE(polygon.convex_points(angle_threshold).size() == 10);
}
}
}
TEST_CASE("Triangle Simplification does not result in less than 3 points", "[Geometry]"){
auto triangle = Slic3r::Polygon(Points({
Point(16000170,26257364), Point(714223,461012), Point(31286371,461008)
}));
REQUIRE(triangle.simplify(250000).at(0).points.size() == 3);
}
SCENARIO("Ported from xs/t/14_geometry.t", "[Geometry]"){
GIVEN(("square")){
Slic3r::Points points { { 100, 100 }, {100, 200 }, { 200, 200 }, { 200, 100 }, { 150, 150 } };
Slic3r::Polygon hull = Slic3r::Geometry::convex_hull(points);
SECTION("convex hull returns the correct number of points") { REQUIRE(hull.points.size() == 4); }
}
SECTION("arrange returns expected number of positions") {
Pointfs positions;
Slic3r::Geometry::arrange(4, Vec2d(20, 20), 5, nullptr, positions);
REQUIRE(positions.size() == 4);
}
SECTION("directions_parallel") {
REQUIRE(Slic3r::Geometry::directions_parallel(0, 0, 0));
REQUIRE(Slic3r::Geometry::directions_parallel(0, M_PI, 0));
REQUIRE(Slic3r::Geometry::directions_parallel(0, 0, M_PI / 180));
REQUIRE(Slic3r::Geometry::directions_parallel(0, M_PI, M_PI / 180));
REQUIRE(! Slic3r::Geometry::directions_parallel(M_PI /2, M_PI, 0));
REQUIRE(! Slic3r::Geometry::directions_parallel(M_PI /2, PI, M_PI /180));
}
}
TEST_CASE("Convex polygon intersection on two disjoint squares", "[Geometry][Rotcalip]") {
Polygon A{{0, 0}, {10, 0}, {10, 10}, {0, 10}};
A.scale(1. / SCALING_FACTOR);
Polygon B = A;
B.translate(20 / SCALING_FACTOR, 0);
bool is_inters = Geometry::convex_polygons_intersect(A, B);
REQUIRE(is_inters == false);
}
TEST_CASE("Convex polygon intersection on two intersecting squares", "[Geometry][Rotcalip]") {
Polygon A{{0, 0}, {10, 0}, {10, 10}, {0, 10}};
A.scale(1. / SCALING_FACTOR);
Polygon B = A;
B.translate(5 / SCALING_FACTOR, 5 / SCALING_FACTOR);
bool is_inters = Geometry::convex_polygons_intersect(A, B);
REQUIRE(is_inters == true);
}
TEST_CASE("Convex polygon intersection on two squares touching one edge", "[Geometry][Rotcalip]") {
Polygon A{{0, 0}, {10, 0}, {10, 10}, {0, 10}};
A.scale(1. / SCALING_FACTOR);
Polygon B = A;
B.translate(10 / SCALING_FACTOR, 0);
bool is_inters = Geometry::convex_polygons_intersect(A, B);
REQUIRE(is_inters == false);
}
TEST_CASE("Convex polygon intersection on two squares touching one vertex", "[Geometry][Rotcalip]") {
Polygon A{{0, 0}, {10, 0}, {10, 10}, {0, 10}};
A.scale(1. / SCALING_FACTOR);
Polygon B = A;
B.translate(10 / SCALING_FACTOR, 10 / SCALING_FACTOR);
SVG svg{std::string("one_vertex_touch") + ".svg"};
svg.draw(A, "blue");
svg.draw(B, "green");
svg.Close();
bool is_inters = Geometry::convex_polygons_intersect(A, B);
REQUIRE(is_inters == false);
}
TEST_CASE("Convex polygon intersection on two overlapping squares", "[Geometry][Rotcalip]") {
Polygon A{{0, 0}, {10, 0}, {10, 10}, {0, 10}};
A.scale(1. / SCALING_FACTOR);
Polygon B = A;
bool is_inters = Geometry::convex_polygons_intersect(A, B);
REQUIRE(is_inters == true);
}
//// Only for benchmarking
//static Polygon gen_convex_poly(std::mt19937_64 &rg, size_t point_cnt)
//{
// std::uniform_int_distribution<coord_t> dist(0, 100);
// Polygon out;
// out.points.reserve(point_cnt);
// coord_t tr = dist(rg) * 2 / SCALING_FACTOR;
// for (size_t i = 0; i < point_cnt; ++i)
// out.points.emplace_back(tr + dist(rg) / SCALING_FACTOR,
// tr + dist(rg) / SCALING_FACTOR);
// return Geometry::convex_hull(out.points);
//}
//TEST_CASE("Convex polygon intersection test on random polygons", "[Geometry]") {
// constexpr size_t TEST_CNT = 1000;
// constexpr size_t POINT_CNT = 1000;
// auto seed = std::random_device{}();
//// unsigned long seed = 2525634386;
// std::mt19937_64 rg{seed};
// Benchmark bench;
// auto tests = reserve_vector<std::pair<Polygon, Polygon>>(TEST_CNT);
// auto results = reserve_vector<bool>(TEST_CNT);
// auto expects = reserve_vector<bool>(TEST_CNT);
// for (size_t i = 0; i < TEST_CNT; ++i) {
// tests.emplace_back(gen_convex_poly(rg, POINT_CNT), gen_convex_poly(rg, POINT_CNT));
// }
// bench.start();
// for (const auto &test : tests)
// results.emplace_back(Geometry::convex_polygons_intersect(test.first, test.second));
// bench.stop();
// std::cout << "Test time: " << bench.getElapsedSec() << std::endl;
// bench.start();
// for (const auto &test : tests)
// expects.emplace_back(!intersection(test.first, test.second).empty());
// bench.stop();
// std::cout << "Clipper time: " << bench.getElapsedSec() << std::endl;
// REQUIRE(results.size() == expects.size());
// auto seedstr = std::to_string(seed);
// for (size_t i = 0; i < results.size(); ++i) {
// // std::cout << expects[i] << " ";
// if (results[i] != expects[i]) {
// SVG svg{std::string("fail_seed") + seedstr + "_" + std::to_string(i) + ".svg"};
// svg.draw(tests[i].first, "blue");
// svg.draw(tests[i].second, "green");
// svg.Close();
// // std::cout << std::endl;
// }
// REQUIRE(results[i] == expects[i]);
// }
// std::cout << std::endl;
//}
struct Pair
{
size_t first, second;
bool operator==(const Pair &b) const { return first == b.first && second == b.second; }
};
template<> struct std::hash<Pair> {
size_t operator()(const Pair &c) const
{
return c.first * PRINTER_PART_POLYGONS.size() + c.second;
}
};
TEST_CASE("Convex polygon intersection test qidi polygons", "[Geometry][Rotcalip]") {
// Overlap of the same polygon should always be an intersection
for (size_t i = 0; i < PRINTER_PART_POLYGONS.size(); ++i) {
Polygon P = PRINTER_PART_POLYGONS[i];
P = Geometry::convex_hull(P.points);
bool res = Geometry::convex_polygons_intersect(P, P);
if (!res) {
SVG svg{std::string("fail_self") + std::to_string(i) + ".svg"};
svg.draw(P, "green");
svg.Close();
}
REQUIRE(res == true);
}
std::unordered_set<Pair> combos;
for (size_t i = 0; i < PRINTER_PART_POLYGONS.size(); ++i) {
for (size_t j = 0; j < PRINTER_PART_POLYGONS.size(); ++j) {
if (i != j) {
size_t a = std::min(i, j), b = std::max(i, j);
combos.insert(Pair{a, b});
}
}
}
// All disjoint
for (const auto &combo : combos) {
Polygon A = PRINTER_PART_POLYGONS[combo.first], B = PRINTER_PART_POLYGONS[combo.second];
A = Geometry::convex_hull(A.points);
B = Geometry::convex_hull(B.points);
auto bba = A.bounding_box();
auto bbb = B.bounding_box();
A.translate(-bba.center());
B.translate(-bbb.center());
B.translate(bba.size() + bbb.size());
bool res = Geometry::convex_polygons_intersect(A, B);
bool ref = !intersection(A, B).empty();
if (res != ref) {
SVG svg{std::string("fail") + std::to_string(combo.first) + "_" + std::to_string(combo.second) + ".svg"};
svg.draw(A, "blue");
svg.draw(B, "green");
svg.Close();
}
REQUIRE(res == ref);
}
// All intersecting
for (const auto &combo : combos) {
Polygon A = PRINTER_PART_POLYGONS[combo.first], B = PRINTER_PART_POLYGONS[combo.second];
A = Geometry::convex_hull(A.points);
B = Geometry::convex_hull(B.points);
auto bba = A.bounding_box();
auto bbb = B.bounding_box();
A.translate(-bba.center());
B.translate(-bbb.center());
bool res = Geometry::convex_polygons_intersect(A, B);
bool ref = !intersection(A, B).empty();
if (res != ref) {
SVG svg{std::string("fail") + std::to_string(combo.first) + "_" + std::to_string(combo.second) + ".svg"};
svg.draw(A, "blue");
svg.draw(B, "green");
svg.Close();
}
REQUIRE(res == ref);
}
}
TEST_CASE("Euler angles roundtrip", "[Geometry]") {
std::vector<Vec3d> euler_angles_vec = {{M_PI/2., -M_PI, 0.},
{M_PI, -M_PI, 0.},
{M_PI, -M_PI, 2*M_PI},
{0., 0., M_PI},
{M_PI, M_PI/2., 0.},
{0.2, 0.3, -0.5}};
// Also include all combinations of zero and +-pi/2:
for (double x : {0., M_PI/2., -M_PI/2.})
for (double y : {0., M_PI/2., -M_PI/2.})
for (double z : {0., M_PI/2., -M_PI/2.})
euler_angles_vec.emplace_back(x, y, z);
for (Vec3d& euler_angles : euler_angles_vec) {
Transform3d trafo1 = Geometry::rotation_transform(euler_angles);
euler_angles = Geometry::extract_rotation(trafo1);
Transform3d trafo2 = Geometry::rotation_transform(euler_angles);
REQUIRE(trafo1.isApprox(trafo2));
}
}

View File

@@ -0,0 +1,21 @@
#include <iostream>
#include <fstream>
#include <catch2/catch.hpp>
#include "libslic3r/SLA/Hollowing.hpp"
TEST_CASE("Hollow two overlapping spheres") {
using namespace Slic3r;
TriangleMesh sphere1 = make_sphere(10., 2 * PI / 20.), sphere2 = sphere1;
sphere1.translate(-5.f, 0.f, 0.f);
sphere2.translate( 5.f, 0.f, 0.f);
sphere1.merge(sphere2);
sla::hollow_mesh(sphere1, sla::HollowingConfig{}, sla::HollowingFlags::hfRemoveInsideTriangles);
sphere1.WriteOBJFile("twospheres.obj");
}

View File

@@ -0,0 +1,240 @@
#include <iostream>
#include <fstream>
#include <catch2/catch.hpp>
#include "libslic3r/TriangleMesh.hpp"
using namespace Slic3r;
TEST_CASE("Split empty mesh", "[its_split][its]") {
using namespace Slic3r;
indexed_triangle_set its;
std::vector<indexed_triangle_set> res = its_split(its);
REQUIRE(res.empty());
}
TEST_CASE("Split simple mesh consisting of one part", "[its_split][its]") {
using namespace Slic3r;
auto cube = its_make_cube(10., 10., 10.);
std::vector<indexed_triangle_set> res = its_split(cube);
REQUIRE(res.size() == 1);
REQUIRE(res.front().indices.size() == cube.indices.size());
REQUIRE(res.front().vertices.size() == cube.vertices.size());
}
void debug_write_obj(const std::vector<indexed_triangle_set> &res, const std::string &name)
{
#ifndef NDEBUG
size_t part_idx = 0;
for (auto &part_its : res) {
its_write_obj(part_its, (name + std::to_string(part_idx++) + ".obj").c_str());
}
#endif
}
TEST_CASE("Split two non-watertight mesh", "[its_split][its]") {
using namespace Slic3r;
auto cube1 = its_make_cube(10., 10., 10.);
cube1.indices.pop_back();
auto cube2 = cube1;
its_transform(cube1, identity3f().translate(Vec3f{-5.f, 0.f, 0.f}));
its_transform(cube2, identity3f().translate(Vec3f{5.f, 0.f, 0.f}));
its_merge(cube1, cube2);
std::vector<indexed_triangle_set> res = its_split(cube1);
REQUIRE(res.size() == 2);
REQUIRE(res[0].indices.size() == res[1].indices.size());
REQUIRE(res[0].indices.size() == cube2.indices.size());
REQUIRE(res[0].vertices.size() == res[1].vertices.size());
REQUIRE(res[0].vertices.size() == cube2.vertices.size());
debug_write_obj(res, "parts_non_watertight");
}
TEST_CASE("Split non-manifold mesh", "[its_split][its]") {
using namespace Slic3r;
auto cube = its_make_cube(10., 10., 10.), cube_low = cube;
its_transform(cube_low, identity3f().translate(Vec3f{10.f, 10.f, 10.f}));
its_merge(cube, cube_low);
its_merge_vertices(cube);
std::vector<indexed_triangle_set> res = its_split(cube);
REQUIRE(res.size() == 2);
REQUIRE(res[0].indices.size() == res[1].indices.size());
REQUIRE(res[0].indices.size() == cube_low.indices.size());
REQUIRE(res[0].vertices.size() == res[1].vertices.size());
REQUIRE(res[0].vertices.size() == cube_low.vertices.size());
debug_write_obj(res, "cubes_non_manifold");
}
TEST_CASE("Split two watertight meshes", "[its_split][its]") {
using namespace Slic3r;
auto sphere1 = its_make_sphere(10., 2 * PI / 200.), sphere2 = sphere1;
its_transform(sphere1, identity3f().translate(Vec3f{-5.f, 0.f, 0.f}));
its_transform(sphere2, identity3f().translate(Vec3f{5.f, 0.f, 0.f}));
its_merge(sphere1, sphere2);
std::vector<indexed_triangle_set> res = its_split(sphere1);
REQUIRE(res.size() == 2);
REQUIRE(res[0].indices.size() == res[1].indices.size());
REQUIRE(res[0].indices.size() == sphere2.indices.size());
REQUIRE(res[0].vertices.size() == res[1].vertices.size());
REQUIRE(res[0].vertices.size() == sphere2.vertices.size());
debug_write_obj(res, "parts_watertight");
}
#include <libslic3r/QuadricEdgeCollapse.hpp>
static float triangle_area(const Vec3f &v0, const Vec3f &v1, const Vec3f &v2)
{
Vec3f ab = v1 - v0;
Vec3f ac = v2 - v0;
return ab.cross(ac).norm() / 2.f;
}
static float triangle_area(const Vec3crd &triangle_inices, const std::vector<Vec3f> &vertices)
{
return triangle_area(vertices[triangle_inices[0]],
vertices[triangle_inices[1]],
vertices[triangle_inices[2]]);
}
#if 0
// clang complains about unused functions
static std::mt19937 create_random_generator() {
std::random_device rd;
std::mt19937 gen(rd());
return gen;
}
#endif
std::vector<Vec3f> its_sample_surface(const indexed_triangle_set &its,
double sample_per_mm2,
std::mt19937 random_generator) // = create_random_generator())
{
std::vector<Vec3f> samples;
std::uniform_real_distribution<float> rand01(0.f, 1.f);
for (const auto &triangle_indices : its.indices) {
float area = triangle_area(triangle_indices, its.vertices);
float countf;
float fractional = std::modf(area * sample_per_mm2, &countf);
int count = static_cast<int>(countf);
float generate = rand01(random_generator);
if (generate < fractional) ++count;
if (count == 0) continue;
const Vec3f &v0 = its.vertices[triangle_indices[0]];
const Vec3f &v1 = its.vertices[triangle_indices[1]];
const Vec3f &v2 = its.vertices[triangle_indices[2]];
for (int c = 0; c < count; c++) {
// barycentric coordinate
Vec3f b;
b[0] = rand01(random_generator);
b[1] = rand01(random_generator);
if ((b[0] + b[1]) > 1.f) {
b[0] = 1.f - b[0];
b[1] = 1.f - b[1];
}
b[2] = 1.f - b[0] - b[1];
Vec3f pos;
for (int i = 0; i < 3; i++) {
pos[i] = b[0] * v0[i] + b[1] * v1[i] + b[2] * v2[i];
}
samples.push_back(pos);
}
}
return samples;
}
#include "libslic3r/AABBTreeIndirect.hpp"
struct CompareConfig
{
float max_distance = 3.f;
float max_average_distance = 2.f;
};
bool is_similar(const indexed_triangle_set &from,
const indexed_triangle_set &to,
const CompareConfig &cfg)
{
// create ABBTree
auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(
from.vertices, from.indices);
float sum_distance = 0.f;
float max_distance = 0.f;
auto collect_distances = [&](const Vec3f &surface_point) {
size_t hit_idx;
Vec3f hit_point;
float distance2 =
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
from.vertices, from.indices, tree, surface_point, hit_idx, hit_point);
float distance = sqrt(distance2);
if (max_distance < distance) max_distance = distance;
sum_distance += distance;
};
for (const Vec3f &vertex : to.vertices) {
collect_distances(vertex);
}
for (const Vec3i &t : to.indices) {
Vec3f center(0,0,0);
for (size_t i = 0; i < 3; ++i) {
center += to.vertices[t[i]] / 3;
}
collect_distances(center);
}
size_t count = to.vertices.size() + to.indices.size();
float avg_distance = sum_distance / count;
if (avg_distance > cfg.max_average_distance ||
max_distance > cfg.max_distance)
return false;
return true;
}
#include "test_utils.hpp"
TEST_CASE("Simplify mesh by Quadric edge collapse to 5%", "[its]")
{
TriangleMesh mesh = load_model("frog_legs.obj");
double original_volume = its_volume(mesh.its);
uint32_t wanted_count = mesh.its.indices.size() * 0.05;
REQUIRE_FALSE(mesh.empty());
indexed_triangle_set its = mesh.its; // copy
float max_error = std::numeric_limits<float>::max();
its_quadric_edge_collapse(its, wanted_count, &max_error);
//its_write_obj(its, "frog_legs_qec.obj");
CHECK(its.indices.size() <= wanted_count);
double volume = its_volume(its);
CHECK(fabs(original_volume - volume) < 33.);
CompareConfig cfg;
cfg.max_average_distance = 0.043f;
cfg.max_distance = 0.32f;
CHECK(is_similar(mesh.its, its, cfg));
CHECK(is_similar(its, mesh.its, cfg));
}

View File

@@ -0,0 +1,35 @@
#include <catch2/catch.hpp>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/JumpPointSearch.hpp"
using namespace Slic3r;
TEST_CASE("Test jump point search path finding", "[JumpPointSearch]")
{
Lines obstacles{};
obstacles.push_back(Line(Point::new_scale(0, 0), Point::new_scale(50, 50)));
obstacles.push_back(Line(Point::new_scale(0, 100), Point::new_scale(50, 50)));
obstacles.push_back(Line(Point::new_scale(0, 0), Point::new_scale(100, 0)));
obstacles.push_back(Line(Point::new_scale(0, 100), Point::new_scale(100, 100)));
obstacles.push_back(Line(Point::new_scale(25, -25), Point::new_scale(25, 125)));
JPSPathFinder jps;
jps.add_obstacles(obstacles);
Polyline path = jps.find_path(Point::new_scale(5, 50), Point::new_scale(100, 50));
path = jps.find_path(Point::new_scale(5, 50), Point::new_scale(150, 50));
path = jps.find_path(Point::new_scale(5, 50), Point::new_scale(25, 15));
path = jps.find_path(Point::new_scale(25, 25), Point::new_scale(125, 125));
// SECTION("Output is empty when source is also the destination") {
// bool found = astar::search_route(DummyTracer{}, 0, std::back_inserter(out));
// REQUIRE(out.empty());
// REQUIRE(found);
// }
// SECTION("Return false when there is no route to destination") {
// bool found = astar::search_route(DummyTracer{}, 1, std::back_inserter(out));
// REQUIRE(!found);
// REQUIRE(out.empty());
// }
}

View File

@@ -0,0 +1,137 @@
#include <catch2/catch.hpp>
#include "libslic3r/KDTreeIndirect.hpp"
#include "libslic3r/Execution/ExecutionSeq.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/PointGrid.hpp"
using namespace Slic3r;
//template<class G>
//struct Within { // Wrapper for the `within` predicate that counts calls.
// kdtree::Within<G> pred;
// Within(G box): pred{box} {}
// // Number of times the predicate was called
// mutable size_t call_count = 0;
// std::pair<bool, unsigned int> operator() (const Vec3f &p, size_t dim)
// {
// ++call_count;
// return pred(p, dim);
// }
//};
static double volume(const BoundingBox3Base<Vec3f> &box)
{
auto sz = box.size();
return sz.x() * sz.y() * sz.z();
}
TEST_CASE("Test kdtree query for a Box", "[KDTreeIndirect]")
{
auto vol = BoundingBox3Base<Vec3f>{{0.f, 0.f, 0.f}, {10.f, 10.f, 10.f}};
auto pgrid = point_grid(ex_seq, vol, Vec3f{0.1f, 0.1f, 0.1f});
REQUIRE(!pgrid.empty());
auto coordfn = [&pgrid] (size_t i, size_t D) { return pgrid.get(i)(int(D)); };
KDTreeIndirect<3, float, decltype(coordfn)> tree{coordfn, pgrid.point_count()};
std::vector<size_t> out;
auto qbox = BoundingBox3Base{Vec3f{0.f, 0.f, 0.f}, Vec3f{.5f, .5f, .5f}};
size_t call_count = 0;
out = find_nearby_points(tree, qbox.min, qbox.max, [&call_count](size_t) {
call_count++;
return true;
});
// Output shall be non-empty
REQUIRE(!out.empty());
std::sort(out.begin(), out.end());
// No duplicates allowed in the output
auto it = std::unique(out.begin(), out.end());
REQUIRE(it == out.end());
// Test if inside points are in the output and outside points are not.
bool succ = true;
for (size_t i = 0; i < pgrid.point_count(); ++i) {
auto foundit = std::find(out.begin(), out.end(), i);
bool contains = qbox.contains(pgrid.get(i));
succ = succ && contains ? foundit != out.end() : foundit == out.end();
if (!succ) {
std::cout << "invalid point: " << i << " " << pgrid.get(i).transpose()
<< std::endl;
break;
}
}
REQUIRE(succ);
// Test for the expected cost of the query.
double gridvolume = volume(vol);
double queryvolume = volume(qbox);
double volratio = (queryvolume / gridvolume);
REQUIRE(call_count < 3 * volratio * pgrid.point_count());
REQUIRE(call_count < pgrid.point_count());
}
//TEST_CASE("Test kdtree query for a Sphere", "[KDTreeIndirect]") {
// auto vol = BoundingBox3Base<Vec3f>{{0.f, 0.f, 0.f}, {10.f, 10.f, 10.f}};
// auto pgrid = point_grid(ex_seq, vol, Vec3f{0.1f, 0.1f, 0.1f});
// REQUIRE(!pgrid.empty());
// auto coordfn = [&pgrid] (size_t i, size_t D) { return pgrid.get(i)(int(D)); };
// kdtree::KDTreeIndirect<3, float, decltype(coordfn)> tree{coordfn, pgrid.point_count()};
// std::vector<size_t> out;
// auto querysphere = kdtree::Sphere{Vec3f{5.f, 5.f, 5.f}, 2.f};
// auto pred = Within(querysphere);
// kdtree::query(tree, pred, std::back_inserter(out));
// // Output shall be non-empty
// REQUIRE(!out.empty());
// std::sort(out.begin(), out.end());
// // No duplicates allowed in the output
// auto it = std::unique(out.begin(), out.end());
// REQUIRE(it == out.end());
// // Test if inside points are in the output and outside points are not.
// bool succ = true;
// for (size_t i = 0; i < pgrid.point_count(); ++i) {
// auto foundit = std::find(out.begin(), out.end(), i);
// bool contains = (querysphere.center - pgrid.get(i)).squaredNorm() < pred.pred.r2;
// succ = succ && contains ? foundit != out.end() : foundit == out.end();
// if (!succ) {
// std::cout << "invalid point: " << i << " " << pgrid.get(i).transpose()
// << std::endl;
// break;
// }
// }
// REQUIRE(succ);
// // Test for the expected cost of the query.
// double gridvolume = volume(vol);
// double queryvolume = volume(querysphere);
// double volratio = (queryvolume / gridvolume);
// REQUIRE(pred.call_count < 3 * volratio * pgrid.point_count());
// REQUIRE(pred.call_count < pgrid.point_count());
//}

View File

@@ -0,0 +1,376 @@
#define NOMINMAX
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <fstream>
#include <libslic3r/MarchingSquares.hpp>
#include <libslic3r/SLA/RasterToPolygons.hpp>
#include <libslic3r/SLA/AGGRaster.hpp>
#include <libslic3r/MTUtils.hpp>
#include <libslic3r/SVG.hpp>
#include <libslic3r/ClipperUtils.hpp>
#include <libslic3r/TriangleMeshSlicer.hpp>
#include <libslic3r/TriangulateWall.hpp>
#include <libslic3r/Tesselate.hpp>
#include <libslic3r/SlicesToTriangleMesh.hpp>
using namespace Slic3r;
static double area(const sla::PixelDim &pxd)
{
return pxd.w_mm * pxd.h_mm;
}
static Slic3r::sla::RasterGrayscaleAA create_raster(
const sla::Resolution &res,
double disp_w = 100.,
double disp_h = 100.)
{
sla::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
sla::RasterBase::Trafo trafo;
trafo.center_x = bb.center().x();
trafo.center_y = bb.center().y();
return sla::RasterGrayscaleAA{res, pixdim, trafo, agg::gamma_threshold(.5)};
}
static ExPolygon square(double a, Point center = {0, 0})
{
ExPolygon poly;
coord_t V = scaled(a / 2.);
poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}};
poly.translate(center.x(), center.y());
return poly;
}
static ExPolygon square_with_hole(double a, Point center = {0, 0})
{
ExPolygon poly = square(a);
poly.holes.emplace_back();
coord_t V = scaled(a / 4.);
poly.holes.front().points = {{-V, V}, {V, V}, {V, -V}, {-V, -V}};
poly.translate(center.x(), center.y());
return poly;
}
static ExPolygons circle_with_hole(double r, Point center = {0, 0}) {
ExPolygon poly;
std::vector<double> pis = linspace_vector(0., 2 * PI, 100);
coord_t rs = scaled(r);
for (double phi : pis) {
poly.contour.points.emplace_back(rs * std::cos(phi), rs * std::sin(phi));
}
poly.holes.emplace_back(poly.contour);
poly.holes.front().reverse();
for (auto &p : poly.holes.front().points) p /= 2;
poly.translate(center.x(), center.y());
return {poly};
}
static const Vec2i W4x4 = {4, 4};
static const Vec2i W2x2 = {2, 2};
template<class Rst>
static void test_expolys(Rst && rst,
const ExPolygons & ref,
Vec2i window,
const std::string &name = "test")
{
for (const ExPolygon &expoly : ref) rst.draw(expoly);
std::fstream out(name + ".png", std::ios::out);
out << rst.encode(sla::PNGRasterEncoder{});
out.close();
ExPolygons extracted = sla::raster_to_polygons(rst, window);
SVG svg(name + ".svg");
svg.draw(extracted);
svg.draw(ref, "green");
svg.Close();
double max_rel_err = 0.1;
sla::PixelDim pxd = rst.pixel_dimensions();
double max_abs_err = area(pxd) * scaled(1.) * scaled(1.);
BoundingBox ref_bb;
for (auto &expoly : ref) ref_bb.merge(expoly.contour.bounding_box());
double max_displacement = 4. * (std::pow(pxd.h_mm, 2) + std::pow(pxd.w_mm, 2));
max_displacement *= scaled<double>(1.) * scaled(1.);
REQUIRE(extracted.size() == ref.size());
for (size_t i = 0; i < ref.size(); ++i) {
REQUIRE(extracted[i].contour.is_counter_clockwise());
REQUIRE(extracted[i].holes.size() == ref[i].holes.size());
for (auto &h : extracted[i].holes) REQUIRE(h.is_clockwise());
double refa = ref[i].area();
double abs_err = std::abs(extracted[i].area() - refa);
double rel_err = abs_err / refa;
REQUIRE((rel_err <= max_rel_err || abs_err <= max_abs_err));
BoundingBox bb;
for (auto &expoly : extracted) bb.merge(expoly.contour.bounding_box());
Point d = bb.center() - ref_bb.center();
REQUIRE(double(d.transpose() * d) <= max_displacement);
}
}
TEST_CASE("Empty raster should result in empty polygons", "[MarchingSquares]") {
sla::RasterGrayscaleAAGammaPower rst{{}, {}, {}};
ExPolygons extracted = sla::raster_to_polygons(rst);
REQUIRE(extracted.size() == 0);
}
TEST_CASE("Marching squares directions", "[MarchingSquares]") {
marchsq::Coord crd{1, 1};
REQUIRE(step(crd, marchsq::__impl::Dir::left).r == 1);
REQUIRE(step(crd, marchsq::__impl::Dir::left).c == 0);
REQUIRE(step(crd, marchsq::__impl::Dir::down).r == 2);
REQUIRE(step(crd, marchsq::__impl::Dir::down).c == 1);
REQUIRE(step(crd, marchsq::__impl::Dir::right).r == 1);
REQUIRE(step(crd, marchsq::__impl::Dir::right).c == 2);
REQUIRE(step(crd, marchsq::__impl::Dir::up).r == 0);
REQUIRE(step(crd, marchsq::__impl::Dir::up).c == 1);
}
TEST_CASE("Fully covered raster should result in a rectangle", "[MarchingSquares]") {
auto rst = create_raster({4, 4}, 4., 4.);
ExPolygon rect = square(4);
SECTION("Full accuracy") {
test_expolys(rst, {rect}, W2x2, "fully_covered_full_acc");
}
SECTION("Half accuracy") {
test_expolys(rst, {rect}, W4x4, "fully_covered_half_acc");
}
}
TEST_CASE("4x4 raster with one ring", "[MarchingSquares]") {
sla::PixelDim pixdim{1, 1};
// We need one additional row and column to detect edges
sla::RasterGrayscaleAA rst{{4, 4}, pixdim, {}, agg::gamma_threshold(.5)};
// Draw a triangle from individual pixels
rst.draw(square(1., {0500000, 0500000}));
rst.draw(square(1., {1500000, 0500000}));
rst.draw(square(1., {2500000, 0500000}));
rst.draw(square(1., {1500000, 1500000}));
rst.draw(square(1., {2500000, 1500000}));
rst.draw(square(1., {2500000, 2500000}));
std::fstream out("4x4.png", std::ios::out);
out << rst.encode(sla::PNGRasterEncoder{});
out.close();
ExPolygons extracted = sla::raster_to_polygons(rst);
SVG svg("4x4.svg");
svg.draw(extracted);
svg.Close();
REQUIRE(extracted.size() == 1);
}
TEST_CASE("4x4 raster with two rings", "[MarchingSquares]") {
sla::PixelDim pixdim{1, 1};
// We need one additional row and column to detect edges
sla::RasterGrayscaleAA rst{{5, 5}, pixdim, {}, agg::gamma_threshold(.5)};
SECTION("Ambiguous case with 'ac' square") {
// Draw a triangle from individual pixels
rst.draw(square(1., {3500000, 2500000}));
rst.draw(square(1., {3500000, 3500000}));
rst.draw(square(1., {2500000, 3500000}));
rst.draw(square(1., {2500000, 1500000}));
rst.draw(square(1., {1500000, 1500000}));
rst.draw(square(1., {1500000, 2500000}));
std::fstream out("4x4_ac.png", std::ios::out);
out << rst.encode(sla::PNGRasterEncoder{});
out.close();
ExPolygons extracted = sla::raster_to_polygons(rst);
SVG svg("4x4_ac.svg");
svg.draw(extracted);
svg.Close();
REQUIRE(extracted.size() == 2);
}
SECTION("Ambiguous case with 'bd' square") {
// Draw a triangle from individual pixels
rst.draw(square(1., {3500000, 1500000}));
rst.draw(square(1., {3500000, 2500000}));
rst.draw(square(1., {2500000, 1500000}));
rst.draw(square(1., {1500000, 2500000}));
rst.draw(square(1., {1500000, 3500000}));
rst.draw(square(1., {2500000, 3500000}));
std::fstream out("4x4_bd.png", std::ios::out);
out << rst.encode(sla::PNGRasterEncoder{});
out.close();
ExPolygons extracted = sla::raster_to_polygons(rst);
SVG svg("4x4_bd.svg");
svg.draw(extracted);
svg.Close();
REQUIRE(extracted.size() == 2);
}
}
TEST_CASE("Square with hole in the middle", "[MarchingSquares]") {
using namespace Slic3r;
ExPolygons inp = {square_with_hole(50.)};
SECTION("Proportional raster, 1x1 mm pixel size, full accuracy") {
test_expolys(create_raster({100, 100}, 100., 100.), inp, W2x2, "square_with_hole_proportional_1x1_mm_px_full");
}
SECTION("Proportional raster, 1x1 mm pixel size, half accuracy") {
test_expolys(create_raster({100, 100}, 100., 100.), inp, W4x4, "square_with_hole_proportional_1x1_mm_px_half");
}
SECTION("Landscape raster, 1x1 mm pixel size, full accuracy") {
test_expolys(create_raster({150, 100}, 150., 100.), inp, W2x2, "square_with_hole_landsc_1x1_mm_px_full");
}
SECTION("Landscape raster, 1x1 mm pixel size, half accuracy") {
test_expolys(create_raster({150, 100}, 150., 100.), inp, W4x4, "square_with_hole_landsc_1x1_mm_px_half");
}
SECTION("Portrait raster, 1x1 mm pixel size, full accuracy") {
test_expolys(create_raster({100, 150}, 100., 150.), inp, W2x2, "square_with_hole_portrait_1x1_mm_px_full");
}
SECTION("Portrait raster, 1x1 mm pixel size, half accuracy") {
test_expolys(create_raster({100, 150}, 100., 150.), inp, W4x4, "square_with_hole_portrait_1x1_mm_px_half");
}
SECTION("Proportional raster, 2x2 mm pixel size, full accuracy") {
test_expolys(create_raster({200, 200}, 100., 100.), inp, W2x2, "square_with_hole_proportional_2x2_mm_px_full");
}
SECTION("Proportional raster, 2x2 mm pixel size, half accuracy") {
test_expolys(create_raster({200, 200}, 100., 100.), inp, W4x4, "square_with_hole_proportional_2x2_mm_px_half");
}
SECTION("Proportional raster, 0.5x0.5 mm pixel size, full accuracy") {
test_expolys(create_raster({50, 50}, 100., 100.), inp, W2x2, "square_with_hole_proportional_0.5x0.5_mm_px_full");
}
SECTION("Proportional raster, 0.5x0.5 mm pixel size, half accuracy") {
test_expolys(create_raster({50, 50}, 100., 100.), inp, W4x4, "square_with_hole_proportional_0.5x0.5_mm_px_half");
}
}
TEST_CASE("Circle with hole in the middle", "[MarchingSquares]") {
using namespace Slic3r;
test_expolys(create_raster({1000, 1000}), circle_with_hole(25.), W2x2, "circle_with_hole");
}
static void recreate_object_from_rasters(const std::string &objname, float lh) {
TriangleMesh mesh = load_model(objname);
auto bb = mesh.bounding_box();
Vec3f tr = -bb.center().cast<float>();
mesh.translate(tr.x(), tr.y(), tr.z());
bb = mesh.bounding_box();
std::vector<ExPolygons> layers = slice_mesh_ex(mesh.its, grid(float(bb.min.z()) + lh, float(bb.max.z()), lh));
sla::Resolution res{2560, 1440};
double disp_w = 120.96;
double disp_h = 68.04;
//#ifndef NDEBUG
// size_t cntr = 0;
//#endif
for (ExPolygons &layer : layers) {
auto rst = create_raster(res, disp_w, disp_h);
for (ExPolygon &island : layer) {
rst.draw(island);
}
//#ifndef NDEBUG
// std::fstream out(objname + std::to_string(cntr) + ".png", std::ios::out);
// out << rst.encode(sla::PNGRasterEncoder{});
// out.close();
//#endif
ExPolygons layer_ = sla::raster_to_polygons(rst);
// float delta = scaled(std::min(rst.pixel_dimensions().h_mm,
// rst.pixel_dimensions().w_mm)) / 2;
// layer_ = expolygons_simplify(layer_, delta);
//#ifndef NDEBUG
// SVG svg(objname + std::to_string(cntr) + ".svg", BoundingBox(Point{0, 0}, Point{scaled(disp_w), scaled(disp_h)}));
// svg.draw(layer_);
// svg.draw(layer, "green");
// svg.Close();
//#endif
double layera = 0., layera_ = 0.;
for (auto &p : layer) layera += p.area();
for (auto &p : layer_) layera_ += p.area();
//#ifndef NDEBUG
// std::cout << cntr++ << std::endl;
//#endif
double diff = std::abs(layera_ - layera);
REQUIRE((diff <= 0.1 * layera || diff < scaled<double>(1.) * scaled<double>(1.)));
layer = std::move(layer_);
}
indexed_triangle_set out = slices_to_mesh(layers, bb.min.z(), double(lh), double(lh));
its_write_obj(out, "out_from_rasters.obj");
}
TEST_CASE("Recreate object from rasters", "[SL1Import]") {
recreate_object_from_rasters("frog_legs.obj", 0.05f);
}

View File

@@ -0,0 +1,51 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/MeshBoolean.hpp>
using namespace Slic3r;
TEST_CASE("CGAL and TriangleMesh conversions", "[MeshBoolean]") {
TriangleMesh sphere = make_sphere(1.);
auto cgalmesh_ptr = MeshBoolean::cgal::triangle_mesh_to_cgal(sphere);
REQUIRE(cgalmesh_ptr);
REQUIRE(! MeshBoolean::cgal::does_self_intersect(*cgalmesh_ptr));
TriangleMesh M = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalmesh_ptr);
REQUIRE(M.its.vertices.size() == sphere.its.vertices.size());
REQUIRE(M.its.indices.size() == sphere.its.indices.size());
REQUIRE(M.volume() == Approx(sphere.volume()));
REQUIRE(! MeshBoolean::cgal::does_self_intersect(M));
}
Vec3d calc_normal(const Vec3i &triangle, const std::vector<Vec3f> &vertices)
{
Vec3d v0 = vertices[triangle[0]].cast<double>();
Vec3d v1 = vertices[triangle[1]].cast<double>();
Vec3d v2 = vertices[triangle[2]].cast<double>();
// n = triangle normal
Vec3d n = (v1 - v0).cross(v2 - v0);
n.normalize();
return n;
}
TEST_CASE("Add TriangleMeshes", "[MeshBoolean]")
{
TriangleMesh tm1 = make_sphere(1.6, 1.6);
size_t init_size = tm1.its.indices.size();
Vec3f move(5, -3, 7);
move.normalize();
tm1.translate(0.3 * move);
//its_write_obj(tm1.its, "tm1.obj");
TriangleMesh tm2 = make_cube(1., 1., 1.);
//its_write_obj(tm2.its, "tm2.obj");
MeshBoolean::cgal::plus(tm1, tm2);
//its_write_obj(tm1.its, "test_add.obj");
CHECK(tm1.its.indices.size() > init_size);
}

View File

@@ -0,0 +1,177 @@
#include <catch2/catch.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/MutablePolygon.hpp"
using namespace Slic3r;
SCENARIO("Iterators", "[MutablePolygon]") {
GIVEN("Polygon with three points") {
Slic3r::MutablePolygon p({ { 0, 0 }, { 0, 1 }, { 1, 0 } });
WHEN("Iterating upwards") {
auto begin = p.begin();
auto end = p.end();
auto it = begin;
THEN("++ it is not equal to begin") {
REQUIRE(++ it != begin);
} THEN("++ it is not equal to end") {
REQUIRE(++ it != end);
} THEN("++ (++ it) is not equal to begin") {
REQUIRE(++ (++ it) != begin);
} THEN("++ (++ it) is equal to end") {
REQUIRE(++ (++ it) == end);
} THEN("++ (++ (++ it)) is equal to begin") {
REQUIRE(++ (++ (++ it)) == begin);
} THEN("++ (++ (++ it)) is not equal to end") {
REQUIRE(++ (++ (++ it)) != end);
}
}
WHEN("Iterating downwards") {
auto begin = p.begin();
auto end = p.end();
auto it = begin;
THEN("-- it is not equal to begin") {
REQUIRE(-- it != begin);
} THEN("-- it is equal to end") {
REQUIRE(-- it == end);
} THEN("-- (-- it) is not equal to begin") {
REQUIRE(-- (-- it) != begin);
} THEN("-- (-- it) is not equal to end") {
REQUIRE(-- (-- it) != end);
} THEN("-- (-- (-- it)) is equal to begin") {
REQUIRE(-- (-- (-- it)) == begin);
} THEN("-- (-- (-- it)) is not equal to end") {
REQUIRE(-- (-- (-- it)) != end);
}
}
WHEN("Deleting 1st point") {
auto it_2nd = p.begin().next();
auto it = p.begin().remove();
THEN("Size is 2") {
REQUIRE(p.size() == 2);
} THEN("p.begin().remove() == it_2nd") {
REQUIRE(it == it_2nd);
} THEN("it_2nd == new begin()") {
REQUIRE(it_2nd == p.begin());
}
}
WHEN("Deleting 2nd point") {
auto it_1st = p.begin();
auto it_2nd = it_1st.next();
auto it = it_2nd.remove();
THEN("Size is 2") {
REQUIRE(p.size() == 2);
REQUIRE(! p.empty());
} THEN("it_2nd.remove() == it_3rd") {
REQUIRE(it == it_2nd);
} THEN("it_1st == new begin()") {
REQUIRE(it_1st == p.begin());
}
}
WHEN("Deleting two points") {
p.begin().remove().remove();
THEN("Size is 1") {
REQUIRE(p.size() == 1);
} THEN("p.begin().next() == p.begin()") {
REQUIRE(p.begin().next() == p.begin());
} THEN("p.begin().prev() == p.begin()") {
REQUIRE(p.begin().prev() == p.begin());
}
}
WHEN("Deleting all points") {
auto it = p.begin().remove().remove().remove();
THEN("Size is 0") {
REQUIRE(p.size() == 0);
REQUIRE(p.empty());
} THEN("! p.begin().valid()") {
REQUIRE(!p.begin().valid());
} THEN("last iterator not valid") {
REQUIRE(! it.valid());
}
}
WHEN("Inserting a point at the beginning") {
p.insert(p.begin(), { 3, 4 });
THEN("Polygon content is ok") {
REQUIRE(p == MutablePolygon{ { 0, 0 }, { 0, 1 }, { 1, 0 }, { 3, 4 } });
}
}
WHEN("Inserting a point at the 2nd position") {
p.insert(++ p.begin(), { 3, 4 });
THEN("Polygon content is ok") {
REQUIRE(p == MutablePolygon{ { 0, 0 }, { 3, 4 }, { 0, 1 }, { 1, 0 } });
}
} WHEN("Inserting a point after a point was removed") {
size_t capacity = p.capacity();
THEN("Initial capacity is 3") {
REQUIRE(capacity == 3);
}
p.begin().remove();
THEN("After removal of the 1st point the capacity is still 3") {
REQUIRE(p.capacity() == 3);
}
THEN("After removal of the 1st point the content is ok") {
REQUIRE(p == MutablePolygon{ { 0, 1 }, { 1, 0 } });
}
p.insert(p.begin(), { 5, 6 });
THEN("After insertion at head position the polygon content is ok") {
REQUIRE(p == MutablePolygon{ { 0, 1 }, { 1, 0 }, { 5, 6 } });
} THEN("and the capacity is still 3") {
REQUIRE(p.capacity() == 3);
}
}
}
}
SCENARIO("Remove degenerate points from MutablePolygon", "[MutablePolygon]") {
GIVEN("Polygon with duplicate points"){
Slic3r::MutablePolygon p({
{ 0, 0 },
{ 0, 100 }, { 0, 100 }, { 0, 100 },
{ 0, 150 },
{ 0, 200 },
{ 200, 200 },
{ 180, 200 }, { 180, 200 },
{ 180, 20 },
{ 180, 0 },
});
WHEN("Duplicate points are removed") {
remove_duplicates(p);
THEN("Polygon content is ok") {
REQUIRE(p == Slic3r::MutablePolygon{ { 0, 0 }, { 0, 100 }, { 0, 150 }, { 0, 200 }, { 200, 200 }, { 180, 200 }, { 180, 20 }, { 180, 0 } });
}
}
}
}
SCENARIO("smooth_outward", "[MutablePolygon]") {
GIVEN("Convex polygon") {
MutablePolygon p{ { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } };
WHEN("smooth_outward") {
MutablePolygon p2{ p };
smooth_outward(p2, scaled<double>(10.));
THEN("Polygon is unmodified") {
REQUIRE(p == p2);
}
}
}
GIVEN("Sharp tiny concave polygon (hole)") {
MutablePolygon p{ { 0, 0 }, { 0, scaled<coord_t>(5.) }, { scaled<coord_t>(10.), 0 } };
WHEN("smooth_outward") {
MutablePolygon p2{ p };
smooth_outward(p2, scaled<double>(10.));
THEN("Hole is closed") {
REQUIRE(p2.empty());
}
}
}
GIVEN("Two polygons") {
Polygons p{ { { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } },
{ { 0, 0 }, { 0, scaled<coord_t>(5.) }, { scaled<coord_t>(10.), 0 } } };
WHEN("smooth_outward") {
p = smooth_outward(p, scaled<double>(10.));
THEN("CCW contour unmodified, CW contour removed.") {
REQUIRE(p == Polygons{ { { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } } });
}
}
}
}

View File

@@ -0,0 +1,461 @@
#include <catch2/catch.hpp>
#include <queue>
#include "libslic3r/MutablePriorityQueue.hpp"
using namespace Slic3r;
// based on https://raw.githubusercontent.com/rollbear/prio_queue/master/self_test.cpp
// original source Copyright Björn Fahller 2015, Boost Software License, Version 1.0, http://www.boost.org/LICENSE_1_0.txt
TEST_CASE("Skip addressing", "[MutableSkipHeapPriorityQueue]") {
using skip_addressing = SkipHeapAddressing<8>;
SECTION("block root") {
REQUIRE(skip_addressing::is_block_root(1));
REQUIRE(skip_addressing::is_block_root(9));
REQUIRE(skip_addressing::is_block_root(17));
REQUIRE(skip_addressing::is_block_root(73));
REQUIRE(! skip_addressing::is_block_root(2));
REQUIRE(! skip_addressing::is_block_root(3));
REQUIRE(! skip_addressing::is_block_root(4));
REQUIRE(! skip_addressing::is_block_root(7));
REQUIRE(! skip_addressing::is_block_root(31));
}
SECTION("block leaf") {
REQUIRE(! skip_addressing::is_block_leaf(1));
REQUIRE(! skip_addressing::is_block_leaf(2));
REQUIRE(! skip_addressing::is_block_leaf(3));
REQUIRE(skip_addressing::is_block_leaf(4));
REQUIRE(skip_addressing::is_block_leaf(5));
REQUIRE(skip_addressing::is_block_leaf(6));
REQUIRE(skip_addressing::is_block_leaf(7));
REQUIRE(skip_addressing::is_block_leaf(28));
REQUIRE(skip_addressing::is_block_leaf(29));
REQUIRE(skip_addressing::is_block_leaf(30));
REQUIRE(! skip_addressing::is_block_leaf(257));
REQUIRE(skip_addressing::is_block_leaf(255));
}
SECTION("Obtaining child") {
REQUIRE(skip_addressing::child_of(1) == 2);
REQUIRE(skip_addressing::child_of(2) == 4);
REQUIRE(skip_addressing::child_of(3) == 6);
REQUIRE(skip_addressing::child_of(4) == 9);
REQUIRE(skip_addressing::child_of(31) == 249);
}
SECTION("Obtaining parent") {
REQUIRE(skip_addressing::parent_of(2) == 1);
REQUIRE(skip_addressing::parent_of(3) == 1);
REQUIRE(skip_addressing::parent_of(6) == 3);
REQUIRE(skip_addressing::parent_of(7) == 3);
REQUIRE(skip_addressing::parent_of(9) == 4);
REQUIRE(skip_addressing::parent_of(17) == 4);
REQUIRE(skip_addressing::parent_of(33) == 5);
REQUIRE(skip_addressing::parent_of(29) == 26);
REQUIRE(skip_addressing::parent_of(1097) == 140);
}
}
struct ValueIndexPair
{
int value;
size_t idx = 0;
};
template<size_t block_size = 16>
static auto make_test_priority_queue()
{
return make_miniheap_mutable_priority_queue<ValueIndexPair, block_size, false>(
[](ValueIndexPair &v, size_t idx){ v.idx = idx; },
[](ValueIndexPair &l, ValueIndexPair &r){ return l.value < r.value; });
}
TEST_CASE("Mutable priority queue - basic tests", "[MutableSkipHeapPriorityQueue]") {
SECTION("a default constructed queue is empty") {
auto q = make_test_priority_queue();
REQUIRE(q.empty());
REQUIRE(q.size() == 0);
}
SECTION("an empty queue is not empty when one element is inserted") {
auto q = make_test_priority_queue();
q.push({ 1 });
REQUIRE(!q.empty());
REQUIRE(q.size() == 1);
}
SECTION("a queue with one element has it on top") {
auto q = make_test_priority_queue();
q.push({ 8 });
REQUIRE(q.top().value == 8);
}
SECTION("a queue with one element becomes empty when popped") {
auto q = make_test_priority_queue();
q.push({ 9 });
q.pop();
REQUIRE(q.empty());
REQUIRE(q.size() == 0);
}
SECTION("insert sorted stays sorted") {
auto q = make_test_priority_queue();
for (auto i : { 1, 2, 3, 4, 5, 6, 7, 8 })
q.push({ i });
REQUIRE(q.top().value == 1);
q.pop();
REQUIRE(q.top().value == 2);
q.pop();
REQUIRE(q.top().value == 3);
q.pop();
REQUIRE(q.top().value == 4);
q.pop();
REQUIRE(q.top().value == 5);
q.pop();
REQUIRE(q.top().value == 6);
q.pop();
REQUIRE(q.top().value == 7);
q.pop();
REQUIRE(q.top().value == 8);
q.pop();
REQUIRE(q.empty());
}
SECTION("randomly inserted elements are popped sorted") {
auto q = make_test_priority_queue();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(1, 100000);
int n[36000];
for (auto& i : n) {
i = dist(gen);
q.push({ i });
}
REQUIRE(!q.empty());
REQUIRE(q.size() == 36000);
std::sort(std::begin(n), std::end(n));
for (auto i : n) {
REQUIRE(q.top().value == i);
q.pop();
}
REQUIRE(q.empty());
}
}
TEST_CASE("Mutable priority queue - reshedule first", "[MutableSkipHeapPriorityQueue]") {
struct MyValue {
int value;
int *ptr;
size_t idx;
};
SECTION("reschedule top with highest prio leaves order unchanged") {
auto q = make_miniheap_mutable_priority_queue<MyValue, 4, false>(
[](MyValue& v, size_t idx) { v.idx = idx; },
[](MyValue& l, MyValue& r) { return l.value < r.value; });
// 0 1 2 3 4 5 6 7 8
int nums[] = { 32, 1, 88, 16, 9, 11, 3, 22, 23 };
for (auto &i : nums)
q.push({ i, &i, 0U });
REQUIRE(q.top().value == 1);
REQUIRE(q.top().ptr == &nums[1]);
REQUIRE(*q.top().ptr == 1);
// Update the top element.
q.top().value = 2;
q.update(1);
REQUIRE(q.top().value == 2);
REQUIRE(q.top().ptr == &nums[1]);
q.pop();
REQUIRE(q.top().value == 3);
REQUIRE(q.top().ptr == &nums[6]);
q.pop();
REQUIRE(q.top().value == 9);
REQUIRE(q.top().ptr == &nums[4]);
q.pop();
REQUIRE(q.top().value == 11);
REQUIRE(q.top().ptr == &nums[5]);
q.pop();
REQUIRE(q.top().value == 16);
REQUIRE(q.top().ptr == &nums[3]);
q.pop();
REQUIRE(q.top().value == 22);
REQUIRE(q.top().ptr == &nums[7]);
q.pop();
REQUIRE(q.top().value == 23);
REQUIRE(q.top().ptr == &nums[8]);
q.pop();
REQUIRE(q.top().value == 32);
REQUIRE(q.top().ptr == &nums[0]);
q.pop();
REQUIRE(q.top().value == 88);
REQUIRE(q.top().ptr == &nums[2]);
q.pop();
REQUIRE(q.empty());
}
SECTION("reschedule to mid range moves element to correct place") {
auto q = make_miniheap_mutable_priority_queue<MyValue, 4, false>(
[](MyValue& v, size_t idx) { v.idx = idx; },
[](MyValue& l, MyValue& r) { return l.value < r.value; });
// 0 1 2 3 4 5 6 7 8
int nums[] = { 32, 1, 88, 16, 9, 11, 3, 22, 23 };
for (auto& i : nums)
q.push({ i, &i, 0U });
REQUIRE(q.top().value == 1);
REQUIRE(q.top().ptr == &nums[1]);
REQUIRE(*q.top().ptr == 1);
// Update the top element.
q.top().value = 12;
q.update(1);
REQUIRE(q.top().value == 3);
REQUIRE(q.top().ptr == &nums[6]);
q.pop();
REQUIRE(q.top().value == 9);
REQUIRE(q.top().ptr == &nums[4]);
q.pop();
REQUIRE(q.top().value == 11);
REQUIRE(q.top().ptr == &nums[5]);
q.pop();
REQUIRE(q.top().value == 12);
REQUIRE(q.top().ptr == &nums[1]);
q.pop();
REQUIRE(q.top().value == 16);
REQUIRE(q.top().ptr == &nums[3]);
q.pop();
REQUIRE(q.top().value == 22);
REQUIRE(q.top().ptr == &nums[7]);
q.pop();
REQUIRE(q.top().value == 23);
REQUIRE(q.top().ptr == &nums[8]);
q.pop();
REQUIRE(q.top().value == 32);
REQUIRE(q.top().ptr == &nums[0]);
q.pop();
REQUIRE(q.top().value == 88);
REQUIRE(q.top().ptr == &nums[2]);
q.pop();
REQUIRE(q.empty());
}
SECTION("reschedule to last moves element to correct place", "heap")
{
auto q = make_miniheap_mutable_priority_queue<MyValue, 4, false>(
[](MyValue& v, size_t idx) { v.idx = idx; },
[](MyValue& l, MyValue& r) { return l.value < r.value; });
// 0 1 2 3 4 5 6 7 8
int nums[] = { 32, 1, 88, 16, 9, 11, 3, 22, 23 };
for (auto& i : nums)
q.push({ i, &i, 0U });
REQUIRE(q.top().value == 1);
REQUIRE(q.top().ptr == &nums[1]);
REQUIRE(*q.top().ptr == 1);
// Update the top element.
q.top().value = 89;
q.update(1);
REQUIRE(q.top().value == 3);
REQUIRE(q.top().ptr == &nums[6]);
q.pop();
REQUIRE(q.top().value == 9);
REQUIRE(q.top().ptr == &nums[4]);
q.pop();
REQUIRE(q.top().value == 11);
REQUIRE(q.top().ptr == &nums[5]);
q.pop();
REQUIRE(q.top().value == 16);
REQUIRE(q.top().ptr == &nums[3]);
q.pop();
REQUIRE(q.top().value == 22);
REQUIRE(q.top().ptr == &nums[7]);
q.pop();
REQUIRE(q.top().value == 23);
REQUIRE(q.top().ptr == &nums[8]);
q.pop();
REQUIRE(q.top().value == 32);
REQUIRE(q.top().ptr == &nums[0]);
q.pop();
REQUIRE(q.top().value == 88);
REQUIRE(q.top().ptr == &nums[2]);
q.pop();
REQUIRE(q.top().value == 89);
REQUIRE(q.top().ptr == &nums[1]);
q.pop();
REQUIRE(q.empty());
}
SECTION("reschedule top of 2 elements to last") {
auto q = make_test_priority_queue<8>();
q.push({ 1 });
q.push({ 2 });
REQUIRE(q.top().value == 1);
// Update the top element.
q.top().value = 3;
q.update(1);
REQUIRE(q.top().value == 2);
}
SECTION("reschedule top of 3 elements left to 2nd") {
auto q = make_test_priority_queue<8>();
q.push({ 1 });
q.push({ 2 });
q.push({ 4 });
REQUIRE(q.top().value == 1);
// Update the top element.
q.top().value = 3;
q.update(1);
REQUIRE(q.top().value == 2);
}
SECTION("reschedule top of 3 elements right to 2nd") {
auto q = make_test_priority_queue<8>();
q.push({ 1 });
q.push({ 4 });
q.push({ 2 });
REQUIRE(q.top().value == 1);
// Update the top element.
q.top().value = 3;
q.update(1);
REQUIRE(q.top().value == 2);
}
SECTION("reschedule top random gives same resultas pop/push") {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<unsigned> dist(1, 100000);
auto pq = make_test_priority_queue<8>();
std::priority_queue<int, std::vector<int>, std::greater<>> stdq;
for (size_t outer = 0; outer < 100; ++ outer) {
int num = gen();
pq.push({ num });
stdq.push({ num });
for (size_t inner = 0; inner < 100; ++ inner) {
int newval = gen();
// Update the top element.
pq.top().value = newval;
pq.update(1);
stdq.pop();
stdq.push({ newval });
auto n = pq.top().value;
auto sn = stdq.top();
REQUIRE(sn == n);
}
}
}
}
TEST_CASE("Mutable priority queue - first pop", "[MutableSkipHeapPriorityQueue]")
{
struct MyValue{
size_t id;
float val;
};
static constexpr const size_t count = 50000;
std::vector<size_t> idxs(count, {0});
auto q = make_miniheap_mutable_priority_queue<MyValue, 16, true>(
[&idxs](MyValue &v, size_t idx) { idxs[v.id] = idx; },
[](MyValue &l, MyValue &r) { return l.val < r.val; });
using QueueType = decltype(q);
THEN("Skip queue has 0th element unused, 1st element is the top of the queue.") {
CHECK(QueueType::address::is_padding(0));
CHECK(!QueueType::address::is_padding(1));
}
q.reserve(count);
for (size_t id = 0; id < count; ++ id)
q.push({ id, rand() / 100.f });
MyValue v = q.top(); // copy
THEN("Element at the top of the queue has a valid ID.") {
CHECK(v.id >= 0);
CHECK(v.id < count);
}
THEN("Element at the top of the queue has its position stored in idxs.") {
CHECK(idxs[v.id] == 1);
}
q.pop();
THEN("Element removed from the queue has its position in idxs reset to invalid.") {
CHECK(idxs[v.id] == q.invalid_id());
}
THEN("Element was removed from the queue, new top of the queue has its index set correctly.") {
CHECK(q.top().id >= 0);
CHECK(q.top().id < count);
CHECK(idxs[q.top().id] == 1);
}
}
TEST_CASE("Mutable priority queue complex", "[MutableSkipHeapPriorityQueue]")
{
struct MyValue {
size_t id;
float val;
};
size_t count = 5000;
std::vector<size_t> idxs(count, {0});
std::vector<bool> dels(count, false);
auto q = make_miniheap_mutable_priority_queue<MyValue, 16, true>(
[&](MyValue &v, size_t idx) { idxs[v.id] = idx; },
[](MyValue &l, MyValue &r) { return l.val < r.val; });
q.reserve(count);
auto rand_val = [&]()->float { return (rand() % 53) / 10.f; };
for (size_t id = 0; id < count; ++ id)
q.push({ id, rand_val() });
auto check = [&]()->bool{
for (size_t i = 0; i < idxs.size(); ++i) {
if (dels[i]) {
if (idxs[i] != q.invalid_id())
return false; // ERROR
} else {
size_t qid = idxs[i];
if (qid >= q.heap_size()) {
return false; // ERROR
}
MyValue &mv = q[qid];
if (mv.id != i) {
return false; // ERROR
}
}
}
return true;
};
CHECK(check()); // initial check
// Generate an element ID of an elmenet, which was not yet deleted, thus it is still valid.
auto get_valid_id = [&]()->int {
int id = 0;
do {
id = rand() % count;
} while (dels[id]);
return id;
};
// Remove first 100 elements from the queue of 5000 elements, cross-validate indices.
// Re-enter every 20th element back to the queue.
for (size_t i = 0; i < 100; i++) {
MyValue v = q.top(); // copy
q.pop();
dels[v.id] = true;
CHECK(check());
if (i % 20 == 0) {
v.val = rand_val();
q.push(v);
dels[v.id] = false;
CHECK(check());
continue;
}
// Remove some valid element from the queue.
int id = get_valid_id();
CHECK(idxs[id] != q.invalid_id());
q.remove(idxs[id]);
dels[id] = true;
CHECK(check());
// and change 5 random elements and reorder them in the queue.
for (size_t j = 0; j < 5; j++) {
int id = get_valid_id();
size_t qid = idxs[id];
MyValue &mv = q[qid];
mv.val = rand_val();
q.update(qid);
CHECK(check());
}
}
}

View File

@@ -0,0 +1,59 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/Optimize/BruteforceOptimizer.hpp>
#include <libslic3r/Optimize/NLoptOptimizer.hpp>
void check_opt_result(double score, double ref, double abs_err, double rel_err)
{
double abs_diff = std::abs(score - ref);
double rel_diff = std::abs(abs_diff / std::abs(ref));
bool abs_reached = abs_diff < abs_err;
bool rel_reached = rel_diff < rel_err;
bool precision_reached = abs_reached || rel_reached;
REQUIRE(precision_reached);
}
template<class Opt> void test_sin(Opt &&opt)
{
using namespace Slic3r::opt;
auto optfunc = [](const auto &in) {
auto [phi] = in;
return std::sin(phi);
};
auto init = initvals({PI});
auto optbounds = bounds({ {0., 2 * PI}});
Result result_min = opt.to_min().optimize(optfunc, init, optbounds);
Result result_max = opt.to_max().optimize(optfunc, init, optbounds);
check_opt_result(result_min.score, -1., 1e-2, 1e-4);
check_opt_result(result_max.score, 1., 1e-2, 1e-4);
}
template<class Opt> void test_sphere_func(Opt &&opt)
{
using namespace Slic3r::opt;
Result result = opt.to_min().optimize([](const auto &in) {
auto [x, y] = in;
return x * x + y * y + 1.;
}, initvals({.6, -0.2}), bounds({{-1., 1.}, {-1., 1.}}));
check_opt_result(result.score, 1., 1e-2, 1e-4);
}
TEST_CASE("Test brute force optimzer for basic 1D and 2D functions", "[Opt]") {
using namespace Slic3r::opt;
Optimizer<AlgBruteForce> opt;
test_sin(opt);
test_sphere_func(opt);
}

View File

@@ -0,0 +1,306 @@
#include <catch2/catch.hpp>
#include "libslic3r/PlaceholderParser.hpp"
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r;
SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
PlaceholderParser parser;
auto config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict( {
{ "printer_notes", " PRINTER_VENDOR_PRUSA3D PRINTER_MODEL_MK2 " },
{ "nozzle_diameter", "0.6;0.6;0.6;0.6" },
{ "temperature", "357;359;363;378" }
});
// To test the "first_layer_extrusion_width" over "first_layer_heigth".
// "first_layer_heigth" over "layer_height" is no more supported after first_layer_height was moved from PrintObjectConfig to PrintConfig.
// config.option<ConfigOptionFloatOrPercent>("first_layer_height")->value = 150.;
// config.option<ConfigOptionFloatOrPercent>("first_layer_height")->percent = true;
config.option<ConfigOptionFloatOrPercent>("first_layer_height")->value = 1.5 * config.opt_float("layer_height");
config.option<ConfigOptionFloatOrPercent>("first_layer_height")->percent = false;
// To let the PlaceholderParser throw when referencing first_layer_speed if it is set to percent, as the PlaceholderParser does not know
// a percent to what.
config.option<ConfigOptionFloatOrPercent>("first_layer_speed")->value = 50.;
config.option<ConfigOptionFloatOrPercent>("first_layer_speed")->percent = true;
ConfigOptionFloatsNullable *opt_filament_retract_length = config.option<ConfigOptionFloatsNullable>("filament_retract_length", true);
opt_filament_retract_length->values = { 5., ConfigOptionFloatsNullable::nil_value(), 3. };
parser.apply_config(config);
parser.set("foo", 0);
parser.set("bar", 2);
parser.set("num_extruders", 4);
parser.set("gcode_flavor", "marlin");
SECTION("nested config options (legacy syntax)") { REQUIRE(parser.process("[temperature_[foo]]") == "357"); }
SECTION("array reference") { REQUIRE(parser.process("{temperature[foo]}") == "357"); }
SECTION("whitespaces and newlines are maintained") { REQUIRE(parser.process("test [ temperature_ [foo] ] \n hu") == "test 357 \n hu"); }
SECTION("nullable is not null") { REQUIRE(parser.process("{is_nil(filament_retract_length[0])}") == "false"); }
SECTION("nullable is null") { REQUIRE(parser.process("{is_nil(filament_retract_length[1])}") == "true"); }
SECTION("nullable is not null 2") { REQUIRE(parser.process("{is_nil(filament_retract_length[2])}") == "false"); }
SECTION("multiple expressions") { REQUIRE(parser.process("{temperature[foo];temperature[foo]}") == "357357"); }
SECTION("multiple expressions with semicolons") { REQUIRE(parser.process("{temperature[foo];;;temperature[foo];}") == "357357"); }
SECTION("multiple expressions with semicolons 2") { REQUIRE(parser.process("{temperature[foo];;temperature[foo];}") == "357357"); }
SECTION("multiple expressions with semicolons 3") { REQUIRE(parser.process("{temperature[foo];;;temperature[foo];;}") == "357357"); }
SECTION("parsing string with escaped characters") { REQUIRE(parser.process("{\"hu\\nha\\\\\\\"ha\\\"\"}") == "hu\nha\\\"ha\""); }
WHEN("An UTF-8 character is used inside the code block") {
THEN("A std::runtime_error exception is thrown.") {
// full-width plus sign instead of plain +
REQUIRE_THROWS_AS(parser.process("{1\xEF\xBC\x8B 3}"), std::runtime_error);
}
}
WHEN("An UTF-8 character is used inside a string") {
THEN("UTF-8 sequence is processed correctly when quoted") {
// japanese "cool" or "stylish"
REQUIRE(parser.process("{1+\"\xE3\x81\x8B\xE3\x81\xA3\xE3\x81\x93\xE3\x81\x84\xE3\x81\x84\"+\" \"+3}") == "1\xE3\x81\x8B\xE3\x81\xA3\xE3\x81\x93\xE3\x81\x84\xE3\x81\x84 3");
}
}
WHEN("An UTF-8 character is used inside a string") {
THEN("UTF-8 sequence is processed correctly outside of code blocks") {
// japanese "cool" or "stylish"
REQUIRE(parser.process("{1+3}\xE3\x81\x8B\xE3\x81\xA3\xE3\x81\x93\xE3\x81\x84\xE3\x81\x84") == "4\xE3\x81\x8B\xE3\x81\xA3\xE3\x81\x93\xE3\x81\x84\xE3\x81\x84");
}
}
// Test the math expressions.
SECTION("math: 2*3") { REQUIRE(parser.process("{2*3}") == "6"); }
SECTION("math: 2*3/6") { REQUIRE(parser.process("{2*3/6}") == "1"); }
SECTION("math: 2*3/12") { REQUIRE(parser.process("{2*3/12}") == "0"); }
SECTION("math: 2.*3/12") { REQUIRE(std::stod(parser.process("{2.*3/12}")) == Approx(0.5)); }
SECTION("math: 10 % 2.5") { REQUIRE(std::stod(parser.process("{10%2.5}")) == Approx(0.)); }
SECTION("math: 11 % 2.5") { REQUIRE(std::stod(parser.process("{11%2.5}")) == Approx(1.)); }
SECTION("math: 2*(3-12)") { REQUIRE(parser.process("{2*(3-12)}") == "-18"); }
SECTION("math: 2*foo*(3-12)") { REQUIRE(parser.process("{2*foo*(3-12)}") == "0"); }
SECTION("math: 2*bar*(3-12)") { REQUIRE(parser.process("{2*bar*(3-12)}") == "-36"); }
SECTION("math: 2.5*bar*(3-12)") { REQUIRE(std::stod(parser.process("{2.5*bar*(3-12)}")) == Approx(-45)); }
SECTION("math: min(12, 14)") { REQUIRE(parser.process("{min(12, 14)}") == "12"); }
SECTION("math: max(12, 14)") { REQUIRE(parser.process("{max(12, 14)}") == "14"); }
SECTION("math: min(13.4, -1238.1)") { REQUIRE(std::stod(parser.process("{min(13.4, -1238.1)}")) == Approx(-1238.1)); }
SECTION("math: max(13.4, -1238.1)") { REQUIRE(std::stod(parser.process("{max(13.4, -1238.1)}")) == Approx(13.4)); }
SECTION("math: int(13.4)") { REQUIRE(parser.process("{int(13.4)}") == "13"); }
SECTION("math: int(-13.4)") { REQUIRE(parser.process("{int(-13.4)}") == "-13"); }
SECTION("math: round(13.4)") { REQUIRE(parser.process("{round(13.4)}") == "13"); }
SECTION("math: round(-13.4)") { REQUIRE(parser.process("{round(-13.4)}") == "-13"); }
SECTION("math: round(13.6)") { REQUIRE(parser.process("{round(13.6)}") == "14"); }
SECTION("math: round(-13.6)") { REQUIRE(parser.process("{round(-13.6)}") == "-14"); }
SECTION("math: digits(5, 15)") { REQUIRE(parser.process("{digits(5, 15)}") == " 5"); }
SECTION("math: digits(5., 15)") { REQUIRE(parser.process("{digits(5., 15)}") == " 5"); }
SECTION("math: zdigits(5, 15)") { REQUIRE(parser.process("{zdigits(5, 15)}") == "000000000000005"); }
SECTION("math: zdigits(5., 15)") { REQUIRE(parser.process("{zdigits(5., 15)}") == "000000000000005"); }
SECTION("math: digits(5, 15, 8)") { REQUIRE(parser.process("{digits(5, 15, 8)}") == " 5.00000000"); }
SECTION("math: digits(5., 15, 8)") { REQUIRE(parser.process("{digits(5, 15, 8)}") == " 5.00000000"); }
SECTION("math: zdigits(5, 15, 8)") { REQUIRE(parser.process("{zdigits(5, 15, 8)}") == "000005.00000000"); }
SECTION("math: zdigits(5., 15, 8)") { REQUIRE(parser.process("{zdigits(5, 15, 8)}") == "000005.00000000"); }
SECTION("math: digits(13.84375892476, 15, 8)") { REQUIRE(parser.process("{digits(13.84375892476, 15, 8)}") == " 13.84375892"); }
SECTION("math: zdigits(13.84375892476, 15, 8)") { REQUIRE(parser.process("{zdigits(13.84375892476, 15, 8)}") == "000013.84375892"); }
SECTION("math: ternary1") { REQUIRE(parser.process("{12 == 12 ? 1 - 3 : 2 * 2 * unknown_symbol}") == "-2"); }
SECTION("math: ternary2") { REQUIRE(parser.process("{12 == 21/2 ? 1 - 1 - unknown_symbol : 2 * 2}") == "4"); }
SECTION("math: ternary3") { REQUIRE(parser.process("{12 == 13 ? 1 - 1 * unknown_symbol : 2 * 2}") == "4"); }
SECTION("math: ternary4") { REQUIRE(parser.process("{12 == 2 * 6 ? 1 - 1 : 2 * unknown_symbol}") == "0"); }
SECTION("math: ternary nested") { REQUIRE(parser.process("{12 == 2 * 6 ? 3 - 1 != 2 ? does_not_exist : 0 * 0 - 0 / 1 + 12345 : bull ? 3 - cokoo : 2 * unknown_symbol}") == "12345"); }
SECTION("math: interpolate_table(13.84375892476, (0, 0), (20, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13.84375892476, (0, 0), (20, 20))}")) == Approx(13.84375892476)); }
SECTION("math: interpolate_table(13, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13, (0, 0), (20, 20), (30, 20))}")) == Approx(13.)); }
SECTION("math: interpolate_table(25, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(25, (0, 0), (20, 20), (30, 20))}")) == Approx(20.)); }
// Test the "coFloatOrPercent" and "xxx_extrusion_width" substitutions.
// first_layer_extrusion_width ratio_over first_layer_heigth.
SECTION("perimeter_extrusion_width") { REQUIRE(std::stod(parser.process("{perimeter_extrusion_width}")) == Approx(0.67500001192092896)); }
SECTION("first_layer_extrusion_width") { REQUIRE(std::stod(parser.process("{first_layer_extrusion_width}")) == Approx(0.9)); }
SECTION("support_material_xy_spacing") { REQUIRE(std::stod(parser.process("{support_material_xy_spacing}")) == Approx(0.3375)); }
// external_perimeter_speed over perimeter_speed
SECTION("external_perimeter_speed") { REQUIRE(std::stod(parser.process("{external_perimeter_speed}")) == Approx(30.)); }
// infill_overlap over perimeter_extrusion_width
SECTION("infill_overlap") { REQUIRE(std::stod(parser.process("{infill_overlap}")) == Approx(0.16875)); }
// If first_layer_speed is set to percent, then it is applied over respective extrusion types by overriding their respective speeds.
// The PlaceholderParser has no way to know which extrusion type the caller has in mind, therefore it throws.
SECTION("first_layer_speed") { REQUIRE_THROWS(parser.process("{first_layer_speed}")); }
// Test the boolean expression parser.
auto boolean_expression = [&parser](const std::string& templ) { return parser.evaluate_boolean_expression(templ, parser.config()); };
SECTION("boolean expression parser: 12 == 12") { REQUIRE(boolean_expression("12 == 12")); }
SECTION("boolean expression parser: 12 != 12") { REQUIRE(! boolean_expression("12 != 12")); }
SECTION("boolean expression parser: regex matches") { REQUIRE(boolean_expression("\"has some PATTERN embedded\" =~ /.*PATTERN.*/")); }
SECTION("boolean expression parser: regex does not match") { REQUIRE(! boolean_expression("\"has some PATTERN embedded\" =~ /.*PTRN.*/")); }
SECTION("boolean expression parser: accessing variables, equal") { REQUIRE(boolean_expression("foo + 2 == bar")); }
SECTION("boolean expression parser: accessing variables, not equal") { REQUIRE(! boolean_expression("foo + 3 == bar")); }
SECTION("boolean expression parser: (12 == 12) and (13 != 14)") { REQUIRE(boolean_expression("(12 == 12) and (13 != 14)")); }
SECTION("boolean expression parser: (12 == 12) && (13 != 14)") { REQUIRE(boolean_expression("(12 == 12) && (13 != 14)")); }
SECTION("boolean expression parser: (12 == 12) or (13 == 14)") { REQUIRE(boolean_expression("(12 == 12) or (13 == 14)")); }
SECTION("boolean expression parser: (12 == 12) || (13 == 14)") { REQUIRE(boolean_expression("(12 == 12) || (13 == 14)")); }
SECTION("boolean expression parser: (12 == 12) and not (13 == 14)") { REQUIRE(boolean_expression("(12 == 12) and not (13 == 14)")); }
SECTION("boolean expression parser: ternary true") { REQUIRE(boolean_expression("(12 == 12) ? (1 - 1 == 0) : (2 * 2 == 3 * unknown_symbol)")); }
SECTION("boolean expression parser: ternary false") { REQUIRE(! boolean_expression("(12 == 21/2) ? (1 - 1 == 0 - unknown_symbol) : (2 * 2 == 3)")); }
SECTION("boolean expression parser: ternary false 2") { REQUIRE(boolean_expression("(12 == 13) ? (1 - 1 == 3 * unknown_symbol) : (2 * 2 == 4)")); }
SECTION("boolean expression parser: ternary true 2") { REQUIRE(! boolean_expression("(12 == 2 * 6) ? (1 - 1 == 3) : (2 * 2 == 4 * unknown_symbol)")); }
SECTION("boolean expression parser: lower than - false") { REQUIRE(! boolean_expression("12 < 3")); }
SECTION("boolean expression parser: lower than - true") { REQUIRE(boolean_expression("12 < 22")); }
SECTION("boolean expression parser: greater than - true") { REQUIRE(boolean_expression("12 > 3")); }
SECTION("boolean expression parser: greater than - false") { REQUIRE(! boolean_expression("12 > 22")); }
SECTION("boolean expression parser: lower than or equal- false") { REQUIRE(! boolean_expression("12 <= 3")); }
SECTION("boolean expression parser: lower than or equal - true") { REQUIRE(boolean_expression("12 <= 22")); }
SECTION("boolean expression parser: greater than or equal - true") { REQUIRE(boolean_expression("12 >= 3")); }
SECTION("boolean expression parser: greater than or equal - false") { REQUIRE(! boolean_expression("12 >= 22")); }
SECTION("boolean expression parser: lower than or equal (same values) - true") { REQUIRE(boolean_expression("12 <= 12")); }
SECTION("boolean expression parser: greater than or equal (same values) - true") { REQUIRE(boolean_expression("12 >= 12")); }
SECTION("boolean expression parser: one_of(\"a\", \"a\", \"b\", \"c\")") { REQUIRE(boolean_expression("one_of(\"a\", \"a\", \"b\", \"c\")")); }
SECTION("boolean expression parser: one_of(\"b\", \"a\", \"b\", \"c\")") { REQUIRE(boolean_expression("one_of(\"b\", \"a\", \"b\", \"c\")")); }
SECTION("boolean expression parser: one_of(\"c\", \"a\", \"b\", \"c\")") { REQUIRE(boolean_expression("one_of(\"c\", \"a\", \"b\", \"c\")")); }
SECTION("boolean expression parser: one_of(\"d\", \"a\", \"b\", \"c\")") { REQUIRE(! boolean_expression("one_of(\"d\", \"a\", \"b\", \"c\")")); }
SECTION("boolean expression parser: one_of(\"a\")") { REQUIRE(! boolean_expression("one_of(\"a\")")); }
SECTION("boolean expression parser: one_of(\"a\", \"a\")") { REQUIRE(boolean_expression("one_of(\"a\", \"a\")")); }
SECTION("boolean expression parser: one_of(\"b\", \"a\")") { REQUIRE(! boolean_expression("one_of(\"b\", \"a\")")); }
SECTION("boolean expression parser: one_of(\"abcdef\", /.*c.*/)") { REQUIRE(boolean_expression("one_of(\"abcdef\", /.*c.*/)")); }
SECTION("boolean expression parser: one_of(\"abcdef\", /.*f.*/, /.*c.*/)") { REQUIRE(boolean_expression("one_of(\"abcdef\", /.*f.*/, /.*c.*/)")); }
SECTION("boolean expression parser: one_of(\"abcdef\", ~\".*f.*\", ~\".*c.*\")") { REQUIRE(boolean_expression("one_of(\"abcdef\", ~\".*f.*\", ~\".*c.*\")")); }
SECTION("boolean expression parser: one_of(\"ghij\", /.*f.*/, /.*c.*/)") { REQUIRE(! boolean_expression("one_of(\"ghij\", /.*f.*/, /.*c.*/)")); }
SECTION("boolean expression parser: one_of(\"ghij\", ~\".*f.*\", ~\".*c.*\")") { REQUIRE(! boolean_expression("one_of(\"ghij\", ~\".*f.*\", ~\".*c.*\")")); }
SECTION("complex expression") { REQUIRE(boolean_expression("printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK2.*/ and nozzle_diameter[0]==0.6 and num_extruders>1")); }
SECTION("complex expression2") { REQUIRE(boolean_expression("printer_notes=~/.*PRINTER_VEwerfNDOR_PRUSA3D.*/ or printer_notes=~/.*PRINTertER_MODEL_MK2.*/ or (nozzle_diameter[0]==0.6 and num_extruders>1)")); }
SECTION("complex expression3") { REQUIRE(! boolean_expression("printer_notes=~/.*PRINTER_VEwerfNDOR_PRUSA3D.*/ or printer_notes=~/.*PRINTertER_MODEL_MK2.*/ or (nozzle_diameter[0]==0.3 and num_extruders>1)")); }
SECTION("enum expression") { REQUIRE(boolean_expression("gcode_flavor == \"marlin\"")); }
SECTION("write to a scalar variable") {
DynamicConfig config_outputs;
config_outputs.set_key_value("writable_string", new ConfigOptionString());
parser.process("{writable_string = \"Written\"}", 0, nullptr, &config_outputs, nullptr);
REQUIRE(parser.process("{writable_string}", 0, nullptr, &config_outputs, nullptr) == "Written");
}
SECTION("write to a vector variable") {
DynamicConfig config_outputs;
config_outputs.set_key_value("writable_floats", new ConfigOptionFloats({ 0., 0., 0. }));
parser.process("{writable_floats[1] = 33}", 0, nullptr, &config_outputs, nullptr);
REQUIRE(config_outputs.opt_float("writable_floats", 1) == Approx(33.));
}
}
SCENARIO("Placeholder parser variables", "[PlaceholderParser]") {
PlaceholderParser parser;
auto config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "filament_notes", "testnotes" },
{ "enable_dynamic_fan_speeds", "1" },
{ "nozzle_diameter", "0.6;0.6;0.6;0.6" },
{ "temperature", "357;359;363;378" }
});
PlaceholderParser::ContextData context_with_global_dict;
context_with_global_dict.global_config = std::make_unique<DynamicConfig>();
SECTION("create an int local variable") { REQUIRE(parser.process("{local myint = 33+2}{myint}", 0, nullptr, nullptr, nullptr) == "35"); }
SECTION("create a string local variable") { REQUIRE(parser.process("{local mystr = \"mine\" + \"only\" + \"mine\"}{mystr}", 0, nullptr, nullptr, nullptr) == "mineonlymine"); }
SECTION("create a bool local variable") { REQUIRE(parser.process("{local mybool = 1 + 1 == 2}{mybool}", 0, nullptr, nullptr, nullptr) == "true"); }
SECTION("create an int global variable") { REQUIRE(parser.process("{global myint = 33+2}{myint}", 0, nullptr, nullptr, &context_with_global_dict) == "35"); }
SECTION("create a string global variable") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); }
SECTION("create a bool global variable") { REQUIRE(parser.process("{global mybool = 1 + 1 == 2}{mybool}", 0, nullptr, nullptr, &context_with_global_dict) == "true"); }
SECTION("create an int local variable and overwrite it") { REQUIRE(parser.process("{local myint = 33+2}{myint = 12}{myint}", 0, nullptr, nullptr, nullptr) == "12"); }
SECTION("create a string local variable and overwrite it") { REQUIRE(parser.process("{local mystr = \"mine\" + \"only\" + \"mine\"}{mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, nullptr) == "yours"); }
SECTION("create a bool local variable and overwrite it") { REQUIRE(parser.process("{local mybool = 1 + 1 == 2}{mybool = false}{mybool}", 0, nullptr, nullptr, nullptr) == "false"); }
SECTION("create an int global variable and overwrite it") { REQUIRE(parser.process("{global myint = 33+2}{myint = 12}{myint}", 0, nullptr, nullptr, &context_with_global_dict) == "12"); }
SECTION("create a string global variable and overwrite it") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "yours"); }
SECTION("create a bool global variable and overwrite it") { REQUIRE(parser.process("{global mybool = 1 + 1 == 2}{mybool = false}{mybool}", 0, nullptr, nullptr, &context_with_global_dict) == "false"); }
SECTION("create an int local variable and redefine it") { REQUIRE(parser.process("{local myint = 33+2}{local myint = 12}{myint}", 0, nullptr, nullptr, nullptr) == "12"); }
SECTION("create a string local variable and redefine it") { REQUIRE(parser.process("{local mystr = \"mine\" + \"only\" + \"mine\"}{local mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, nullptr) == "yours"); }
SECTION("create a bool local variable and redefine it") { REQUIRE(parser.process("{local mybool = 1 + 1 == 2}{local mybool = false}{mybool}", 0, nullptr, nullptr, nullptr) == "false"); }
SECTION("create an int global variable and redefine it") { REQUIRE(parser.process("{global myint = 33+2}{global myint = 12}{myint}", 0, nullptr, nullptr, &context_with_global_dict) == "12"); }
SECTION("create a string global variable and redefine it") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{global mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "yours"); }
SECTION("create a bool global variable and redefine it") { REQUIRE(parser.process("{global mybool = 1 + 1 == 2}{global mybool = false}{mybool}", 0, nullptr, nullptr, &context_with_global_dict) == "false"); }
SECTION("create an ints local variable with repeat()") { REQUIRE(parser.process("{local myint = repeat(2*3, 4*6)}{myint[5]}", 0, nullptr, nullptr, nullptr) == "24"); }
SECTION("create a strings local variable with repeat()") { REQUIRE(parser.process("{local mystr = repeat(2*3, \"mine\" + \"only\" + \"mine\")}{mystr[5]}", 0, nullptr, nullptr, nullptr) == "mineonlymine"); }
SECTION("create a bools local variable with repeat()") { REQUIRE(parser.process("{local mybool = repeat(5, 1 + 1 == 2)}{mybool[4]}", 0, nullptr, nullptr, nullptr) == "true"); }
SECTION("create an ints global variable with repeat()") { REQUIRE(parser.process("{global myint = repeat(2*3, 4*6)}{myint[5]}", 0, nullptr, nullptr, &context_with_global_dict) == "24"); }
SECTION("create a strings global variable with repeat()") { REQUIRE(parser.process("{global mystr = repeat(2*3, \"mine\" + \"only\" + \"mine\")}{mystr[5]}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); }
SECTION("create a bools global variable with repeat()") { REQUIRE(parser.process("{global mybool = repeat(5, 1 + 1 == 2)}{mybool[4]}", 0, nullptr, nullptr, &context_with_global_dict) == "true"); }
SECTION("create an ints local variable with initializer list") { REQUIRE(parser.process("{local myint = (2*3, 4*6, 5*5)}{myint[1]}", 0, nullptr, nullptr, nullptr) == "24"); }
SECTION("create a strings local variable with initializer list") { REQUIRE(parser.process("{local mystr = (2*3, \"mine\" + \"only\" + \"mine\", 8)}{mystr[1]}", 0, nullptr, nullptr, nullptr) == "mineonlymine"); }
SECTION("create a bools local variable with initializer list") { REQUIRE(parser.process("{local mybool = (3*3 == 8, 1 + 1 == 2)}{mybool[1]}", 0, nullptr, nullptr, nullptr) == "true"); }
SECTION("create an ints global variable with initializer list") { REQUIRE(parser.process("{global myint = (2*3, 4*6, 5*5)}{myint[1]}", 0, nullptr, nullptr, &context_with_global_dict) == "24"); }
SECTION("create a strings global variable with initializer list") { REQUIRE(parser.process("{global mystr = (2*3, \"mine\" + \"only\" + \"mine\", 8)}{mystr[1]}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); }
SECTION("create a bools global variable with initializer list") { REQUIRE(parser.process("{global mybool = (2*3 == 8, 1 + 1 == 2, 5*5 != 33)}{mybool[1]}", 0, nullptr, nullptr, &context_with_global_dict) == "true"); }
SECTION("create an ints local variable by a copy") { REQUIRE(parser.process("{local myint = temperature}{myint[0]}", 0, &config, nullptr, nullptr) == "357"); }
SECTION("create a strings local variable by a copy") { REQUIRE(parser.process("{local mystr = filament_notes}{mystr[0]}", 0, &config, nullptr, nullptr) == "testnotes"); }
SECTION("create a bools local variable by a copy") { REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool[0]}", 0, &config, nullptr, nullptr) == "true"); }
SECTION("create an ints global variable by a copy") { REQUIRE(parser.process("{global myint = temperature}{myint[0]}", 0, &config, nullptr, &context_with_global_dict) == "357"); }
SECTION("create a strings global variable by a copy") { REQUIRE(parser.process("{global mystr = filament_notes}{mystr[0]}", 0, &config, nullptr, &context_with_global_dict) == "testnotes"); }
SECTION("create a bools global variable by a copy") { REQUIRE(parser.process("{global mybool = enable_dynamic_fan_speeds}{mybool[0]}", 0, &config, nullptr, &context_with_global_dict) == "true"); }
SECTION("create an ints local variable by a copy and overwrite it") {
REQUIRE(parser.process("{local myint = temperature}{myint = repeat(2*3, 4*6)}{myint[5]}", 0, &config, nullptr, nullptr) == "24");
REQUIRE(parser.process("{local myint = temperature}{myint = (2*3, 4*6)}{myint[1]}", 0, &config, nullptr, nullptr) == "24");
REQUIRE(parser.process("{local myint = temperature}{myint = (1)}{myint = temperature}{myint[0]}", 0, &config, nullptr, nullptr) == "357");
}
SECTION("create a strings local variable by a copy and overwrite it") {
REQUIRE(parser.process("{local mystr = filament_notes}{mystr = repeat(2*3, \"mine\" + \"only\" + \"mine\")}{mystr[5]}", 0, &config, nullptr, nullptr) == "mineonlymine");
REQUIRE(parser.process("{local mystr = filament_notes}{mystr = (2*3, \"mine\" + \"only\" + \"mine\")}{mystr[1]}", 0, &config, nullptr, nullptr) == "mineonlymine");
REQUIRE(parser.process("{local mystr = filament_notes}{mystr = (2*3, \"mine\" + \"only\" + \"mine\")}{mystr = filament_notes}{mystr[0]}", 0, &config, nullptr, nullptr) == "testnotes");
}
SECTION("create a bools local variable by a copy and overwrite it") {
REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool = repeat(2*3, true)}{mybool[5]}", 0, &config, nullptr, nullptr) == "true");
REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool = (false, true)}{mybool[1]}", 0, &config, nullptr, nullptr) == "true");
REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool = (false, false)}{mybool = enable_dynamic_fan_speeds}{mybool[0]}", 0, &config, nullptr, nullptr) == "true");
}
SECTION("size() of a non-empty vector returns the right size") { REQUIRE(parser.process("{local myint = (0, 1, 2, 3)}{size(myint)}", 0, nullptr, nullptr, nullptr) == "4"); }
SECTION("size() of a an empty vector returns the right size") { REQUIRE(parser.process("{local myint = (0);myint=();size(myint)}", 0, nullptr, nullptr, nullptr) == "0"); }
SECTION("empty() of a non-empty vector returns false") { REQUIRE(parser.process("{local myint = (0, 1, 2, 3)}{empty(myint)}", 0, nullptr, nullptr, nullptr) == "false"); }
SECTION("empty() of a an empty vector returns true") { REQUIRE(parser.process("{local myint = (0);myint=();empty(myint)}", 0, nullptr, nullptr, nullptr) == "true"); }
SECTION("nested if with new variables") {
std::string script =
"{if 1 == 1}{local myints = (5, 4, 3, 2, 1)}{else}{local myfloats = (1., 2., 3., 4., 5., 6., 7.)}{endif}"
"{myints[1]},{size(myints)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "4,5");
}
SECTION("nested if with new variables 2") {
std::string script =
"{if 1 == 0}{local myints = (5, 4, 3, 2, 1)}{else}{local myfloats = (1., 2., 3., 4., 5., 6., 7.)}{endif}"
"{size(myfloats)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "7");
}
SECTION("nested if with new variables 2, mixing }{ with ;") {
std::string script =
"{if 1 == 0 then local myints = (5, 4, 3, 2, 1);else;local myfloats = (1., 2., 3., 4., 5., 6., 7.);endif}"
"{size(myfloats)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "7");
}
SECTION("nested if with new variables, two level") {
std::string script =
"{if 1 == 1}{if 2 == 3}{nejaka / haluz}{else}{local myints = (6, 5, 4, 3, 2, 1)}{endif}{else}{if zase * haluz}{else}{local myfloats = (1., 2., 3., 4., 5., 6., 7.)}{endif}{endif}"
"{size(myints)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "6");
}
SECTION("if with empty block and ;") {
std::string script =
"{if false then else;local myfloats = (1., 2., 3., 4., 5., 6., 7.);endif}"
"{size(myfloats)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "7");
}
SECTION("nested if with new variables, two level, mixing }{ with ;") {
std::string script =
"{if 1 == 1 then if 2 == 3}nejaka / haluz{else local myints = (6, 5, 4, 3, 2, 1) endif else if zase * haluz then else local myfloats = (1., 2., 3., 4., 5., 6., 7.) endif endif}"
"{size(myints)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "6");
}
SECTION("nested if with new variables, two level, mixing }{ with ; 2") {
std::string script =
"{if 1 == 1 then if 2 == 3 then nejaka / haluz else}{local myints = (6, 5, 4, 3, 2, 1)}{endif else if zase * haluz then else local myfloats = (1., 2., 3., 4., 5., 6., 7.) endif endif}"
"{size(myints)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "6");
}
SECTION("nested if with new variables, two level, mixing }{ with ; 3") {
std::string script =
"{if 1 == 1 then if 2 == 3 then nejaka / haluz else}{local myints = (6, 5, 4, 3, 2, 1)}{endif else}{if zase * haluz}{else local myfloats = (1., 2., 3., 4., 5., 6., 7.) endif}{endif}"
"{size(myints)}";
REQUIRE(parser.process(script, 0, nullptr, nullptr, nullptr) == "6");
}
SECTION("if else completely empty") { REQUIRE(parser.process("{if false then elsif false then else endif}", 0, nullptr, nullptr, nullptr) == ""); }
}

View File

@@ -0,0 +1,55 @@
#define NOMINMAX
#include <catch2/catch.hpp>
#include <numeric>
#include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/SLA/AGGRaster.hpp"
#include "libslic3r/BoundingBox.hpp"
using namespace Slic3r;
static sla::RasterGrayscaleAA create_raster(const sla::Resolution &res)
{
sla::PixelDim pixdim{1., 1.};
auto bb = BoundingBox({0, 0}, {scaled(1.), scaled(1.)});
sla::RasterBase::Trafo trafo;
trafo.center_x = bb.center().x();
trafo.center_y = bb.center().y();
return sla::RasterGrayscaleAA{res, pixdim, trafo, agg::gamma_threshold(.5)};
}
TEST_CASE("PNG read", "[PNG]") {
auto rst = create_raster({100, 100});
size_t rstsum = 0;
for (size_t r = 0; r < rst.resolution().height_px; ++r)
for (size_t c = 0; c < rst.resolution().width_px; ++c)
rstsum += rst.read_pixel(c, r);
SECTION("Correct png buffer should be recognized as such.") {
auto enc_rst = rst.encode(sla::PNGRasterEncoder{});
REQUIRE(Slic3r::png::is_png({enc_rst.data(), enc_rst.size()}));
}
SECTION("Fake png buffer should be recognized as such.") {
std::vector<uint8_t> fake(10, '\0');
REQUIRE(!Slic3r::png::is_png({fake.data(), fake.size()}));
}
SECTION("Decoded PNG buffer resolution should match the original") {
auto enc_rst = rst.encode(sla::PNGRasterEncoder{});
png::ImageGreyscale img;
png::decode_png({enc_rst.data(), enc_rst.size()}, img);
REQUIRE(img.rows == rst.resolution().height_px);
REQUIRE(img.cols == rst.resolution().width_px);
size_t sum = std::accumulate(img.buf.begin(), img.buf.end(), size_t(0));
REQUIRE(sum == rstsum);
}
}

View File

@@ -0,0 +1,230 @@
#include <catch2/catch.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
using namespace Slic3r;
SCENARIO("Converted Perl tests", "[Polygon]") {
GIVEN("ccw_square") {
Polygon ccw_square{ { 100, 100 }, { 200, 100 }, { 200, 200 }, { 100, 200 } };
Polygon cw_square(ccw_square);
cw_square.reverse();
THEN("ccw_square is valid") {
REQUIRE(ccw_square.is_valid());
}
THEN("cw_square is valid") {
REQUIRE(cw_square.is_valid());
}
THEN("ccw_square.area") {
REQUIRE(ccw_square.area() == 100 * 100);
}
THEN("cw_square.area") {
REQUIRE(cw_square.area() == - 100 * 100);
}
THEN("ccw_square.centroid") {
REQUIRE(ccw_square.centroid() == Point { 150, 150 });
}
THEN("cw_square.centroid") {
REQUIRE(cw_square.centroid() == Point { 150, 150 });
}
THEN("ccw_square.contains_point(150, 150)") {
REQUIRE(ccw_square.contains({ 150, 150 }));
}
THEN("cw_square.contains_point(150, 150)") {
REQUIRE(cw_square.contains({ 150, 150 }));
}
THEN("conversion to lines") {
REQUIRE(ccw_square.lines() == Lines{
{ { 100, 100 }, { 200, 100 } },
{ { 200, 100 }, { 200, 200 } },
{ { 200, 200 }, { 100, 200 } },
{ { 100, 200 }, { 100, 100 } } });
}
THEN("split_at_first_point") {
REQUIRE(ccw_square.split_at_first_point() == Polyline { ccw_square[0], ccw_square[1], ccw_square[2], ccw_square[3], ccw_square[0] });
}
THEN("split_at_index(2)") {
REQUIRE(ccw_square.split_at_index(2) == Polyline { ccw_square[2], ccw_square[3], ccw_square[0], ccw_square[1], ccw_square[2] });
}
THEN("split_at_vertex(ccw_square[2])") {
REQUIRE(ccw_square.split_at_vertex(ccw_square[2]) == Polyline { ccw_square[2], ccw_square[3], ccw_square[0], ccw_square[1], ccw_square[2] });
}
THEN("is_counter_clockwise") {
REQUIRE(ccw_square.is_counter_clockwise());
}
THEN("! is_counter_clockwise") {
REQUIRE(! cw_square.is_counter_clockwise());
}
THEN("make_counter_clockwise") {
cw_square.make_counter_clockwise();
REQUIRE(cw_square.is_counter_clockwise());
}
THEN("make_counter_clockwise^2") {
cw_square.make_counter_clockwise();
cw_square.make_counter_clockwise();
REQUIRE(cw_square.is_counter_clockwise());
}
THEN("first_point") {
REQUIRE(&ccw_square.first_point() == &ccw_square.points.front());
}
}
GIVEN("Triangulating hexagon") {
Polygon hexagon{ { 100, 0 } };
for (size_t i = 1; i < 6; ++ i) {
Point p = hexagon.points.front();
p.rotate(PI / 3 * i);
hexagon.points.emplace_back(p);
}
Polygons triangles;
hexagon.triangulate_convex(&triangles);
THEN("right number of triangles") {
REQUIRE(triangles.size() == 4);
}
THEN("all triangles are ccw") {
auto it = std::find_if(triangles.begin(), triangles.end(), [](const Polygon &tri) { return tri.is_clockwise(); });
REQUIRE(it == triangles.end());
}
}
GIVEN("General triangle") {
Polygon polygon { { 50000000, 100000000 }, { 300000000, 102000000 }, { 50000000, 104000000 } };
Line line { { 175992032, 102000000 }, { 47983964, 102000000 } };
Point intersection;
bool has_intersection = polygon.intersection(line, &intersection);
THEN("Intersection with line") {
REQUIRE(has_intersection);
REQUIRE(intersection == Point { 50000000, 102000000 });
}
}
}
TEST_CASE("Centroid of Trapezoid must be inside", "[Polygon][Utils]")
{
Slic3r::Polygon trapezoid {
{ 4702134, 1124765853 },
{ -4702134, 1124765853 },
{ -9404268, 1049531706 },
{ 9404268, 1049531706 },
};
Point centroid = trapezoid.centroid();
CHECK(trapezoid.contains(centroid));
}
// This test currently only covers remove_collinear_points.
// All remaining tests are to be ported from xs/t/06_polygon.t
Slic3r::Points collinear_circle({
Slic3r::Point::new_scale(0, 0), // 3 collinear points at beginning
Slic3r::Point::new_scale(10, 0),
Slic3r::Point::new_scale(20, 0),
Slic3r::Point::new_scale(30, 10),
Slic3r::Point::new_scale(40, 20), // 2 collinear points
Slic3r::Point::new_scale(40, 30),
Slic3r::Point::new_scale(30, 40), // 3 collinear points
Slic3r::Point::new_scale(20, 40),
Slic3r::Point::new_scale(10, 40),
Slic3r::Point::new_scale(-10, 20),
Slic3r::Point::new_scale(-20, 10),
Slic3r::Point::new_scale(-20, 0), // 3 collinear points at end
Slic3r::Point::new_scale(-10, 0),
Slic3r::Point::new_scale(-5, 0)
});
SCENARIO("Remove collinear points from Polygon", "[Polygon]") {
GIVEN("Polygon with collinear points"){
Slic3r::Polygon p(collinear_circle);
WHEN("collinear points are removed") {
remove_collinear(p);
THEN("Leading collinear points are removed") {
REQUIRE(p.points.front() == Slic3r::Point::new_scale(20, 0));
}
THEN("Trailing collinear points are removed") {
REQUIRE(p.points.back() == Slic3r::Point::new_scale(-20, 0));
}
THEN("Number of remaining points is correct") {
REQUIRE(p.points.size() == 7);
}
}
}
}
SCENARIO("Simplify polygon", "[Polygon]")
{
GIVEN("gear") {
auto gear = Polygon::new_scale({
{144.9694,317.1543}, {145.4181,301.5633}, {146.3466,296.921}, {131.8436,294.1643}, {131.7467,294.1464},
{121.7238,291.5082}, {117.1631,290.2776}, {107.9198,308.2068}, {100.1735,304.5101}, {104.9896,290.3672},
{106.6511,286.2133}, {93.453,279.2327}, {81.0065,271.4171}, {67.7886,286.5055}, {60.7927,280.1127},
{69.3928,268.2566}, {72.7271,264.9224}, {61.8152,253.9959}, {52.2273,242.8494}, {47.5799,245.7224},
{34.6577,252.6559}, {30.3369,245.2236}, {42.1712,236.3251}, {46.1122,233.9605}, {43.2099,228.4876},
{35.0862,211.5672}, {33.1441,207.0856}, {13.3923,212.1895}, {10.6572,203.3273}, {6.0707,204.8561},
{7.2775,204.4259}, {29.6713,196.3631}, {25.9815,172.1277}, {25.4589,167.2745}, {19.8337,167.0129},
{5.0625,166.3346}, {5.0625,156.9425}, {5.3701,156.9282}, {21.8636,156.1628}, {25.3713,156.4613},
{25.4243,155.9976}, {29.3432,155.8157}, {30.3838,149.3549}, {26.3596,147.8137}, {27.1085,141.2604},
{29.8466,126.8337}, {24.5841,124.9201}, {10.6664,119.8989}, {13.4454,110.9264}, {33.1886,116.0691},
{38.817,103.1819}, {45.8311,89.8133}, {30.4286,76.81}, {35.7686,70.0812}, {48.0879,77.6873},
{51.564,81.1635}, {61.9006,69.1791}, {72.3019,58.7916}, {60.5509,42.5416}, {68.3369,37.1532},
{77.9524,48.1338}, {80.405,52.2215}, {92.5632,44.5992}, {93.0123,44.3223}, {106.3561,37.2056},
{100.8631,17.4679}, {108.759,14.3778}, {107.3148,11.1283}, {117.0002,32.8627}, {140.9109,27.3974},
{145.7004,26.4994}, {145.1346,6.1011}, {154.502,5.4063}, {156.9398,25.6501}, {171.0557,26.2017},
{181.3139,27.323}, {186.2377,27.8532}, {191.6031,8.5474}, {200.6724,11.2756}, {197.2362,30.2334},
{220.0789,39.1906}, {224.3261,41.031}, {236.3506,24.4291}, {243.6897,28.6723}, {234.2956,46.7747},
{245.6562,55.1643}, {257.2523,65.0901}, {261.4374,61.5679}, {273.1709,52.8031}, {278.555,59.5164},
{268.4334,69.8001}, {264.1615,72.3633}, {268.2763,77.9442}, {278.8488,93.5305}, {281.4596,97.6332},
{286.4487,95.5191}, {300.2821,90.5903}, {303.4456,98.5849}, {286.4523,107.7253}, {293.7063,131.1779},
{294.9748,135.8787}, {314.918,133.8172}, {315.6941,143.2589}, {300.9234,146.1746}, {296.6419,147.0309},
{297.1839,161.7052}, {296.6136,176.3942}, {302.1147,177.4857}, {316.603,180.3608}, {317.1658,176.7341},
{315.215,189.6589}, {315.1749,189.6548}, {294.9411,187.5222}, {291.13,201.7233}, {286.2615,215.5916},
{291.1944,218.2545}, {303.9158,225.1271}, {299.2384,233.3694}, {285.7165,227.6001}, {281.7091,225.1956},
{273.8981,237.6457}, {268.3486,245.2248}, {267.4538,246.4414}, {264.8496,250.0221}, {268.6392,253.896},
{278.5017,265.2131}, {272.721,271.4403}, {257.2776,258.3579}, {234.4345,276.5687}, {242.6222,294.8315},
{234.9061,298.5798}, {227.0321,286.2841}, {225.2505,281.8301}, {211.5387,287.8187}, {202.3025,291.0935},
{197.307,292.831}, {199.808,313.1906}, {191.5298,315.0787}, {187.3082,299.8172}, {186.4201,295.3766},
{180.595,296.0487}, {161.7854,297.4248}, {156.8058,297.6214}, {154.3395,317.8592}
});
WHEN("simplified") {
size_t num_points = gear.size();
Polygons simplified = gear.simplify(1000.);
THEN("gear simplified to a single polygon") {
REQUIRE(simplified.size() == 1);
}
THEN("gear was reduced using Douglas-Peucker") {
//note printf "original points: %d\nnew points: %d", $num_points, scalar(@{$simplified->[0]});
REQUIRE(simplified.front().size() < num_points);
}
}
}
}
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/ExPolygonsIndex.hpp"
TEST_CASE("Indexing expolygons", "[ExPolygon]")
{
ExPolygons expolys{
ExPolygon{Polygon{{0, 0}, {10, 0}, {0, 5}}, Polygon{{4, 3}, {6, 3}, {5, 2}}},
ExPolygon{Polygon{{100, 0}, {110, 0}, {100, 5}}, Polygon{{104, 3}, {106, 3}, {105, 2}}}
};
Points points = to_points(expolys);
Lines lines = to_lines(expolys);
Linesf linesf = to_linesf(expolys);
ExPolygonsIndices ids(expolys);
REQUIRE(points.size() == lines.size());
REQUIRE(points.size() == linesf.size());
REQUIRE(points.size() == ids.get_count());
for (size_t i = 0; i < ids.get_count(); i++) {
ExPolygonsIndex id = ids.cvt(i);
const ExPolygon &expoly = expolys[id.expolygons_index];
const Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()];
const Points &pts = poly.points;
const Point &p = pts[id.point_index];
CHECK(points[i] == p);
CHECK(lines[i].a == p);
CHECK(linesf[i].a.cast<int>() == p);
CHECK(ids.cvt(id) == i);
const Point &p_b = ids.is_last_point(id) ? pts.front() : pts[id.point_index + 1];
CHECK(lines[i].b == p_b);
CHECK(linesf[i].b.cast<int>() == p_b);
}
}

View File

@@ -0,0 +1,28 @@
#include <catch2/catch.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
using namespace Slic3r;
SCENARIO("Simplify polyline", "[Polyline]")
{
GIVEN("polyline 1") {
auto polyline = Polyline{ {0,0},{1,0},{2,0},{2,1},{2,2},{1,2},{0,2},{0,1},{0,0} };
WHEN("simplified with Douglas-Peucker") {
polyline.simplify(1.);
THEN("simplified correctly") {
REQUIRE(polyline == Polyline{ {0,0}, {2,0}, {2,2}, {0,2}, {0,0} });
}
}
}
GIVEN("polyline 2") {
auto polyline = Polyline{ {0,0}, {50,50}, {100,0}, {125,-25}, {150,50} };
WHEN("simplified with Douglas-Peucker") {
polyline.simplify(25.);
THEN("not simplified") {
REQUIRE(polyline == Polyline{ {0,0}, {50,50}, {125,-25}, {150,50} });
}
}
}
}

View File

@@ -0,0 +1,304 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/QuadricEdgeCollapse.hpp>
#include <libslic3r/TriangleMesh.hpp> // its - indexed_triangle_set
#include "libslic3r/AABBTreeIndirect.hpp" // is similar
using namespace Slic3r;
namespace Private {
struct Similarity
{
float max_distance = 0.f;
float average_distance = 0.f;
Similarity() = default;
Similarity(float max_distance, float average_distance)
: max_distance(max_distance), average_distance(average_distance)
{}
};
// border for our algorithm with frog_leg model and decimation to 5%
Similarity frog_leg_5(0.32f, 0.043f);
Similarity get_similarity(const indexed_triangle_set &from,
const indexed_triangle_set &to)
{
// create ABBTree
auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(
from.vertices, from.indices);
float sum_distance = 0.f;
float max_distance = 0.f;
auto collect_distances = [&](const Vec3f &surface_point) {
size_t hit_idx;
Vec3f hit_point;
float distance2 =
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
from.vertices, from.indices, tree, surface_point, hit_idx,
hit_point);
float distance = sqrt(distance2);
if (max_distance < distance) max_distance = distance;
sum_distance += distance;
};
for (const Vec3f &vertex : to.vertices) { collect_distances(vertex); }
for (const Vec3i &t : to.indices) {
Vec3f center(0, 0, 0);
for (size_t i = 0; i < 3; ++i) { center += to.vertices[t[i]] / 3; }
collect_distances(center);
}
size_t count = to.vertices.size() + to.indices.size();
float average_distance = sum_distance / count;
std::cout << "max_distance = " << max_distance << ", average_distance = " << average_distance << std::endl;
return Similarity(max_distance, average_distance);
}
void is_better_similarity(const indexed_triangle_set &its_first,
const indexed_triangle_set &its_second,
const Similarity & compare)
{
Similarity s1 = get_similarity(its_first, its_second);
Similarity s2 = get_similarity(its_second, its_first);
CHECK(s1.average_distance < compare.average_distance);
CHECK(s1.max_distance < compare.max_distance);
CHECK(s2.average_distance < compare.average_distance);
CHECK(s2.max_distance < compare.max_distance);
}
void is_worse_similarity(const indexed_triangle_set &its_first,
const indexed_triangle_set &its_second,
const Similarity & compare)
{
Similarity s1 = get_similarity(its_first, its_second);
Similarity s2 = get_similarity(its_second, its_first);
if (s1.max_distance < compare.max_distance &&
s2.max_distance < compare.max_distance)
CHECK(false);
}
bool exist_triangle_with_twice_vertices(const std::vector<stl_triangle_vertex_indices> &indices)
{
for (const auto &face : indices)
if (face[0] == face[1] || face[0] == face[2] || face[1] == face[2])
return true;
return false;
}
} // namespace Private
TEST_CASE("Reduce one edge by Quadric Edge Collapse", "[its]")
{
indexed_triangle_set its;
its.vertices = {Vec3f(-1.f, 0.f, 0.f), Vec3f(0.f, 1.f, 0.f),
Vec3f(1.f, 0.f, 0.f), Vec3f(0.f, 0.f, 1.f),
// vertex to be removed
Vec3f(0.9f, .1f, -.1f)};
its.indices = {Vec3i(1, 0, 3), Vec3i(2, 1, 3), Vec3i(0, 2, 3),
Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(2, 0, 4)};
// edge to remove is between vertices 2 and 4 on trinagles 4 and 5
indexed_triangle_set its_ = its; // copy
// its_write_obj(its, "tetrhedron_in.obj");
uint32_t wanted_count = its.indices.size() - 1;
its_quadric_edge_collapse(its, wanted_count);
// its_write_obj(its, "tetrhedron_out.obj");
CHECK(its.indices.size() == 4);
CHECK(its.vertices.size() == 4);
for (size_t i = 0; i < 3; i++) {
CHECK(its.indices[i] == its_.indices[i]);
}
for (size_t i = 0; i < 4; i++) {
if (i == 2) continue;
CHECK(its.vertices[i] == its_.vertices[i]);
}
const Vec3f &v = its.vertices[2]; // new vertex
const Vec3f &v2 = its_.vertices[2]; // moved vertex
const Vec3f &v4 = its_.vertices[4]; // removed vertex
for (size_t i = 0; i < 3; i++) {
bool is_between = (v[i] < v4[i] && v[i] > v2[i]) ||
(v[i] > v4[i] && v[i] < v2[i]);
CHECK(is_between);
}
Private::Similarity max_similarity(0.75f, 0.014f);
Private::is_better_similarity(its, its_, max_similarity);
}
static bool is_equal(const std::vector<stl_vertex> &v1,
const std::vector<stl_vertex> &v2,
float epsilon = std::numeric_limits<float>::epsilon())
{
// is same count?
if (v1.size() != v2.size()) return false;
// check all v1 vertices
for (const auto &v1_ : v1) {
auto is_equal = [&v1_, epsilon](const auto &v2_) {
for (size_t i = 0; i < 3; i++)
if (fabs(v1_[i] - v2_[i]) > epsilon)
return false;
return true;
};
// is v1 vertex in v2 vertices?
if(std::find_if(v2.begin(), v2.end(), is_equal) == v2.end()) return false;
}
return true;
}
TEST_CASE("Reduce to one triangle by Quadric Edge Collapse", "[its]")
{
// !!! Not work (no manifold - open edges{0-1, 1-2, 2-4, 4-5, 5-3, 3-0}):
/////////////image////
// * 5 //
// |\ //
// | \ //
// 3 *--* 4 //
// | /|\ //
// |/ | \ //
// 0 *--*--* 2 //
// 1 //
//////////////////////
// all triangles are on a plane therefore quadric is zero and
// when reduce edge between vertices 3 and 4 new vertex lay on vertex 3 not 4 !!!
indexed_triangle_set its;
its.vertices = {Vec3f(0.f, 0.f, 0.f), Vec3f(1.f, 0.f, 0.f),
Vec3f(2.f, 0.f, 0.f), Vec3f(0.f, 1.f, 0.f),
Vec3f(1.f, 1.f, 0.f), Vec3f(0.f, 2.f, 0.f)};
its.indices = {Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(0, 4, 3),
Vec3i(3, 4, 5)};
std::vector<stl_vertex> triangle_vertices = {its.vertices[0],
its.vertices[2],
its.vertices[5]};
uint32_t wanted_count = 1;
its_quadric_edge_collapse(its, wanted_count);
// result should be one triangle made of vertices 0, 2, 5
// NOT WORK
//CHECK(its.indices.size() == wanted_count);
//// check all triangle vertices
//CHECK(is_equal(its.vertices, triangle_vertices));
}
TEST_CASE("Reduce to one tetrahedron by Quadric Edge Collapse", "[its]")
{
// Extend previous test to tetrahedron to make it manifold
indexed_triangle_set its;
its.vertices = {
Vec3f(0.f, 0.f, 0.f), Vec3f(1.f, 0.f, 0.f), Vec3f(2.f, 0.f, 0.f),
Vec3f(0.f, 1.f, 0.f), Vec3f(1.f, 1.f, 0.f),
Vec3f(0.f, 2.f, 0.f)
// tetrahedron extetion
, Vec3f(0.f, 0.f, -2.f)
};
std::vector<stl_vertex> tetrahedron_vertices = {its.vertices[0],
its.vertices[2],
its.vertices[5],
// tetrahedron extetion
its.vertices[6]};
its.indices = {Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(0, 4, 3), Vec3i(3, 4, 5),
// tetrahedron extetion
Vec3i(4, 2, 6), Vec3i(5, 4, 6), Vec3i(3, 5, 6), Vec3i(0, 3, 6), Vec3i(1, 0, 6), Vec3i(2, 1, 6)
};
uint32_t wanted_count = 4;
//its_write_obj(its, "tetrhedron_in.obj");
its_quadric_edge_collapse(its, wanted_count);
//its_write_obj(its, "tetrhedron_out.obj");
// result should be tetrahedron
CHECK(its.indices.size() == wanted_count);
// check all tetrahedron vertices
CHECK(is_equal(its.vertices, tetrahedron_vertices));
}
TEST_CASE("Simplify frog_legs.obj to 5% by Quadric edge collapse", "[its][quadric_edge_collapse]")
{
TriangleMesh mesh = load_model("frog_legs.obj");
double original_volume = its_volume(mesh.its);
uint32_t wanted_count = mesh.its.indices.size() * 0.05;
REQUIRE_FALSE(mesh.empty());
indexed_triangle_set its = mesh.its; // copy
float max_error = std::numeric_limits<float>::max();
its_quadric_edge_collapse(its, wanted_count, &max_error);
// its_write_obj(its, "frog_legs_qec.obj");
CHECK(its.indices.size() <= wanted_count);
double volume = its_volume(its);
CHECK(fabs(original_volume - volume) < 33.);
Private::is_better_similarity(mesh.its, its, Private::frog_leg_5);
}
#include <libigl/igl/qslim.h>
TEST_CASE("Simplify frog_legs.obj to 5% by IGL/qslim", "[]")
{
std::string obj_filename = "frog_legs.obj";
TriangleMesh mesh = load_model(obj_filename);
REQUIRE_FALSE(mesh.empty());
indexed_triangle_set &its = mesh.its;
//double original_volume = its_volume(its);
uint32_t wanted_count = its.indices.size() * 0.05;
Eigen::MatrixXd V(its.vertices.size(), 3);
Eigen::MatrixXi F(its.indices.size(), 3);
for (size_t j = 0; j < its.vertices.size(); ++j) {
Vec3d vd = its.vertices[j].cast<double>();
for (int i = 0; i < 3; ++i) V(j, i) = vd(i);
}
for (size_t j = 0; j < its.indices.size(); ++j) {
const auto &f = its.indices[j];
for (int i = 0; i < 3; ++i) F(j, i) = f(i);
}
size_t max_m = wanted_count;
Eigen::MatrixXd U;
Eigen::MatrixXi G;
Eigen::VectorXi J, I;
CHECK(igl::qslim(V, F, max_m, U, G, J, I));
// convert to its
indexed_triangle_set its_out;
its_out.vertices.reserve(U.size()/3);
its_out.indices.reserve(G.size()/3);
size_t U_size = U.size() / 3;
for (size_t i = 0; i < U_size; i++)
its_out.vertices.emplace_back(U(i, 0), U(i, 1), U(i, 2));
size_t G_size = G.size() / 3;
for (size_t i = 0; i < G_size; i++)
its_out.indices.emplace_back(G(i, 0), G(i, 1), G(i, 2));
// check if algorithm is still worse than our
Private::is_worse_similarity(its_out, its, Private::frog_leg_5);
// its_out, its --> avg_distance: 0.0351217, max_distance 0.364316
// its, its_out --> avg_distance: 0.0412358, max_distance 0.238913
}
TEST_CASE("Simplify trouble case", "[its]")
{
TriangleMesh tm = load_model("simplification.obj");
REQUIRE_FALSE(tm.empty());
float max_error = std::numeric_limits<float>::max();
uint32_t wanted_count = 0;
its_quadric_edge_collapse(tm.its, wanted_count, &max_error);
CHECK(!Private::exist_triangle_with_twice_vertices(tm.its.indices));
}
TEST_CASE("Simplified cube should not be empty.", "[its]")
{
auto its = its_make_cube(1, 2, 3);
float max_error = std::numeric_limits<float>::max();
uint32_t wanted_count = 0;
its_quadric_edge_collapse(its, wanted_count, &max_error);
CHECK(!its.indices.empty());
}

View File

@@ -0,0 +1,284 @@
#include <catch2/catch.hpp>
#include <libslic3r/libslic3r.h>
#include <libslic3r/Algorithm/RegionExpansion.hpp>
#include <libslic3r/ClipperUtils.hpp>
#include <libslic3r/ExPolygon.hpp>
#include <libslic3r/Polygon.hpp>
#include <libslic3r/SVG.cpp>
using namespace Slic3r;
//#define DEBUG_TEMP_DIR "d:\\temp\\"
SCENARIO("Region expansion basics", "[RegionExpansion]") {
static constexpr const coord_t ten = scaled<coord_t>(10.);
GIVEN("two touching squares") {
Polygon square1{ { 1 * ten, 1 * ten }, { 2 * ten, 1 * ten }, { 2 * ten, 2 * ten }, { 1 * ten, 2 * ten } };
Polygon square2{ { 2 * ten, 1 * ten }, { 3 * ten, 1 * ten }, { 3 * ten, 2 * ten }, { 2 * ten, 2 * ten } };
Polygon square3{ { 1 * ten, 2 * ten }, { 2 * ten, 2 * ten }, { 2 * ten, 3 * ten }, { 1 * ten, 3 * ten } };
static constexpr const float expansion = scaled<float>(1.);
auto test_expansion = [](const Polygon &src, const Polygon &boundary) {
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{src} }, { ExPolygon{boundary} },
expansion,
scaled<float>(0.3), // expansion step
5); // max num steps
THEN("Single anchor is produced") {
REQUIRE(expanded.size() == 1);
}
THEN("The area of the anchor is 10mm2") {
REQUIRE(area(expanded.front()) == Approx(expansion * ten));
}
};
WHEN("second square expanded into the first square (to left)") {
test_expansion(square2, square1);
}
WHEN("first square expanded into the second square (to right)") {
test_expansion(square1, square2);
}
WHEN("third square expanded into the first square (down)") {
test_expansion(square3, square1);
}
WHEN("first square expanded into the third square (up)") {
test_expansion(square1, square3);
}
}
GIVEN("simple bridge") {
Polygon square1{ { 1 * ten, 1 * ten }, { 2 * ten, 1 * ten }, { 2 * ten, 2 * ten }, { 1 * ten, 2 * ten } };
Polygon square2{ { 2 * ten, 1 * ten }, { 3 * ten, 1 * ten }, { 3 * ten, 2 * ten }, { 2 * ten, 2 * ten } };
Polygon square3{ { 3 * ten, 1 * ten }, { 4 * ten, 1 * ten }, { 4 * ten, 2 * ten }, { 3 * ten, 2 * ten } };
WHEN("expanded") {
static constexpr const float expansion = scaled<float>(1.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{square2} }, { ExPolygon{square1}, ExPolygon{square3} },
expansion,
scaled<float>(0.3), // expansion step
5); // max num steps
THEN("Two anchors are produced") {
REQUIRE(expanded.size() == 1);
REQUIRE(expanded.front().size() == 2);
}
THEN("The area of each anchor is 10mm2") {
REQUIRE(area(expanded.front().front()) == Approx(expansion * ten));
REQUIRE(area(expanded.front().back()) == Approx(expansion * ten));
}
}
WHEN("fully expanded") {
static constexpr const float expansion = scaled<float>(10.1);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{square2} }, { ExPolygon{square1}, ExPolygon{square3} },
expansion,
scaled<float>(2.3), // expansion step
5); // max num steps
THEN("Two anchors are produced") {
REQUIRE(expanded.size() == 1);
REQUIRE(expanded.front().size() == 2);
}
THEN("The area of each anchor is 100mm2") {
REQUIRE(area(expanded.front().front()) == Approx(sqr<double>(ten)));
REQUIRE(area(expanded.front().back()) == Approx(sqr<double>(ten)));
}
}
}
GIVEN("two bridges") {
Polygon left_support { { 1 * ten, 1 * ten }, { 2 * ten, 1 * ten }, { 2 * ten, 4 * ten }, { 1 * ten, 4 * ten } };
Polygon right_support { { 3 * ten, 1 * ten }, { 4 * ten, 1 * ten }, { 4 * ten, 4 * ten }, { 3 * ten, 4 * ten } };
Polygon bottom_bridge { { 2 * ten, 1 * ten }, { 3 * ten, 1 * ten }, { 3 * ten, 2 * ten }, { 2 * ten, 2 * ten } };
Polygon top_bridge { { 2 * ten, 3 * ten }, { 3 * ten, 3 * ten }, { 3 * ten, 4 * ten }, { 2 * ten, 4 * ten } };
WHEN("expanded") {
static constexpr const float expansion = scaled<float>(1.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{bottom_bridge}, ExPolygon{top_bridge} }, { ExPolygon{left_support}, ExPolygon{right_support} },
expansion,
scaled<float>(0.3), // expansion step
5); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "two_bridges-out.svg",
{ { { { ExPolygon{left_support}, ExPolygon{right_support} } }, { "supports", "orange", 0.5f } },
{ { { ExPolygon{bottom_bridge}, ExPolygon{top_bridge} } }, { "bridges", "blue", 0.5f } },
{ { union_ex(union_(expanded.front(), expanded.back())) }, { "expanded", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("Two anchors are produced for each bridge") {
REQUIRE(expanded.size() == 2);
REQUIRE(expanded.front().size() == 2);
REQUIRE(expanded.back().size() == 2);
}
THEN("The area of each anchor is 10mm2") {
double a = expansion * ten + M_PI * sqr(expansion) / 4;
double eps = sqr(scaled<double>(0.1));
REQUIRE(is_approx(area(expanded.front().front()), a, eps));
REQUIRE(is_approx(area(expanded.front().back()), a, eps));
REQUIRE(is_approx(area(expanded.back().front()), a, eps));
REQUIRE(is_approx(area(expanded.back().back()), a, eps));
}
}
}
GIVEN("rectangle with rhombic cut-out") {
double diag = 1 * ten * sqrt(2.) / 4.;
Polygon square_with_rhombic_cutout{ { 0, 0 }, { 1 * ten, 0 }, { ten / 2, ten / 2 }, { 1 * ten, 1 * ten }, { 0, 1 * ten } };
Polygon rhombic { { ten / 2, ten / 2 }, { 3 * ten / 4, ten / 4 }, { 1 * ten, ten / 2 }, { 3 * ten / 4, 3 * ten / 4 } };
WHEN("expanded") {
static constexpr const float expansion = scaled<float>(1.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{rhombic} }, { ExPolygon{square_with_rhombic_cutout} },
expansion,
scaled<float>(0.1), // expansion step
11); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "rectangle_with_rhombic_cut-out.svg",
{ { { { ExPolygon{square_with_rhombic_cutout} } }, { "square_with_rhombic_cutout", "orange", 0.5f } },
{ { { ExPolygon{rhombic} } }, { "rhombic", "blue", 0.5f } },
{ { union_ex(expanded.front()) }, { "bridges", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("Single anchor is produced") {
REQUIRE(expanded.size() == 1);
}
THEN("The area of anchor is correct") {
double area_calculated = area(expanded.front());
double area_expected = 2. * diag * expansion + M_PI * sqr(expansion) * 0.75;
REQUIRE(is_approx(area_expected, area_calculated, sqr(scaled<double>(0.2))));
}
}
WHEN("extra expanded") {
static constexpr const float expansion = scaled<float>(2.5);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{rhombic} }, { ExPolygon{square_with_rhombic_cutout} },
expansion,
scaled<float>(0.25), // expansion step
11); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "rectangle_with_rhombic_cut-out2.svg",
{ { { { ExPolygon{square_with_rhombic_cutout} } }, { "square_with_rhombic_cutout", "orange", 0.5f } },
{ { { ExPolygon{rhombic} } }, { "rhombic", "blue", 0.5f } },
{ { union_ex(expanded.front()) }, { "bridges", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("Single anchor is produced") {
REQUIRE(expanded.size() == 1);
}
THEN("The area of anchor is correct") {
double area_calculated = area(expanded.front());
double area_expected = 2. * diag * expansion + M_PI * sqr(expansion) * 0.75;
REQUIRE(is_approx(area_expected, area_calculated, sqr(scaled<double>(0.3))));
}
}
}
GIVEN("square with two holes") {
Polygon outer{ { 0, 0 }, { 3 * ten, 0 }, { 3 * ten, 5 * ten }, { 0, 5 * ten } };
Polygon hole1{ { 1 * ten, 1 * ten }, { 1 * ten, 2 * ten }, { 2 * ten, 2 * ten }, { 2 * ten, 1 * ten } };
Polygon hole2{ { 1 * ten, 3 * ten }, { 1 * ten, 4 * ten }, { 2 * ten, 4 * ten }, { 2 * ten, 3 * ten } };
ExPolygon boundary(outer);
boundary.holes = { hole1, hole2 };
Polygon anchor{ { -1 * ten, coord_t(1.5 * ten) }, { 0 * ten, coord_t(1.5 * ten) }, { 0, coord_t(3.5 * ten) }, { -1 * ten, coord_t(3.5 * ten) } };
WHEN("expanded") {
static constexpr const float expansion = scaled<float>(5.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary },
expansion,
scaled<float>(0.4), // expansion step
15); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-out.svg",
{ { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } },
{ { { boundary } }, { "boundary", "blue", 0.5f } },
{ { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("The anchor expands into a single region") {
REQUIRE(expanded.size() == 1);
REQUIRE(expanded.front().size() == 1);
}
THEN("The area of anchor is correct") {
double area_calculated = area(expanded.front());
double area_expected = double(expansion) * 2. * double(ten) + M_PI * sqr(expansion) * 0.5;
REQUIRE(is_approx(area_expected, area_calculated, sqr(scaled<double>(0.45))));
}
}
WHEN("expanded even more") {
static constexpr const float expansion = scaled<float>(25.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary },
expansion,
scaled<float>(2.), // expansion step
15); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-expanded2-out.svg",
{ { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } },
{ { { boundary } }, { "boundary", "blue", 0.5f } },
{ { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("The anchor expands into a single region") {
REQUIRE(expanded.size() == 1);
REQUIRE(expanded.front().size() == 1);
}
}
WHEN("expanded yet even more") {
static constexpr const float expansion = scaled<float>(28.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary },
expansion,
scaled<float>(2.), // expansion step
20); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-expanded3-out.svg",
{ { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } },
{ { { boundary } }, { "boundary", "blue", 0.5f } },
{ { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("The anchor expands into a single region with two holes") {
REQUIRE(expanded.size() == 1);
REQUIRE(expanded.front().size() == 3);
}
}
WHEN("expanded fully") {
static constexpr const float expansion = scaled<float>(35.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary },
expansion,
scaled<float>(2.), // expansion step
25); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-expanded_fully-out.svg",
{ { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } },
{ { { boundary } }, { "boundary", "blue", 0.5f } },
{ { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("The anchor expands into a single region with two holes, fully covering the boundary") {
REQUIRE(expanded.size() == 1);
REQUIRE(expanded.front().size() == 3);
REQUIRE(area(expanded.front()) == Approx(area(boundary)));
}
}
}
GIVEN("square with hole, hole edge anchored") {
Polygon outer{ { -1 * ten, -1 * ten }, { 2 * ten, -1 * ten }, { 2 * ten, 2 * ten }, { -1 * ten, 2 * ten } };
Polygon hole { { 0, ten }, { ten, ten }, { ten, 0 }, { 0, 0 } };
Polygon anchor{ { 0, 0 }, { ten, 0 }, { ten, ten }, { 0, ten } };
ExPolygon boundary(outer);
boundary.holes = { hole };
WHEN("expanded") {
static constexpr const float expansion = scaled<float>(5.);
std::vector<Polygons> expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary },
expansion,
scaled<float>(0.4), // expansion step
15); // max num steps
#if 0
SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_hole_anchored-out.svg",
{ { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } },
{ { { boundary } }, { "boundary", "blue", 0.5f } },
{ { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled<coord_t>(0.1f), 0.5f } } });
#endif
THEN("The anchor expands into a single region with a hole") {
REQUIRE(expanded.size() == 1);
REQUIRE(expanded.front().size() == 2);
}
THEN("The area of anchor is correct") {
double area_calculated = area(expanded.front());
double area_expected = double(expansion) * 4. * double(ten) + M_PI * sqr(expansion);
REQUIRE(is_approx(area_expected, area_calculated, sqr(scaled<double>(0.6))));
}
}
}
}

View File

@@ -0,0 +1,57 @@
#include <catch2/catch.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/STL.hpp"
using namespace Slic3r;
static inline std::string stl_path(const char* path)
{
return std::string(TEST_DATA_DIR) + "/test_stl/" + path;
}
SCENARIO("Reading an STL file", "[stl]") {
GIVEN("umlauts in the path of a binary STL file, Czech characters in the file name") {
WHEN("STL file is read") {
Slic3r::Model model;
THEN("load should succeed") {
REQUIRE(Slic3r::load_stl(stl_path("Geräte/20mmbox-čřšřěá.stl").c_str(), &model));
REQUIRE(is_approx(model.objects.front()->volumes.front()->mesh().size(), Vec3d(20, 20, 20)));
}
}
}
GIVEN("in ASCII format") {
WHEN("line endings LF") {
Slic3r::Model model;
THEN("load should succeed") {
REQUIRE(Slic3r::load_stl(stl_path("ASCII/20mmbox-LF.stl").c_str(), &model));
REQUIRE(is_approx(model.objects.front()->volumes.front()->mesh().size(), Vec3d(20, 20, 20)));
}
}
WHEN("line endings CRLF") {
Slic3r::Model model;
THEN("load should succeed") {
REQUIRE(Slic3r::load_stl(stl_path("ASCII/20mmbox-CRLF.stl").c_str(), &model));
REQUIRE(is_approx(model.objects.front()->volumes.front()->mesh().size(), Vec3d(20, 20, 20)));
}
}
#if 0
// ASCII STLs ending with just carriage returns are not supported. These were used by the old Macs, while the Unix based MacOS uses LFs as any other Unix.
WHEN("line endings CR") {
Slic3r::Model model;
THEN("load should succeed") {
REQUIRE(Slic3r::load_stl(stl_path("ASCII/20mmbox-CR.stl").c_str(), &model));
REQUIRE(is_approx(model.objects.front()->volumes.front()->mesh().size(), Vec3d(20, 20, 20)));
}
}
#endif
WHEN("nonstandard STL file (text after ending tags, invalid normals, for example infinities)") {
Slic3r::Model model;
THEN("load should succeed") {
REQUIRE(Slic3r::load_stl(stl_path("ASCII/20mmbox-nonstandard.stl").c_str(), &model));
REQUIRE(is_approx(model.objects.front()->volumes.front()->mesh().size(), Vec3d(20, 20, 20)));
}
}
}
}

View File

@@ -0,0 +1,122 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/SurfaceMesh.hpp>
using namespace Slic3r;
// Generate a broken cube mesh. Face 8 is inverted, face 11 is missing.
indexed_triangle_set its_make_cube_broken(double xd, double yd, double zd)
{
auto x = float(xd), y = float(yd), z = float(zd);
return {
{ {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7},
{0, 4, 7}, {0, 7, 1}, {1, 7, 6}, {1, 6, 2},
{2, 5, 6}, {2, 5, 3}, {4, 0, 3} /*missing face*/ },
{ {x, y, 0}, {x, 0, 0}, {0, 0, 0}, {0, y, 0},
{x, y, z}, {0, y, z}, {0, 0, z}, {x, 0, z} }
};
}
TEST_CASE("SurfaceMesh on a cube", "[SurfaceMesh]") {
indexed_triangle_set cube = its_make_cube(1., 1., 1.);
SurfaceMesh sm(cube);
const Halfedge_index hi_first = sm.halfedge(Face_index(0));
Halfedge_index hi = hi_first;
REQUIRE(! hi_first.is_invalid());
SECTION("next / prev halfedge") {
hi = sm.next(hi);
REQUIRE(hi != hi_first);
hi = sm.next(hi);
hi = sm.next(hi);
REQUIRE(hi == hi_first);
hi = sm.prev(hi);
REQUIRE(hi != hi_first);
hi = sm.prev(hi);
hi = sm.prev(hi);
REQUIRE(hi == hi_first);
}
SECTION("next_around_target") {
// Check that we get to the same halfedge after applying next_around_target
// four times.
const Vertex_index target_vert = sm.target(hi_first);
for (int i=0; i<4;++i) {
hi = sm.next_around_target(hi);
REQUIRE((hi == hi_first) == (i == 3));
REQUIRE(sm.is_same_vertex(sm.target(hi), target_vert));
REQUIRE(! sm.is_border(hi));
}
}
SECTION("iterate around target and source") {
hi = sm.next_around_target(hi);
hi = sm.prev_around_target(hi);
hi = sm.prev_around_source(hi);
hi = sm.next_around_source(hi);
REQUIRE(hi == hi_first);
}
SECTION("opposite") {
const Vertex_index target = sm.target(hi);
const Vertex_index source = sm.source(hi);
hi = sm.opposite(hi);
REQUIRE(sm.is_same_vertex(target, sm.source(hi)));
REQUIRE(sm.is_same_vertex(source, sm.target(hi)));
hi = sm.opposite(hi);
REQUIRE(hi == hi_first);
}
SECTION("halfedges walk") {
for (int i=0; i<4; ++i) {
hi = sm.next(hi);
hi = sm.opposite(hi);
}
REQUIRE(hi == hi_first);
}
SECTION("point accessor") {
Halfedge_index hi = sm.halfedge(Face_index(0));
hi = sm.opposite(hi);
hi = sm.prev(hi);
hi = sm.opposite(hi);
REQUIRE(hi.face() == Face_index(6));
REQUIRE(sm.point(sm.target(hi)).isApprox(cube.vertices[7]));
}
}
TEST_CASE("SurfaceMesh on a broken cube", "[SurfaceMesh]") {
indexed_triangle_set cube = its_make_cube_broken(1., 1., 1.);
SurfaceMesh sm(cube);
SECTION("Check inverted face") {
Halfedge_index hi = sm.halfedge(Face_index(8));
for (int i=0; i<3; ++i) {
REQUIRE(! hi.is_invalid());
REQUIRE(sm.is_border(hi));
}
REQUIRE(hi == sm.halfedge(Face_index(8)));
hi = sm.opposite(hi);
REQUIRE(hi.is_invalid());
}
SECTION("missing face") {
Halfedge_index hi = sm.halfedge(Face_index(0));
for (int i=0; i<3; ++i)
hi = sm.next_around_source(hi);
hi = sm.next(hi);
REQUIRE(sm.is_border(hi));
REQUIRE(! hi.is_invalid());
hi = sm.opposite(hi);
REQUIRE(hi.is_invalid());
}
}

View File

@@ -0,0 +1,49 @@
#include <catch2/catch.hpp>
#include "libslic3r/Time.hpp"
#include <sstream>
#include <iomanip>
#include <locale>
using namespace Slic3r;
static 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);
std::string mydate = "20190710T085000Z";
time_t t = Slic3r::Utils::parse_iso_utc_timestamp(mydate);
std::string date = Slic3r::Utils::iso_utc_timestamp(t);
REQUIRE(date == mydate);
}
TEST_CASE("Slic3r_UTC_Time_Format", "[Timeutils]") {
using namespace Slic3r::Utils;
test_time_fmt(TimeFormat::gcode);
std::string mydate = "2019-07-10 at 08:50:00 UTC";
time_t t = Slic3r::Utils::str2time(mydate, TimeZone::utc, TimeFormat::gcode);
std::string date = Slic3r::Utils::utc_timestamp(t);
REQUIRE(date == mydate);
}

View File

@@ -0,0 +1,127 @@
#include <catch2/catch.hpp>
#include <libslic3r/Triangulation.hpp>
#include <libslic3r/SVG.hpp> // only debug visualization
using namespace Slic3r;
namespace Private{
void store_trinagulation(const ExPolygons &shape,
const std::vector<Vec3i> &triangles,
const char* file_name = "C:/data/temp/triangulation.svg",
double scale = 1e5)
{
BoundingBox bb;
for (const auto &expoly : shape) bb.merge(expoly.contour.points);
bb.scale(scale);
SVG svg_vis(file_name, bb);
svg_vis.draw(shape, "gray", .7f);
Points pts = to_points(shape);
svg_vis.draw(pts, "black", 4 * scale);
for (const Vec3i &t : triangles) {
Slic3r::Polygon triangle({pts[t[0]], pts[t[1]], pts[t[2]]});
triangle.scale(scale);
svg_vis.draw(triangle, "green");
}
// prevent visualization in test
CHECK(false);
}
} // namespace
TEST_CASE("Triangulate rectangle with restriction on edge", "[Triangulation]")
{
// 0 1 2 3
Points points = {Point(1, 1), Point(2, 1), Point(2, 2), Point(1, 2)};
Triangulation::HalfEdges edges1 = {{1, 3}};
std::vector<Vec3i> indices1 = Triangulation::triangulate(points, edges1);
auto check = [](int i1, int i2, Vec3i t) -> bool {
return true;
return (t[0] == i1 || t[1] == i1 || t[2] == i1) &&
(t[0] == i2 || t[1] == i2 || t[2] == i2);
};
REQUIRE(indices1.size() == 2);
int i1 = edges1.begin()->first, i2 = edges1.begin()->second;
CHECK(check(i1, i2, indices1[0]));
CHECK(check(i1, i2, indices1[1]));
Triangulation::HalfEdges edges2 = {{0, 2}};
std::vector<Vec3i> indices2 = Triangulation::triangulate(points, edges2);
REQUIRE(indices2.size() == 2);
i1 = edges2.begin()->first;
i2 = edges2.begin()->second;
CHECK(check(i1, i2, indices2[0]));
CHECK(check(i1, i2, indices2[1]));
}
TEST_CASE("Triangulation polygon", "[triangulation]")
{
Points points = {Point(416, 346), Point(445, 362), Point(463, 389),
Point(469, 427), Point(445, 491)};
Polygon polygon(points);
Polygons polygons({polygon});
ExPolygon expolygon(points);
ExPolygons expolygons({expolygon});
std::vector<Vec3i> tp = Triangulation::triangulate(polygon);
std::vector<Vec3i> tps = Triangulation::triangulate(polygons);
std::vector<Vec3i> tep = Triangulation::triangulate(expolygon);
std::vector<Vec3i> teps = Triangulation::triangulate(expolygons);
//Private::store_trinagulation(expolygons, teps);
CHECK(tp.size() == tps.size());
CHECK(tep.size() == teps.size());
CHECK(tp.size() == tep.size());
CHECK(tp.size() == 3);
}
TEST_CASE("Triangulation M shape polygon", "[triangulation]")
{
// 0 1 2 3 4
Polygon shape_M = {Point(0, 0), Point(2, 0), Point(2, 2), Point(1, 1), Point(0, 2)};
std::vector<Vec3i> triangles = Triangulation::triangulate(shape_M);
// Check outer triangle is not contain
std::set<int> outer_triangle = {2, 3, 4};
bool is_in = false;
for (const Vec3i &t : triangles) {
for (size_t i = 0; i < 3; i++) {
int index = t[i];
if (outer_triangle.find(index) == outer_triangle.end()) {
is_in = false;
break;
} else {
is_in = true;
}
}
if (is_in) break;
}
//Private::store_trinagulation({ExPolygon(shape_M)}, triangles);
CHECK(triangles.size() == 3);
CHECK(!is_in);
}
// same point in triangulation are not Supported
TEST_CASE("Triangulation 2 polygons with same point", "[triangulation]")
{
Slic3r::Polygon polygon1 = {
Point(416, 346), Point(445, 362),
Point(463, 389), Point(469, 427) /* This point */,
Point(445, 491)
};
Slic3r::Polygon polygon2 = {
Point(495, 488), Point(469, 427) /* This point */,
Point(495, 364)
};
ExPolygons shape2d = {ExPolygon(polygon1), ExPolygon(polygon2)};
std::vector<Vec3i> shape_triangles = Triangulation::triangulate(shape2d);
//Private::store_trinagulation(shape2d, shape_triangles);
CHECK(shape_triangles.size() == 4);
}

View File

@@ -0,0 +1,35 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
SCENARIO("Test fast_round_up()") {
using namespace Slic3r;
THEN("fast_round_up<int>(1.5) is 2") {
REQUIRE(fast_round_up<int>(1.5) == 2);
}
THEN("fast_round_up<int>(1.499999999999999) is 1") {
REQUIRE(fast_round_up<int>(1.499999999999999) == 1);
}
THEN("fast_round_up<int>(0.5) is 1") {
REQUIRE(fast_round_up<int>(0.5) == 1);
}
THEN("fast_round_up<int>(0.49999999999999994) is 0") {
REQUIRE(fast_round_up<int>(0.49999999999999994) == 0);
}
THEN("fast_round_up<int>(-0.5) is 0") {
REQUIRE(fast_round_up<int>(-0.5) == 0);
}
THEN("fast_round_up<int>(-0.51) is -1") {
REQUIRE(fast_round_up<int>(-0.51) == -1);
}
THEN("fast_round_up<int>(-0.51) is -1") {
REQUIRE(fast_round_up<int>(-0.51) == -1);
}
THEN("fast_round_up<int>(-1.5) is -1") {
REQUIRE(fast_round_up<int>(-1.5) == -1);
}
THEN("fast_round_up<int>(-1.51) is -2") {
REQUIRE(fast_round_up<int>(-1.51) == -2);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp
sla_print_tests.cpp
sla_test_utils.hpp sla_test_utils.cpp
sla_supptgen_tests.cpp
sla_raycast_tests.cpp
sla_supptreeutils_tests.cpp
sla_archive_readwrite_tests.cpp)
# mold linker for successful linking needs also to link TBB library and link it before libslic3r.
target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb TBB::tbbmalloc libslic3r)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
if (WIN32)
qidislicer_copy_dlls(${_TEST_NAME}_tests)
endif()
# catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${CATCH_EXTRA_ARGS})

View File

@@ -0,0 +1,70 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include "libslic3r/SLAPrint.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Format/SLAArchiveFormatRegistry.hpp"
#include "libslic3r/Format/SLAArchiveWriter.hpp"
#include "libslic3r/Format/SLAArchiveReader.hpp"
#include <boost/filesystem.hpp>
using namespace Slic3r;
TEST_CASE("Archive export test", "[sla_archives]") {
auto registry = registered_sla_archives();
for (const char * pname : {"20mm_cube", "extruder_idler"})
for (const ArchiveEntry &entry : registry) {
INFO(std::string("Testing archive type: ") + entry.id + " -- writing...");
SLAPrint print;
SLAFullPrintConfig fullcfg;
auto m = Model::read_from_file(TEST_DATA_DIR PATH_SEPARATOR + std::string(pname) + ".obj", nullptr);
fullcfg.printer_technology.setInt(ptSLA); // FIXME this should be ensured
fullcfg.set("sla_archive_format", entry.id);
fullcfg.set("supports_enable", false);
fullcfg.set("pad_enable", false);
DynamicPrintConfig cfg;
cfg.apply(fullcfg);
print.set_status_callback([](const PrintBase::SlicingStatus&) {});
print.apply(m, cfg);
print.process();
ThumbnailsList thumbnails;
auto outputfname = std::string("output_") + pname + "." + entry.ext;
print.export_print(outputfname, thumbnails, pname);
// Not much can be checked about the archives...
REQUIRE(boost::filesystem::exists(outputfname));
double vol_written = m.mesh().volume();
if (entry.rdfactoryfn) {
INFO(std::string("Testing archive type: ") + entry.id + " -- reading back...");
indexed_triangle_set its;
DynamicPrintConfig cfg;
try {
// Leave format_id deliberetaly empty, guessing should always
// work here.
import_sla_archive(outputfname, "", its, cfg);
} catch (...) {
REQUIRE(false);
}
// its_write_obj(its, (outputfname + ".obj").c_str());
REQUIRE(!cfg.empty());
REQUIRE(!its.empty());
double vol_read = its_volume(its);
double rel_err = std::abs(vol_written - vol_read) / vol_written;
REQUIRE(rel_err < 0.1);
}
}
}

View File

@@ -0,0 +1,273 @@
#include <unordered_map>
#include <random>
#include <numeric>
#include <cstdint>
#include "sla_test_utils.hpp"
#include <libslic3r/TriangleMeshSlicer.hpp>
#include <libslic3r/SLA/SupportTreeMesher.hpp>
#include <libslic3r/BranchingTree/PointCloud.hpp>
namespace {
const char *const BELOW_PAD_TEST_OBJECTS[] = {
"20mm_cube.obj",
"V.obj",
};
const char *const AROUND_PAD_TEST_OBJECTS[] = {
"20mm_cube.obj",
"V.obj",
"frog_legs.obj",
"cube_with_concave_hole_enlarged.obj",
};
const char *const SUPPORT_TEST_MODELS[] = {
"cube_with_concave_hole_enlarged_standing.obj",
"A_upsidedown.obj",
"extruder_idler.obj"
};
} // namespace
TEST_CASE("Support point generator should be deterministic if seeded",
"[SLASupportGeneration], [SLAPointGen]") {
TriangleMesh mesh = load_model("A_upsidedown.obj");
AABBMesh emesh{mesh};
sla::SupportTreeConfig supportcfg;
sla::SupportPointGenerator::Config autogencfg;
autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm);
sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}};
auto bb = mesh.bounding_box();
double zmin = bb.min.z();
double zmax = bb.max.z();
double gnd = zmin - supportcfg.object_elevation_mm;
auto layer_h = 0.05f;
auto slicegrid = grid(float(gnd), float(zmax), layer_h);
std::vector<ExPolygons> slices = slice_mesh_ex(mesh.its, slicegrid, CLOSING_RADIUS);
point_gen.seed(0);
point_gen.execute(slices, slicegrid);
auto get_chksum = [](const std::vector<sla::SupportPoint> &pts){
int64_t chksum = 0;
for (auto &pt : pts) {
auto p = scaled(pt.pos);
chksum += p.x() + p.y() + p.z();
}
return chksum;
};
int64_t checksum = get_chksum(point_gen.output());
size_t ptnum = point_gen.output().size();
REQUIRE(point_gen.output().size() > 0);
for (int i = 0; i < 20; ++i) {
point_gen.output().clear();
point_gen.seed(0);
point_gen.execute(slices, slicegrid);
REQUIRE(point_gen.output().size() == ptnum);
REQUIRE(checksum == get_chksum(point_gen.output()));
}
}
TEST_CASE("Flat pad geometry is valid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Disable wings
padcfg.wall_height_mm = .0;
for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("WingedPadGeometryIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 1.;
for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("FlatPadAroundObjectIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 0.;
// padcfg.embed_object.stick_stride_mm = 0.;
padcfg.embed_object.enabled = true;
padcfg.embed_object.everywhere = true;
for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("WingedPadAroundObjectIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 1.;
padcfg.embed_object.enabled = true;
padcfg.embed_object.everywhere = true;
for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("DefaultSupports::ElevatedSupportGeometryIsValid", "[SLASupportGeneration]") {
sla::SupportTreeConfig supportcfg;
supportcfg.object_elevation_mm = 10.;
for (auto fname : SUPPORT_TEST_MODELS) test_supports(fname, supportcfg);
}
TEST_CASE("DefaultSupports::FloorSupportGeometryIsValid", "[SLASupportGeneration]") {
sla::SupportTreeConfig supportcfg;
supportcfg.object_elevation_mm = 0;
for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg);
}
TEST_CASE("DefaultSupports::ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration]") {
sla::SupportTreeConfig supportcfg;
supportcfg.object_elevation_mm = 10.;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
TEST_CASE("DefaultSupports::FloorSupportsDoNotPierceModel", "[SLASupportGeneration]") {
sla::SupportTreeConfig supportcfg;
supportcfg.object_elevation_mm = 0;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
//TEST_CASE("BranchingSupports::ElevatedSupportGeometryIsValid", "[SLASupportGeneration][Branching]") {
// sla::SupportTreeConfig supportcfg;
// supportcfg.object_elevation_mm = 10.;
// supportcfg.tree_type = sla::SupportTreeType::Branching;
// for (auto fname : SUPPORT_TEST_MODELS) test_supports(fname, supportcfg);
//}
//TEST_CASE("BranchingSupports::FloorSupportGeometryIsValid", "[SLASupportGeneration][Branching]") {
// sla::SupportTreeConfig supportcfg;
// supportcfg.object_elevation_mm = 0;
// supportcfg.tree_type = sla::SupportTreeType::Branching;
// for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg);
//}
TEST_CASE("BranchingSupports::ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration][Branching]") {
sla::SupportTreeConfig supportcfg;
supportcfg.object_elevation_mm = 10.;
supportcfg.tree_type = sla::SupportTreeType::Branching;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
TEST_CASE("BranchingSupports::FloorSupportsDoNotPierceModel", "[SLASupportGeneration][Branching]") {
sla::SupportTreeConfig supportcfg;
supportcfg.object_elevation_mm = 0;
supportcfg.tree_type = sla::SupportTreeType::Branching;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
TEST_CASE("InitializedRasterShouldBeNONEmpty", "[SLARasterOutput]") {
// Default QIDI SL1 display parameters
sla::Resolution res{2560, 1440};
sla::PixelDim pixdim{120. / res.width_px, 68. / res.height_px};
sla::RasterGrayscaleAAGammaPower raster(res, pixdim, {}, 1.);
REQUIRE(raster.resolution().width_px == res.width_px);
REQUIRE(raster.resolution().height_px == res.height_px);
REQUIRE(raster.pixel_dimensions().w_mm == Approx(pixdim.w_mm));
REQUIRE(raster.pixel_dimensions().h_mm == Approx(pixdim.h_mm));
}
TEST_CASE("MirroringShouldBeCorrect", "[SLARasterOutput]") {
sla::RasterBase::TMirroring mirrorings[] = {sla::RasterBase::NoMirror,
sla::RasterBase::MirrorX,
sla::RasterBase::MirrorY,
sla::RasterBase::MirrorXY};
sla::RasterBase::Orientation orientations[] =
{sla::RasterBase::roLandscape, sla::RasterBase::roPortrait};
for (auto orientation : orientations)
for (auto &mirror : mirrorings)
check_raster_transformations(orientation, mirror);
}
TEST_CASE("RasterizedPolygonAreaShouldMatch", "[SLARasterOutput]") {
double disp_w = 120., disp_h = 68.;
sla::Resolution res{2560, 1440};
sla::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
double gamma = 1.;
sla::RasterGrayscaleAAGammaPower raster(res, pixdim, {}, gamma);
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
ExPolygon poly = square_with_hole(10.);
poly.translate(bb.center().x(), bb.center().y());
raster.draw(poly);
double a = poly.area() / (scaled<double>(1.) * scaled(1.));
double ra = raster_white_area(raster);
double diff = std::abs(a - ra);
REQUIRE(diff <= predict_error(poly, pixdim));
raster.clear();
poly = square_with_hole(60.);
poly.translate(bb.center().x(), bb.center().y());
raster.draw(poly);
a = poly.area() / (scaled<double>(1.) * scaled(1.));
ra = raster_white_area(raster);
diff = std::abs(a - ra);
REQUIRE(diff <= predict_error(poly, pixdim));
sla::RasterGrayscaleAA raster0(res, pixdim, {}, [](double) { return 0.; });
REQUIRE(raster_pxsum(raster0) == 0);
raster0.draw(poly);
ra = raster_white_area(raster);
REQUIRE(raster_pxsum(raster0) == 0);
}
TEST_CASE("halfcone test", "[halfcone]") {
sla::DiffBridge br{Vec3d{1., 1., 1.}, Vec3d{10., 10., 10.}, 0.25, 0.5};
indexed_triangle_set m = sla::get_mesh(br, 45);
its_merge_vertices(m);
its_write_obj(m, "Halfcone.obj");
}
TEST_CASE("Test concurrency")
{
std::vector<double> vals = grid(0., 100., 10.);
double ref = std::accumulate(vals.begin(), vals.end(), 0.);
double s = execution::accumulate(ex_tbb, vals.begin(), vals.end(), 0.);
REQUIRE(s == Approx(ref));
}

View File

@@ -0,0 +1 @@
#include <catch_main.hpp>

View File

@@ -0,0 +1,96 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/AABBMesh.hpp>
#include <libslic3r/SLA/Hollowing.hpp>
#include "sla_test_utils.hpp"
using namespace Slic3r;
// First do a simple test of the hole raycaster.
TEST_CASE("Raycaster - find intersections of a line and cylinder")
{
sla::DrainHole hole{Vec3f(0,0,0), Vec3f(0,0,1), 5, 10};
std::array<std::pair<float, Vec3d>, 2> out;
Vec3f s;
Vec3f dir;
// Start inside the hole and cast perpendicular to its axis.
s = {-1.f, 0, 5.f};
dir = {1.f, 0, 0};
hole.get_intersections(s, dir, out);
REQUIRE(out[0].first == Approx(-4.f));
REQUIRE(out[1].first == Approx(6.f));
// Start outside and cast parallel to axis.
s = {0, 0, -1.f};
dir = {0, 0, 1.f};
hole.get_intersections(s, dir, out);
REQUIRE(std::abs(out[0].first - 1.f) < 0.001f);
REQUIRE(std::abs(out[1].first - 11.f) < 0.001f);
// Start outside and cast so that entry is in base and exit on the cylinder
s = {0, -1.f, -1.f};
dir = {0, 1.f, 1.f};
dir.normalize();
hole.get_intersections(s, dir, out);
REQUIRE(std::abs(out[0].first - std::sqrt(2.f)) < 0.001f);
REQUIRE(std::abs(out[1].first - std::sqrt(72.f)) < 0.001f);
}
#ifdef SLIC3R_HOLE_RAYCASTER
// Create a simple scene with a 20mm cube and a big hole in the front wall
// with 5mm radius. Then shoot rays from interesting positions and see where
// they land.
TEST_CASE("Raycaster with loaded drillholes", "[sla_raycast]")
{
// Load the cube and make it hollow.
TriangleMesh cube = load_model("20mm_cube.obj");
sla::HollowingConfig hcfg;
std::unique_ptr<TriangleMesh> cube_inside = sla::generate_interior(cube, hcfg);
REQUIRE(cube_inside);
// Helper bb
auto boxbb = cube.bounding_box();
// Create the big 10mm long drainhole in the front wall.
Vec3f center = boxbb.center().cast<float>();
Vec3f p = {center.x(), 0., center.z()};
Vec3f normal = {0.f, 1.f, 0.f};
float radius = 5.f;
float hole_length = 10.;
sla::DrainHoles holes = { sla::DrainHole{p, normal, radius, hole_length} };
cube.merge(*cube_inside);
sla::IndexedMesh emesh{cube};
emesh.load_holes(holes);
Vec3d s = center.cast<double>();
// Fire from center, should hit the interior wall
auto hit = emesh.query_ray_hit(s, {0, 1., 0.});
REQUIRE(hit.distance() == Approx(boxbb.size().x() / 2 - hcfg.min_thickness));
// Fire upward from hole center, hit distance equals the radius (hits the
// side of the hole cut.
s.y() = hcfg.min_thickness / 2;
hit = emesh.query_ray_hit(s, {0, 0., 1.});
REQUIRE(hit.distance() == Approx(radius));
// Fire from outside, hit the back side of the cube interior
s.y() = -1.;
hit = emesh.query_ray_hit(s, {0, 1., 0.});
REQUIRE(hit.distance() == Approx(boxbb.max.y() - hcfg.min_thickness - s.y()));
// Fire downwards from above the hole cylinder. Has to go through the cyl.
// as it was not there.
s = center.cast<double>();
s.z() = boxbb.max.z() - hcfg.min_thickness - 1.;
hit = emesh.query_ray_hit(s, {0, 0., -1.});
REQUIRE(hit.distance() == Approx(s.z() - boxbb.min.z() - hcfg.min_thickness));
// Check for support tree correctness
test_support_model_collision("20mm_cube.obj", {}, hcfg, holes);
}
#endif

View File

@@ -0,0 +1,142 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <libslic3r/ExPolygon.hpp>
#include <libslic3r/BoundingBox.hpp>
#include <libslic3r/SLA/SpatIndex.hpp>
#include "sla_test_utils.hpp"
namespace Slic3r { namespace sla {
TEST_CASE("Overhanging point should be supported", "[SupGen]") {
// Pyramid with 45 deg slope
TriangleMesh mesh = make_pyramid(10.f, 10.f);
mesh.rotate_y(float(PI));
mesh.WriteOBJFile("Pyramid.obj");
sla::SupportPoints pts = calc_support_pts(mesh);
// The overhang, which is the upside-down pyramid's edge
Vec3f overh{0., 0., -10.};
REQUIRE(!pts.empty());
float dist = (overh - pts.front().pos).norm();
for (const auto &pt : pts)
dist = std::min(dist, (overh - pt.pos).norm());
// Should require exactly one support point at the overhang
REQUIRE(pts.size() > 0);
REQUIRE(dist < 1.f);
}
double min_point_distance(const sla::SupportPoints &pts)
{
sla::PointIndex index;
for (size_t i = 0; i < pts.size(); ++i)
index.insert(pts[i].pos.cast<double>(), i);
auto d = std::numeric_limits<double>::max();
index.foreach([&d, &index](const sla::PointIndexEl &el) {
auto res = index.nearest(el.first, 2);
for (const sla::PointIndexEl &r : res)
if (r.second != el.second)
d = std::min(d, (el.first - r.first).norm());
});
return d;
}
TEST_CASE("Overhanging horizontal surface should be supported", "[SupGen]") {
double width = 10., depth = 10., height = 1.;
TriangleMesh mesh = make_cube(width, depth, height);
mesh.translate(0., 0., 5.); // lift up
mesh.WriteOBJFile("Cuboid.obj");
sla::SupportPointGenerator::Config cfg;
sla::SupportPoints pts = calc_support_pts(mesh, cfg);
double mm2 = width * depth;
REQUIRE(!pts.empty());
REQUIRE(pts.size() * cfg.support_force() > mm2 * cfg.tear_pressure());
REQUIRE(min_point_distance(pts) >= cfg.minimal_distance);
}
template<class M> auto&& center_around_bb(M &&mesh)
{
auto bb = mesh.bounding_box();
mesh.translate(-bb.center().template cast<float>());
return std::forward<M>(mesh);
}
TEST_CASE("Overhanging edge should be supported", "[SupGen]") {
float width = 10.f, depth = 10.f, height = 5.f;
TriangleMesh mesh = make_prism(width, depth, height);
mesh.rotate_y(float(PI)); // rotate on its back
mesh.translate(0., 0., height);
mesh.WriteOBJFile("Prism.obj");
sla::SupportPointGenerator::Config cfg;
sla::SupportPoints pts = calc_support_pts(mesh, cfg);
Linef3 overh{ {0.f, -depth / 2.f, 0.f}, {0.f, depth / 2.f, 0.f}};
// Get all the points closer that 1 mm to the overhanging edge:
sla::SupportPoints overh_pts; overh_pts.reserve(pts.size());
std::copy_if(pts.begin(), pts.end(), std::back_inserter(overh_pts),
[&overh](const sla::SupportPoint &pt){
return line_alg::distance_to(overh, Vec3d{pt.pos.cast<double>()}) < 1.;
});
REQUIRE(overh_pts.size() * cfg.support_force() > overh.length() * cfg.tear_pressure());
double ddiff = min_point_distance(pts) - cfg.minimal_distance;
REQUIRE(ddiff > - 0.1 * cfg.minimal_distance);
}
TEST_CASE("Hollowed cube should be supported from the inside", "[SupGen][Hollowed]") {
TriangleMesh mesh = make_cube(20., 20., 20.);
hollow_mesh(mesh, HollowingConfig{});
mesh.WriteOBJFile("cube_hollowed.obj");
auto bb = mesh.bounding_box();
auto h = float(bb.max.z() - bb.min.z());
Vec3f mv = bb.center().cast<float>() - Vec3f{0.f, 0.f, 0.5f * h};
mesh.translate(-mv);
sla::SupportPointGenerator::Config cfg;
sla::SupportPoints pts = calc_support_pts(mesh, cfg);
sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON);
REQUIRE(!pts.empty());
}
TEST_CASE("Two parallel plates should be supported", "[SupGen][Hollowed]")
{
double width = 20., depth = 20., height = 1.;
TriangleMesh mesh = center_around_bb(make_cube(width + 5., depth + 5., height));
TriangleMesh mesh_high = center_around_bb(make_cube(width, depth, height));
mesh_high.translate(0., 0., 10.); // lift up
mesh.merge(mesh_high);
mesh.WriteOBJFile("parallel_plates.obj");
sla::SupportPointGenerator::Config cfg;
sla::SupportPoints pts = calc_support_pts(mesh, cfg);
sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON);
REQUIRE(!pts.empty());
}
}} // namespace Slic3r::sla

View File

@@ -0,0 +1,377 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include <unordered_set>
#include "libslic3r/Execution/ExecutionSeq.hpp"
#include "libslic3r/SLA/SupportTreeUtils.hpp"
#include "libslic3r/SLA/SupportTreeUtilsLegacy.hpp"
// Test pair hash for 'nums' random number pairs.
template <class I, class II> void test_pairhash()
{
const constexpr size_t nums = 1000;
I A[nums] = {0}, B[nums] = {0};
std::unordered_set<I> CH;
std::unordered_map<II, std::pair<I, I>> ints;
std::random_device rd;
std::mt19937 gen(rd());
const I Ibits = int(sizeof(I) * CHAR_BIT);
const II IIbits = int(sizeof(II) * CHAR_BIT);
int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits;
if (std::is_signed<I>::value) bits -= 1;
const I Imin = 0;
const I Imax = I(std::pow(2., bits) - 1);
std::uniform_int_distribution<I> dis(Imin, Imax);
for (size_t i = 0; i < nums;) {
I a = dis(gen);
if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; }
}
for (size_t i = 0; i < nums;) {
I b = dis(gen);
if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; }
}
for (size_t i = 0; i < nums; ++i) {
I a = A[i], b = B[i];
REQUIRE(a != b);
II hash_ab = Slic3r::sla::pairhash<I, II>(a, b);
II hash_ba = Slic3r::sla::pairhash<I, II>(b, a);
REQUIRE(hash_ab == hash_ba);
auto it = ints.find(hash_ab);
if (it != ints.end()) {
REQUIRE((
(it->second.first == a && it->second.second == b) ||
(it->second.first == b && it->second.second == a)
));
} else
ints[hash_ab] = std::make_pair(a, b);
}
}
TEST_CASE("Pillar pairhash should be unique", "[suptreeutils]") {
test_pairhash<int, int>();
test_pairhash<int, long>();
test_pairhash<unsigned, unsigned>();
test_pairhash<unsigned, unsigned long>();
}
static void eval_ground_conn(const Slic3r::sla::GroundConnection &conn,
const Slic3r::sla::SupportableMesh &sm,
const Slic3r::sla::Junction &j,
double end_r,
const std::string &stl_fname = "output.stl")
{
using namespace Slic3r;
//#ifndef NDEBUG
sla::SupportTreeBuilder builder;
if (!conn)
builder.add_junction(j);
sla::build_ground_connection(builder, sm, conn);
indexed_triangle_set mesh = *sm.emesh.get_triangle_mesh();
its_merge(mesh, builder.merged_mesh());
its_write_stl_ascii(stl_fname.c_str(), "stl_fname", mesh);
//#endif
REQUIRE(bool(conn));
// The route should include the source and one avoidance junction.
REQUIRE(conn.path.size() == 2);
// Check if the radius increases with each node
REQUIRE(conn.path.front().r < conn.path.back().r);
REQUIRE(conn.path.back().r < conn.pillar_base->r_top);
// The end radius and the pillar base's upper radius should match
REQUIRE(conn.pillar_base->r_top == Approx(end_r));
}
TEST_CASE("Pillar search dumb case", "[suptreeutils]") {
using namespace Slic3r;
constexpr double FromR = 0.5;
auto j = sla::Junction{Vec3d::Zero(), FromR};
SECTION("with empty mesh") {
sla::SupportableMesh sm{indexed_triangle_set{},
sla::SupportPoints{},
sla::SupportTreeConfig{}};
constexpr double EndR = 1.;
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN);
REQUIRE(conn);
// REQUIRE(conn.path.size() == 1);
REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm)));
}
SECTION("with zero R source and destination") {
sla::SupportableMesh sm{indexed_triangle_set{},
sla::SupportPoints{},
sla::SupportTreeConfig{}};
j.r = 0.;
constexpr double EndR = 0.;
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN);
REQUIRE(conn);
// REQUIRE(conn.path.size() == 1);
REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm)));
REQUIRE(conn.pillar_base->r_top == Approx(0.));
}
SECTION("with zero init direction") {
sla::SupportableMesh sm{indexed_triangle_set{},
sla::SupportPoints{},
sla::SupportTreeConfig{}};
constexpr double EndR = 1.;
Vec3d init_dir = Vec3d::Zero();
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, init_dir);
REQUIRE(conn);
// REQUIRE(conn.path.size() == 1);
REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm)));
}
}
TEST_CASE("Avoid disk below junction", "[suptreeutils]")
{
// In this test there will be a disk mesh with some radius, centered at
// (0, 0, 0) and above the disk, a junction from which the support pillar
// should be routed. The algorithm needs to find an avoidance route.
using namespace Slic3r;
constexpr double FromRadius = .5;
constexpr double EndRadius = 1.;
constexpr double CylRadius = 4.;
constexpr double CylHeight = 1.;
sla::SupportTreeConfig cfg;
indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight);
// 2.5 * CyRadius height should be enough to be able to insert a bridge
// with 45 degree tilt above the disk.
sla::Junction j{Vec3d{0., 0., 2.5 * CylRadius}, FromRadius};
sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg};
SECTION("with elevation") {
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN);
eval_ground_conn(conn, sm, j, EndRadius, "disk.stl");
// Check if the avoidance junction is indeed outside of the disk barrier's
// edge.
auto p = conn.path.back().pos;
double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y());
REQUIRE(pR + FromRadius > CylRadius);
}
SECTION("without elevation") {
sm.cfg.object_elevation_mm = 0.;
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_tbb, sm, j, EndRadius, sla::DOWN);
eval_ground_conn(conn, sm, j, EndRadius, "disk_ze.stl");
// Check if the avoidance junction is indeed outside of the disk barrier's
// edge.
auto p = conn.path.back().pos;
double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y());
REQUIRE(pR + FromRadius > CylRadius);
}
}
TEST_CASE("Avoid disk below junction with barrier on the side", "[suptreeutils]")
{
// In this test there will be a disk mesh with some radius, centered at
// (0, 0, 0) and above the disk, a junction from which the support pillar
// should be routed. The algorithm needs to find an avoidance route.
using namespace Slic3r;
constexpr double FromRadius = .5;
constexpr double EndRadius = 1.;
constexpr double CylRadius = 4.;
constexpr double CylHeight = 1.;
constexpr double JElevX = 2.5;
sla::SupportTreeConfig cfg;
indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight);
indexed_triangle_set wall = its_make_cube(1., 2 * CylRadius, JElevX * CylRadius);
its_translate(wall, Vec3f{float(FromRadius), -float(CylRadius), 0.f});
its_merge(disk, wall);
// 2.5 * CyRadius height should be enough to be able to insert a bridge
// with 45 degree tilt above the disk.
sla::Junction j{Vec3d{0., 0., JElevX * CylRadius}, FromRadius};
sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg};
SECTION("with elevation") {
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN);
eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier.stl");
// Check if the avoidance junction is indeed outside of the disk barrier's
// edge.
auto p = conn.path.back().pos;
double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y());
REQUIRE(pR + FromRadius > CylRadius);
}
SECTION("without elevation") {
sm.cfg.object_elevation_mm = 0.;
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN);
eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier_ze.stl");
// Check if the avoidance junction is indeed outside of the disk barrier's
// edge.
auto p = conn.path.back().pos;
double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y());
REQUIRE(pR + FromRadius > CylRadius);
}
}
TEST_CASE("Find ground route just above ground", "[suptreeutils]") {
using namespace Slic3r;
sla::SupportTreeConfig cfg;
cfg.object_elevation_mm = 0.;
sla::Junction j{Vec3d{0., 0., 2. * cfg.head_back_radius_mm}, cfg.head_back_radius_mm};
sla::SupportableMesh sm{{}, sla::SupportPoints{}, cfg};
sla::GroundConnection conn =
sla::deepsearch_ground_connection(ex_seq, sm, j, Geometry::spheric_to_dir(3 * PI/ 4, PI));
REQUIRE(conn);
REQUIRE(conn.pillar_base->pos.z() >= Approx(ground_level(sm)));
}
TEST_CASE("BranchingSupports::MergePointFinder", "[suptreeutils]") {
using namespace Slic3r;
SECTION("Identical points have the same merge point") {
Vec3f a{0.f, 0.f, 0.f}, b = a;
auto slope = float(PI / 4.);
auto mergept = sla::find_merge_pt(a, b, slope);
REQUIRE(bool(mergept));
REQUIRE((*mergept - b).norm() < EPSILON);
REQUIRE((*mergept - a).norm() < EPSILON);
}
// ^ Z
// | a *
// |
// | b * <= mergept
SECTION("Points at different heights have the lower point as mergepoint") {
Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 0.f, -1.f};
auto slope = float(PI / 4.);
auto mergept = sla::find_merge_pt(a, b, slope);
REQUIRE(bool(mergept));
REQUIRE((*mergept - b).squaredNorm() < 2 * EPSILON);
}
// -|---------> X
// a b
// * *
// * <= mergept
SECTION("Points at different X have mergept in the middle at lower Z") {
Vec3f a{0.f, 0.f, 0.f}, b = {1.f, 0.f, 0.f};
auto slope = float(PI / 4.);
auto mergept = sla::find_merge_pt(a, b, slope);
REQUIRE(bool(mergept));
// Distance of mergept should be equal from both input points
float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm());
REQUIRE(D < EPSILON);
REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope));
REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope));
}
// -|---------> Y
// a b
// * *
// * <= mergept
SECTION("Points at different Y have mergept in the middle at lower Z") {
Vec3f a{0.f, 0.f, 0.f}, b = {0.f, 1.f, 0.f};
auto slope = float(PI / 4.);
auto mergept = sla::find_merge_pt(a, b, slope);
REQUIRE(bool(mergept));
// Distance of mergept should be equal from both input points
float D = std::abs((*mergept - b).squaredNorm() - (*mergept - a).squaredNorm());
REQUIRE(D < EPSILON);
REQUIRE(!sla::is_outside_support_cone(a, *mergept, slope));
REQUIRE(!sla::is_outside_support_cone(b, *mergept, slope));
}
SECTION("Points separated by less than critical angle have the lower point as mergept") {
Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -2.f};
auto slope = float(PI / 4.);
auto mergept = sla::find_merge_pt(a, b, slope);
REQUIRE(bool(mergept));
REQUIRE((*mergept - b).norm() < 2 * EPSILON);
}
// -|----------------------------> Y
// a b
// * * <= mergept *
//
SECTION("Points at same height have mergepoint in the middle if critical angle is zero ") {
Vec3f a{-1.f, -1.f, -1.f}, b = {-1.5f, -1.5f, -1.f};
auto slope = EPSILON;
auto mergept = sla::find_merge_pt(a, b, slope);
REQUIRE(bool(mergept));
Vec3f middle = (b + a) / 2.;
REQUIRE((*mergept - middle).norm() < 4 * EPSILON);
}
}

View File

@@ -0,0 +1,484 @@
#include "sla_test_utils.hpp"
#include "libslic3r/TriangleMeshSlicer.hpp"
#include "libslic3r/SLA/AGGRaster.hpp"
#include "libslic3r/SLA/DefaultSupportTree.hpp"
#include "libslic3r/SLA/BranchingTreeSLA.hpp"
#include <iomanip>
void test_support_model_collision(
const std::string &obj_filename,
const sla::SupportTreeConfig &input_supportcfg,
const sla::HollowingConfig &hollowingcfg,
const sla::DrainHoles &drainholes)
{
SupportByproducts byproducts;
sla::SupportTreeConfig supportcfg = input_supportcfg;
// Set head penetration to a small negative value which should ensure that
// the supports will not touch the model body.
supportcfg.head_penetration_mm = -0.2;
test_supports(obj_filename, supportcfg, hollowingcfg, drainholes, byproducts);
// Slice the support mesh given the slice grid of the model.
std::vector<ExPolygons> support_slices =
sla::slice(byproducts.suptree_builder.retrieve_mesh(sla::MeshType::Support),
byproducts.suptree_builder.retrieve_mesh(sla::MeshType::Pad),
byproducts.slicegrid, CLOSING_RADIUS, {});
// The slices originate from the same slice grid so the numbers must match
bool support_mesh_is_empty =
byproducts.suptree_builder.retrieve_mesh(sla::MeshType::Pad).empty() &&
byproducts.suptree_builder.retrieve_mesh(sla::MeshType::Support).empty();
if (support_mesh_is_empty)
REQUIRE(support_slices.empty());
else
REQUIRE(support_slices.size() == byproducts.model_slices.size());
bool notouch = true;
for (size_t n = 0; notouch && n < support_slices.size(); ++n) {
const ExPolygons &sup_slice = support_slices[n];
const ExPolygons &mod_slice = byproducts.model_slices[n];
Polygons intersections = intersection(sup_slice, mod_slice);
double pinhead_r = scaled(input_supportcfg.head_front_radius_mm);
// TODO:: make it strict without a threshold of PI * pihead_radius ^ 2
notouch = notouch && area(intersections) < PI * pinhead_r * pinhead_r;
}
if (!notouch)
export_failed_case(support_slices, byproducts);
REQUIRE(notouch);
}
void export_failed_case(const std::vector<ExPolygons> &support_slices, const SupportByproducts &byproducts)
{
bool do_export_stl = false;
for (size_t n = 0; n < support_slices.size(); ++n) {
const ExPolygons &sup_slice = support_slices[n];
const ExPolygons &mod_slice = byproducts.model_slices[n];
Polygons intersections = intersection(sup_slice, mod_slice);
std::stringstream ss;
if (!intersections.empty()) {
ss << byproducts.obj_fname << std::setprecision(4) << n << ".svg";
SVG svg(ss.str());
svg.draw(sup_slice, "green");
svg.draw(mod_slice, "blue");
svg.draw(intersections, "red");
svg.Close();
}
do_export_stl = do_export_stl || !intersections.empty();
}
if (do_export_stl) {
indexed_triangle_set its;
byproducts.suptree_builder.retrieve_full_mesh(its);
TriangleMesh m{its};
m.merge(byproducts.input_mesh);
m.WriteOBJFile((Catch::getResultCapture().getCurrentTestName() + "_" +
byproducts.obj_fname).c_str());
}
}
void test_supports(const std::string &obj_filename,
const sla::SupportTreeConfig &supportcfg,
const sla::HollowingConfig &hollowingcfg,
const sla::DrainHoles &drainholes,
SupportByproducts &out)
{
using namespace Slic3r;
TriangleMesh mesh = load_model(obj_filename);
REQUIRE_FALSE(mesh.empty());
if (hollowingcfg.enabled) {
sla::InteriorPtr interior = sla::generate_interior(mesh.its, hollowingcfg);
REQUIRE(interior);
mesh.merge(TriangleMesh{sla::get_mesh(*interior)});
}
auto bb = mesh.bounding_box();
double zmin = bb.min.z();
double zmax = bb.max.z();
double gnd = zmin - supportcfg.object_elevation_mm;
auto layer_h = 0.05f;
out.slicegrid = grid(float(gnd), float(zmax), layer_h);
out.model_slices = slice_mesh_ex(mesh.its, out.slicegrid, CLOSING_RADIUS);
sla::cut_drainholes(out.model_slices, out.slicegrid, CLOSING_RADIUS, drainholes, []{});
// Create the special index-triangle mesh with spatial indexing which
// is the input of the support point and support mesh generators
sla::SupportableMesh sm{mesh.its, {}, supportcfg};
#ifdef SLIC3R_HOLE_RAYCASTER
if (hollowingcfg.enabled)
emesh.load_holes(drainholes);
#endif
// TODO: do the cgal hole cutting...
// Create the support point generator
sla::SupportPointGenerator::Config autogencfg;
autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm);
sla::SupportPointGenerator point_gen{sm.emesh, autogencfg, [] {}, [](int) {}};
point_gen.seed(0); // Make the test repeatable
point_gen.execute(out.model_slices, out.slicegrid);
// Get the calculated support points.
sm.pts = point_gen.output();
int validityflags = ASSUME_NO_REPAIR;
// If there is no elevation, support points shall be removed from the
// bottom of the object.
if (std::abs(supportcfg.object_elevation_mm) < EPSILON) {
sla::remove_bottom_points(sm.pts, zmin + supportcfg.base_height_mm);
} else {
// Should be support points at least on the bottom of the model
REQUIRE_FALSE(sm.pts.empty());
// Also the support mesh should not be empty.
validityflags |= ASSUME_NO_EMPTY;
}
// Generate the actual support tree
sla::SupportTreeBuilder treebuilder;
switch (sm.cfg.tree_type) {
case sla::SupportTreeType::Default: {
sla::DefaultSupportTree::execute(treebuilder, sm);
check_support_tree_integrity(treebuilder, supportcfg, sla::ground_level(sm));
break;
}
case sla::SupportTreeType::Branching: {
create_branching_tree(treebuilder, sm);
// TODO: check_support_tree_integrity(treebuilder, supportcfg);
break;
}
default:;
}
TriangleMesh output_mesh{treebuilder.retrieve_mesh(sla::MeshType::Support)};
check_validity(output_mesh, validityflags);
// Quick check if the dimensions and placement of supports are correct
auto obb = output_mesh.bounding_box();
double allowed_zmin = zmin - supportcfg.object_elevation_mm;
if (std::abs(supportcfg.object_elevation_mm) < EPSILON)
allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm;
#ifndef NDEBUG
if (!(obb.min.z() >= Approx(allowed_zmin)) || !(obb.max.z() <= Approx(zmax)))
{
indexed_triangle_set its;
treebuilder.retrieve_full_mesh(its);
TriangleMesh m{its};
m.merge(mesh);
m.WriteOBJFile((Catch::getResultCapture().getCurrentTestName() + "_" +
obj_filename).c_str());
}
#endif
REQUIRE(obb.min.z() >= Approx(allowed_zmin));
REQUIRE(obb.max.z() <= Approx(zmax));
// Move out the support tree into the byproducts, we can examine it further
// in various tests.
out.obj_fname = std::move(obj_filename);
out.suptree_builder = std::move(treebuilder);
out.input_mesh = std::move(mesh);
}
void check_support_tree_integrity(const sla::SupportTreeBuilder &stree,
const sla::SupportTreeConfig &cfg,
double gnd)
{
double H1 = cfg.max_solo_pillar_height_mm;
double H2 = cfg.max_dual_pillar_height_mm;
for (const sla::Head &head : stree.heads()) {
REQUIRE((!head.is_valid() || head.pillar_id != sla::SupportTreeNode::ID_UNSET ||
head.bridge_id != sla::SupportTreeNode::ID_UNSET));
}
for (const sla::Pillar &pillar : stree.pillars()) {
if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) {
double h = pillar.height;
if (h > H1) REQUIRE(pillar.links >= 1);
else if(h > H2) { REQUIRE(pillar.links >= 2); }
}
REQUIRE(pillar.links <= cfg.pillar_cascade_neighbors);
REQUIRE(pillar.bridges <= cfg.max_bridges_on_pillar);
}
double max_bridgelen = 0.;
auto chck_bridge = [&cfg](const sla::Bridge &bridge, double &max_brlen) {
Vec3d n = bridge.endp - bridge.startp;
double d = sla::distance(n);
max_brlen = std::max(d, max_brlen);
double z = n.z();
double polar = std::acos(z / d);
double slope = -polar + PI / 2.;
REQUIRE(std::abs(slope) >= cfg.bridge_slope - EPSILON);
};
for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen);
REQUIRE(max_bridgelen <= Approx(cfg.max_bridge_length_mm));
max_bridgelen = 0;
for (auto &bridge : stree.crossbridges()) chck_bridge(bridge, max_bridgelen);
double md = cfg.max_pillar_link_distance_mm / std::cos(-cfg.bridge_slope);
REQUIRE(max_bridgelen <= md);
}
void test_pad(const std::string &obj_filename, const sla::PadConfig &padcfg, PadByproducts &out)
{
REQUIRE(padcfg.validate().empty());
TriangleMesh mesh = load_model(obj_filename);
REQUIRE_FALSE(mesh.empty());
// Create pad skeleton only from the model
Slic3r::sla::pad_blueprint(mesh.its, out.model_contours);
test_concave_hull(out.model_contours);
REQUIRE_FALSE(out.model_contours.empty());
// Create the pad geometry for the model contours only
indexed_triangle_set out_its;
Slic3r::sla::create_pad({}, out.model_contours, out_its, padcfg);
out.mesh = TriangleMesh{out_its};
check_validity(out.mesh);
auto bb = out.mesh.bounding_box();
REQUIRE(bb.max.z() - bb.min.z() == Approx(padcfg.full_height()));
}
static void _test_concave_hull(const Polygons &hull, const ExPolygons &polys)
{
REQUIRE(polys.size() >=hull.size());
double polys_area = 0;
for (const ExPolygon &p : polys) polys_area += p.area();
double cchull_area = 0;
for (const Slic3r::Polygon &p : hull) cchull_area += p.area();
REQUIRE(cchull_area >= Approx(polys_area));
size_t cchull_holes = 0;
for (const Slic3r::Polygon &p : hull)
cchull_holes += p.is_clockwise() ? 1 : 0;
REQUIRE(cchull_holes == 0);
Polygons diff_poly = diff(to_polygons(polys), hull);
if (!diff_poly.empty()) {
BOOST_LOG_TRIVIAL(warning)
<< "Concave hull diff with original shape is not completely empty."
<< "See pad_chull.svg for details.";
SVG svg("pad_chull.svg");
svg.draw(polys, "green");
svg.draw(hull, "red");
svg.draw(diff_poly, "blue");
}
double diff_area = area(diff_poly);
REQUIRE(std::abs(diff_area) < std::pow(scaled(2 * EPSILON), 2));
}
void test_concave_hull(const ExPolygons &polys) {
sla::PadConfig pcfg;
Slic3r::sla::ConcaveHull cchull{polys, pcfg.max_merge_dist_mm, []{}};
_test_concave_hull(cchull.polygons(), polys);
coord_t delta = scaled(pcfg.brim_size_mm + pcfg.wing_distance());
ExPolygons wafflex = sla::offset_waffle_style_ex(cchull, delta);
Polygons waffl = sla::offset_waffle_style(cchull, delta);
_test_concave_hull(to_polygons(wafflex), polys);
_test_concave_hull(waffl, polys);
}
//FIXME this functionality is gone after TriangleMesh refactoring to get rid of admesh.
void check_validity(const TriangleMesh &input_mesh, int flags)
{
/*
TriangleMesh mesh{input_mesh};
if (flags & ASSUME_NO_EMPTY) {
REQUIRE_FALSE(mesh.empty());
} else if (mesh.empty())
return; // If it can be empty and it is, there is nothing left to do.
bool do_update_shared_vertices = false;
mesh.repair(do_update_shared_vertices);
if (flags & ASSUME_NO_REPAIR) {
REQUIRE_FALSE(mesh.repaired());
}
if (flags & ASSUME_MANIFOLD) {
if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj");
REQUIRE(mesh.is_manifold());
}
*/
}
void check_raster_transformations(sla::RasterBase::Orientation o, sla::RasterBase::TMirroring mirroring)
{
double disp_w = 120., disp_h = 68.;
sla::Resolution res{2560, 1440};
sla::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
sla::RasterBase::Trafo trafo{o, mirroring};
trafo.center_x = bb.center().x();
trafo.center_y = bb.center().y();
double gamma = 1.;
sla::RasterGrayscaleAAGammaPower raster{res, pixdim, trafo, gamma};
// create box of size 32x32 pixels (not 1x1 to avoid antialiasing errors)
coord_t pw = 32 * coord_t(std::ceil(scaled<double>(pixdim.w_mm)));
coord_t ph = 32 * coord_t(std::ceil(scaled<double>(pixdim.h_mm)));
ExPolygon box;
box.contour.points = {{-pw, -ph}, {pw, -ph}, {pw, ph}, {-pw, ph}};
double tr_x = scaled<double>(20.), tr_y = tr_x;
box.translate(tr_x, tr_y);
ExPolygon expected_box = box;
// Now calculate the position of the translated box according to output
// trafo.
if (o == sla::RasterBase::Orientation::roPortrait) expected_box.rotate(PI / 2.);
if (mirroring[X])
for (auto &p : expected_box.contour.points) p.x() = -p.x();
if (mirroring[Y])
for (auto &p : expected_box.contour.points) p.y() = -p.y();
raster.draw(box);
Point expected_coords = expected_box.contour.bounding_box().center();
double rx = unscaled(expected_coords.x() + bb.center().x()) / pixdim.w_mm;
double ry = unscaled(expected_coords.y() + bb.center().y()) / pixdim.h_mm;
auto w = size_t(std::floor(rx));
auto h = res.height_px - size_t(std::floor(ry));
REQUIRE((w < res.width_px && h < res.height_px));
auto px = raster.read_pixel(w, h);
if (px != FullWhite) {
std::fstream outf("out.png", std::ios::out);
outf << raster.encode(sla::PNGRasterEncoder());
}
REQUIRE(px == FullWhite);
}
ExPolygon square_with_hole(double v)
{
ExPolygon poly;
coord_t V = scaled(v / 2.);
poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}};
poly.holes.emplace_back();
V = V / 2;
poly.holes.front().points = {{-V, V}, {V, V}, {V, -V}, {-V, -V}};
return poly;
}
long raster_pxsum(const sla::RasterGrayscaleAA &raster)
{
auto res = raster.resolution();
long a = 0;
for (size_t x = 0; x < res.width_px; ++x)
for (size_t y = 0; y < res.height_px; ++y)
a += raster.read_pixel(x, y);
return a;
}
double raster_white_area(const sla::RasterGrayscaleAA &raster)
{
if (raster.resolution().pixels() == 0) return NaNd;
auto res = raster.resolution();
double a = 0;
for (size_t x = 0; x < res.width_px; ++x)
for (size_t y = 0; y < res.height_px; ++y) {
auto px = raster.read_pixel(x, y);
a += pixel_area(px, raster.pixel_dimensions());
}
return a;
}
double predict_error(const ExPolygon &p, const sla::PixelDim &pd)
{
auto lines = p.lines();
double pix_err = pixel_area(FullWhite, pd) / 2.;
// Worst case is when a line is parallel to the shorter axis of one pixel,
// when the line will be composed of the max number of pixels
double pix_l = std::min(pd.h_mm, pd.w_mm);
double error = 0.;
for (auto &l : lines)
error += (unscaled(l.length()) / pix_l) * pix_err;
return error;
}
sla::SupportPoints calc_support_pts(
const TriangleMesh & mesh,
const sla::SupportPointGenerator::Config &cfg)
{
// Prepare the slice grid and the slices
auto bb = cast<float>(mesh.bounding_box());
std::vector<float> heights = grid(bb.min.z(), bb.max.z(), 0.1f);
std::vector<ExPolygons> slices = slice_mesh_ex(mesh.its, heights, CLOSING_RADIUS);
// Prepare the support point calculator
AABBMesh emesh{mesh};
sla::SupportPointGenerator spgen{emesh, cfg, []{}, [](int){}};
// Calculate the support points
spgen.seed(0);
spgen.execute(slices, heights);
return spgen.output();
}

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