diff --git a/qa/CMakeLists.txt b/qa/CMakeLists.txt index e8cd8846bf..e82afcd73c 100644 --- a/qa/CMakeLists.txt +++ b/qa/CMakeLists.txt @@ -12,6 +12,7 @@ if( KICAD_SCRIPTING_MODULES ) endif() +add_subdirectory( geometry ) add_subdirectory( shape_poly_set_refactor ) add_subdirectory( pcb_test_window ) add_subdirectory( polygon_triangulation ) diff --git a/qa/geometry/CMakeLists.txt b/qa/geometry/CMakeLists.txt new file mode 100644 index 0000000000..031c02b3a7 --- /dev/null +++ b/qa/geometry/CMakeLists.txt @@ -0,0 +1,49 @@ +# This program source code file is part of KiCad, a free EDA CAD application. +# +# Copyright (C) 2018 KiCad Developers, see CHANGELOG.TXT for contributors. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, you may find one here: +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# or you may search the http://www.gnu.org website for the version 2 license, +# or you may write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +find_package(Boost COMPONENTS unit_test_framework REQUIRED) +find_package( wxWidgets 3.0.0 COMPONENTS gl aui adv html core net base xml stc REQUIRED ) + + +add_definitions(-DBOOST_TEST_DYN_LINK) + +add_executable( qa_geometry + test_module.cpp + test_fillet.cpp +) + +include_directories( + ${CMAKE_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/polygon + ${Boost_INCLUDE_DIR} +) + +target_link_libraries( qa_geometry + common + polygon + ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} + ${wxWidgets_LIBRARIES} +) + +add_test( NAME geometry + COMMAND qa_geometry +) \ No newline at end of file diff --git a/qa/geometry/geom_test_utils.h b/qa/geometry/geom_test_utils.h new file mode 100644 index 0000000000..cb4e9721f8 --- /dev/null +++ b/qa/geometry/geom_test_utils.h @@ -0,0 +1,212 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2018 KiCad Developers, see CHANGELOG.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GEOM_TEST_UTILS_H +#define GEOM_TEST_UTILS_H + +/** + * @brief Utility functions for testing geometry functions. + */ +namespace GEOM_TEST +{ + +constexpr double PI = atan(1.0) * 4.0; +constexpr double PI_2 = atan(1.0) * 2.0; + +/** + * @brief Check if a value is within a tolerance of a nominal value + * +* @return value is in [aNominal - aError, aNominal + aError] + */ +template +bool IsWithin( T aValue, T aNominal, T aError ) +{ + return ( aValue >= aNominal - aError ) + && ( aValue <= aNominal + aError ); +} + +/** + * @brief Check if a value is within a tolerance of a nominal value, + * with different allowances for errors above and below. + * + * @return value is in [aNominal - aErrorBelow, aNominal + aErrorAbove] + */ +template +bool IsWithin( T aValue, T aNominal, T aErrorAbove, T aErrorBelow ) +{ + return ( aValue >= aNominal - aErrorBelow ) + && ( aValue <= aNominal + aErrorAbove ); +} + +/** + * @brief value is in range [aNominal - aErrorBelow, aNominal] + */ +template +bool IsWithinAndBelow( T aValue, T aNominal, T aErrorBelow ) +{ + return IsWithin( aValue, aNominal, 0, aErrorBelow ); +} + +/** + * @brief value is in range [aNominal, aNominal + aErrorAbove] + */ +template +bool IsWithinAndAbove( T aValue, T aNominal, T aErrorAbove ) +{ + return IsWithin( aValue, aNominal, aErrorAbove, 0 ); +} + +/** + * @brief Geometric quadrants, from top-right, anti-clockwise + * + * ^ y + * | + * Q2 | Q1 + * -------> x + * Q3 | Q4 + */ +enum class QUADRANT { + Q1, Q2, Q3, Q4 +}; + +/* + * @brief Check value in Quadrant 1 (x and y both >= 0) + */ +template +bool IsInQuadrant( const VECTOR2& aPoint, QUADRANT aQuadrant ) +{ + bool isInQuad = false; + + switch( aQuadrant ) + { + case QUADRANT::Q1: + isInQuad = aPoint.x >= 0 && aPoint.y >= 0; + break; + case QUADRANT::Q2: + isInQuad = aPoint.x <= 0 && aPoint.y >= 0; + break; + case QUADRANT::Q3: + isInQuad = aPoint.x <= 0 && aPoint.y <= 0; + break; + case QUADRANT::Q4: + isInQuad = aPoint.x >= 0 && aPoint.y <= 0; + break; + } + + return isInQuad; +} + +/* + * @Brief Check if both ends of a segment are in Quadrant 1 + */ +bool SegmentCompletelyInQuadrant( const SEG& aSeg, QUADRANT aQuadrant ) +{ + return IsInQuadrant( aSeg.A, aQuadrant) + && IsInQuadrant( aSeg.B, aQuadrant ); +} + +/* + * @brief Check if at least one end of the segment is in Quadrant 1 + */ +bool SegmentEndsInQuadrant( const SEG& aSeg, QUADRANT aQuadrant ) +{ + return IsInQuadrant( aSeg.A, aQuadrant ) + || IsInQuadrant( aSeg.B, aQuadrant ); +} + +/* + * @brief Check if a segment is entirely within a certain radius of a point. + */ +bool SegmentCompletelyWithinRadius( const SEG& aSeg, const VECTOR2I& aPt, + const int aRadius ) +{ + // This is true iff both ends of the segment are within the radius + return ( ( aSeg.A - aPt ).EuclideanNorm() < aRadius ) + && ( ( aSeg.B - aPt ).EuclideanNorm() < aRadius ); +} + +/* + * @brief Check if two vectors are perpendicular + * + * @param a: vector A + * @param b: vector B + * @param aTolerance: the allowed deviation from PI/2 (e.g. when rounding) + */ + +template +bool ArePerpendicular( const VECTOR2& a, const VECTOR2& b, double aTolerance ) +{ + auto angle = std::abs( a.Angle() - b.Angle() ); + + // Normalise: angles of 3*pi/2 are also perpendicular + if (angle > PI) + { + angle -= PI; + } + + return IsWithin( angle, PI_2, aTolerance ); +} + +/** + * @brief construct a square polygon of given size width and centre + * + * @param aSize: the side width (must be divisible by 2 if want to avoid rounding) + * @param aCentre: the centre of the square + */ +SHAPE_LINE_CHAIN MakeSquarePolyLine( int aSize, const VECTOR2I& aCentre ) +{ + SHAPE_LINE_CHAIN polyLine; + + const VECTOR2I corner = aCentre + aSize / 2; + + polyLine.Append( VECTOR2I( corner.x, corner.y ) ); + polyLine.Append( VECTOR2I( -corner.x, corner.y ) ) ; + polyLine.Append( VECTOR2I( -corner.x, -corner.y ) ); + polyLine.Append( VECTOR2I( corner.x, -corner.y ) ); + + polyLine.SetClosed( true ); + + return polyLine; +} + +/* + * @brief Fillet every polygon in a set and return a new set + */ +SHAPE_POLY_SET FilletPolySet( SHAPE_POLY_SET& aPolySet, int aRadius, + int aError ) +{ + SHAPE_POLY_SET filletedPolySet; + + for ( int i = 0; i < aPolySet.OutlineCount(); ++i ) + { + const auto filleted = aPolySet.FilletPolygon( aRadius, aError, i ); + + filletedPolySet.AddOutline( filleted[0] ); + } + + return filletedPolySet; +} + +} + +#endif // GEOM_TEST_UTILS_H \ No newline at end of file diff --git a/qa/geometry/test_fillet.cpp b/qa/geometry/test_fillet.cpp new file mode 100644 index 0000000000..10a7fe7258 --- /dev/null +++ b/qa/geometry/test_fillet.cpp @@ -0,0 +1,210 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2018 KiCad Developers, see CHANGELOG.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include +#include + +#include +#include + +#include + +#include + +struct FilletFixture +{ +}; + +/** + * Declares the FilletFixture struct as the boost test fixture. + */ +BOOST_FIXTURE_TEST_SUITE( Fillet, FilletFixture ) + +/* + * @brief check that a single segment of a fillet complies with the geometric + * constraint: + * + * 1: The end points are radius from the centre point + * 2: The mid point error is acceptable + * 3: The segment midpoints are perpendicular to the radius + */ +void TestFilletSegmentConstraints( const SEG& aSeg, VECTOR2I aRadCentre, + int aRadius, int aError ) +{ + using namespace GEOM_TEST; + + const auto diffA = aRadCentre - aSeg.A; + const auto diffB = aRadCentre - aSeg.B; + const auto diffC = aRadCentre - aSeg.Center(); + + // Check 1: radii (error of 1 for rounding) + BOOST_CHECK_PREDICATE( IsWithinAndBelow, + ( diffA.EuclideanNorm() )( aRadius )( 1 ) ); + BOOST_CHECK_PREDICATE( IsWithinAndBelow, + ( diffB.EuclideanNorm() )( aRadius )( 1 ) ); + + // Check 2: Mid-point error + BOOST_CHECK_PREDICATE( IsWithinAndBelow, + ( diffC.EuclideanNorm() )( aRadius )( aError + 1 ) ); + + // Check 3: Mid-point -> radius centre perpendicular + BOOST_CHECK_PREDICATE( ArePerpendicular, + ( diffC )( aSeg.A - aSeg.B )( PI_2 / 10 ) ); +} + + +/** + * @brief: Create a square, fillet it, and check a corner for correctness + */ +void TestSquareFillet( int aSquareSize, int aRadius, int aError ) +{ + using namespace GEOM_TEST; + + SHAPE_POLY_SET squarePolySet; + + squarePolySet.AddOutline( MakeSquarePolyLine(aSquareSize, VECTOR2I(0, 0) ) ); + + SHAPE_POLY_SET filleted = FilletPolySet(squarePolySet, aRadius, aError); + + // expect a single filleted polygon + BOOST_CHECK_EQUAL( filleted.OutlineCount(), 1 ); + + auto segIter = filleted.IterateSegments(); + + const VECTOR2I radCentre { aSquareSize / 2 - aRadius, + aSquareSize / 2 - aRadius }; + + int checked = 0; + + for( ; segIter; segIter++ ) + { + // Only check the first Quadrant + if ( SegmentCompletelyInQuadrant( *segIter, QUADRANT::Q1 ) ) + { + TestFilletSegmentConstraints( *segIter, radCentre, aRadius, aError ); + checked++; + } + } + + // we expect there to be at least one segment in the fillet + BOOST_CHECK( checked > 0 ); +} + + +/** + * @brief: Create a square concave corner, fillet and check correctness + */ +void TestConcaveSquareFillet( int aSquareSize, int aRadius, int aError ) +{ + using namespace GEOM_TEST; + + SHAPE_POLY_SET polySet; + SHAPE_LINE_CHAIN polyLine; + + /* + * L-shape: + * ---- + * | | + * ---- | + * | | + * -------- + */ + + polyLine.Append( VECTOR2I{ 0, 0 } ); + polyLine.Append( VECTOR2I{ 0, aSquareSize / 2 } ); + polyLine.Append( VECTOR2I{ aSquareSize / 2 , aSquareSize / 2 } ); + polyLine.Append( VECTOR2I{ aSquareSize / 2 , aSquareSize } ); + polyLine.Append( VECTOR2I{ aSquareSize, aSquareSize } ); + polyLine.Append( VECTOR2I{ aSquareSize, 0 } ); + + polyLine.SetClosed( true ); + + polySet.AddOutline( polyLine ); + + SHAPE_POLY_SET filleted = FilletPolySet(polySet, aRadius, aError); + + // expect a single filleted polygon + BOOST_CHECK_EQUAL( filleted.OutlineCount(), 1 ); + + auto segIter = filleted.IterateSegments(); + + const VECTOR2I radCentre { aSquareSize / 2 - aRadius, + aSquareSize / 2 + aRadius }; + + int checked = 0; + + for( ; segIter; segIter++ ) + { + // Only check segments around the concave corner + if ( SegmentCompletelyWithinRadius( *segIter, radCentre, aRadius + 1) ) + { + TestFilletSegmentConstraints( *segIter, radCentre, aRadius, aError ); + checked++; + } + } + + // we expect there to be at least one segment in the fillet + BOOST_CHECK( checked > 0 ); +} + + +struct SquareFilletTestCase +{ + int squareSize; + int radius; + int error; +}; + +const std::vector squareFilletCases { + { 1000, 120, 10 }, + { 1000, 10, 1 }, + + /* Large error relative to fillet */ + { 1000, 10, 5 }, + + /* Very small error relative to fillet(many segments in interpolation) */ + { 70000, 1000, 1 }, +}; + +/** + * Tests the SHAPE_POLY_SET::FilletPolygon method against certain geometric + * constraints. + */ +BOOST_AUTO_TEST_CASE( SquareFillet ) +{ + for ( const auto& testCase : squareFilletCases ) + { + TestSquareFillet( testCase.squareSize, testCase.radius, testCase.error ); + } +} + +BOOST_AUTO_TEST_CASE( SquareConcaveFillet ) +{ + for ( const auto& testCase : squareFilletCases ) + { + TestConcaveSquareFillet( testCase.squareSize, testCase.radius, testCase.error ); + } +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/qa/geometry/test_module.cpp b/qa/geometry/test_module.cpp new file mode 100644 index 0000000000..6fb45de1fd --- /dev/null +++ b/qa/geometry/test_module.cpp @@ -0,0 +1,32 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2018 KiCad Developers, see CHANGELOG.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * Main file for the geometry tests to be compiled + */ + +#define BOOST_TEST_MAIN +#define BOOST_TEST_MODULE "Geometry module tests" + + +#include