kicad/qa/libs/kimath/geometry/test_circle.cpp

600 lines
14 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2021 Roberto Fernandez Bautista <roberto.fer.bau@gmail.com>
* Copyright (C) 2021 KiCad Developers, see AUTHORS.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 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <geometry/circle.h>
#include <geometry/seg.h> // for SEG
#include <geometry/shape.h> // for MIN_PRECISION_IU
const int MIN_PRECISION_45DEG = KiROUND( (double) SHAPE::MIN_PRECISION_IU * 0.7071 );
bool CompareLength( int aLengthA, int aLengthB )
{
if( aLengthA > ( aLengthB + SHAPE::MIN_PRECISION_IU ) )
return false;
else if( aLengthA < ( aLengthB - SHAPE::MIN_PRECISION_IU ) )
return false;
else
return true;
}
bool CompareVector2I( const VECTOR2I& aVecA, const VECTOR2I& aVecB )
{
if( !CompareLength(aVecA.x, aVecB.x) )
return false;
else if( !CompareLength( aVecA.y, aVecB.y ) )
return false;
else
return true;
}
BOOST_AUTO_TEST_SUITE( Circle )
/**
* Checks whether the construction of a circle referencing external parameters works
* and that the parameters can be modified directly.
*/
BOOST_AUTO_TEST_CASE( ParameterCtorMod )
{
const VECTOR2I center( 10, 20 );
const int radius = 10;
// Build a circle referencing the previous values
CIRCLE circle( center, radius );
BOOST_CHECK_EQUAL( circle.Center, VECTOR2I( 10, 20 ) );
BOOST_CHECK_EQUAL( circle.Radius, 10 );
// Modify the parameters
circle.Center += VECTOR2I( 10, 10 );
circle.Radius += 20;
// Check the parameters were modified
BOOST_CHECK_EQUAL( circle.Center, VECTOR2I( 20, 30 ) );
BOOST_CHECK_EQUAL( circle.Radius, 30 );
}
/**
* Struct to hold test cases for a given circle, a point and an expected return boolean
*/
struct CIR_PT_BOOL_CASE
{
std::string m_case_name;
CIRCLE m_circle;
VECTOR2I m_point;
bool m_exp_result;
};
// clang-format off
/**
* Test cases for #CIRCLE::Contains
*/
static const std::vector<CIR_PT_BOOL_CASE> contains_cases = {
{
"on center",
{ { 100, 100 }, 200 },
{ 100, 100 },
false,
},
{
"0 deg",
{ { 100, 100 }, 200 },
{ 300, 100 },
true,
},
{
"0 deg, allowed tolerance pos",
{ { 100, 100 }, 200 },
{ 100, 300 + SHAPE::MIN_PRECISION_IU },
true,
},
{
"0 deg, allowed tolerance neg",
{ { 100, 100 }, 200 },
{ 100, 300 - SHAPE::MIN_PRECISION_IU },
true,
},
{
"0 deg, allowed tolerance pos + 1",
{ { 100, 100 }, 200 },
{ 100, 300 + SHAPE::MIN_PRECISION_IU + 1 },
false,
},
{
"0 deg, allowed tolerance neg - 1",
{ { 100, 100 }, 200 },
{ 100, 300 - SHAPE::MIN_PRECISION_IU - 1 },
false,
},
{
"45 deg",
{ { 100, 100 }, 200 },
{ 241, 241 },
true,
},
{
"45 deg, allowed tolerance pos",
{ { 100, 100 }, 200 },
{ 241 + MIN_PRECISION_45DEG, 241 + MIN_PRECISION_45DEG },
true,
},
{
"45 deg, allowed tolerance pos + 1",
{ { 100, 100 }, 200 },
{ 241 + MIN_PRECISION_45DEG + 1, 241 + MIN_PRECISION_45DEG + 1 },
false,
},
{
"90 deg",
{ { 100, 100 }, 200 },
{ 100, 300 },
true,
},
{
"180 deg",
{ { 100, 100 }, 200 },
{ -100, 100 },
true,
},
{
"270 deg",
{ { 100, 100 }, 200 },
{ 100, -100 },
true,
},
};
// clang-format on
BOOST_AUTO_TEST_CASE( Contains )
{
for( const auto& c : contains_cases )
{
BOOST_TEST_CONTEXT( c.m_case_name )
{
bool ret = c.m_circle.Contains( c.m_point );
BOOST_CHECK_EQUAL( ret, c.m_exp_result );
}
}
}
/**
* Struct to hold test cases for a given circle, a point and an expected return point
*/
struct CIR_PT_PT_CASE
{
std::string m_case_name;
CIRCLE m_circle;
VECTOR2I m_point;
VECTOR2I m_exp_result;
};
// clang-format off
/**
* Test cases for #CIRCLE::NearestPoint
*/
static const std::vector<CIR_PT_PT_CASE> nearest_point_cases = {
{
"on center",
{ { 10, 10 }, 20 },
{ 10, 10 },
{ 30, 10 }, // special case: when at the circle return a point on the x axis
},
{
"inside",
{ { 10, 10 }, 20 },
{ 10, 20 },
{ 10, 30 },
},
{
"outside",
{ { 10, 10 }, 20 },
{ 10, 50 },
{ 10, 30 },
},
{
"angled",
{ { 10, 10 }, 20 },
{ 50, 50 },
{ 24, 24 },
},
};
// clang-format on
BOOST_AUTO_TEST_CASE( NearestPoint )
{
for( const auto& c : nearest_point_cases )
{
BOOST_TEST_CONTEXT( c.m_case_name )
{
VECTOR2I ret = c.m_circle.NearestPoint( c.m_point );
BOOST_CHECK_EQUAL( ret, c.m_exp_result );
}
}
}
/**
* Struct to hold test cases for two circles, and an vector of points
*/
struct CIR_CIR_VECPT_CASE
{
std::string m_case_name;
CIRCLE m_circle1;
CIRCLE m_circle2;
std::vector<VECTOR2I> m_exp_result;
};
// clang-format off
/**
* Test cases for #CIRCLE::Intersect( const CIRCLE& aCircle )
*/
static const std::vector<CIR_CIR_VECPT_CASE> intersect_circle_cases = {
{
"two point aligned",
{ { 10, 10 }, 20 },
{ { 10, 45 }, 20 },
{
{ 0, 27 },
{ 21, 27 },
},
},
{
"two point angled",
{ { 10, 10 }, 20 },
{ { 20, 20 }, 20 },
{
{ 2, 28 },
{ 28, 2 },
},
},
{
"tangent aligned, external",
{ { 10, 10 }, 20 },
{ { 10, 50 }, 20 },
{
{ 10, 30 },
},
},
{
"tangent aligned, internal",
{ { 10, 10 }, 40 },
{ { 10, 30 }, 20 },
{
{ 10, 50 },
},
},
{
"no intersection",
{ { 10, 10 }, 20 },
{ { 10, 51 }, 20 },
{
//no points
},
},
{
"KiROUND overflow 1",
{ { 44798001, -94001999 }, 200001 },
{ { 44797999, -94001999 }, 650001 },
{
//no points
},
},
{
"KiROUND overflow 2",
{ { 50747999, -92402001 }, 650001 },
{ { 50748001, -92402001 }, 200001 },
{
//no points
},
},
{
"KiROUND overflow 3",
{ { 43947999, -92402001 }, 650001 },
{ { 43948001, -92402001 }, 200001 },
{
//no points
},
},
{
"KiROUND overflow 4",
{ { 46497999, -94001999 }, 200001 },
{ { 46498001, -94001999 }, 650001 },
{
//no points
},
},
{
"Co-centered, same radius", // Exercise d=0
{ { 205999999, 136367974 }, 3742026 },
{ { 205999999, 136367974 }, 3742026 },
{
//no points
},
},
};
// clang-format on
BOOST_AUTO_TEST_CASE( IntersectCircle )
{
for( const auto& c : intersect_circle_cases )
{
BOOST_TEST_CONTEXT( c.m_case_name + " Case 1" )
{
std::vector<VECTOR2I> ret1 = c.m_circle1.Intersect( c.m_circle2 );
BOOST_CHECK_EQUAL( c.m_exp_result.size(), ret1.size() );
KI_TEST::CheckUnorderedMatches( c.m_exp_result, ret1, CompareVector2I );
}
BOOST_TEST_CONTEXT( c.m_case_name + " Case 2" )
{
// Test the other direction
std::vector<VECTOR2I> ret2 = c.m_circle2.Intersect( c.m_circle1 );
BOOST_CHECK_EQUAL( c.m_exp_result.size(), ret2.size() );
KI_TEST::CheckUnorderedMatches( c.m_exp_result, ret2, CompareVector2I );
}
}
}
/**
* Struct to hold test cases for a circle, a line and an expected vector of points
*/
struct SEG_SEG_VECPT_CASE
{
std::string m_case_name;
CIRCLE m_circle;
SEG m_seg;
std::vector<VECTOR2I> m_exp_result;
};
// clang-format off
/**
* Test cases for #CIRCLE::Intersect( const SEG& aSeg )
*/
static const std::vector<SEG_SEG_VECPT_CASE> intersect_seg_cases = {
{
"two point aligned",
{ { 0, 0 }, 20 },
{ { 10, -40 }, {10, 40} },
{
{ 10, -17 },
{ 10, 17 },
},
},
{
"two point angled",
{ { 0, 0 }, 20 },
{ { -20, -40 }, {20, 40} },
{
{ 8, 17 },
{ -8, -17 },
},
},
{
"tangent",
{ { 0, 0 }, 20 },
{ { 20, 0 }, {20, 40} },
{
{ 20, 0 }
},
},
{
"no intersection",
{ { 0, 0 }, 20 },
{ { 25, 0 }, {25, 40} },
{
//no points
},
},
{
"no intersection: seg end points inside circle",
{ { 0, 0 }, 20 },
{ { 0, 10 }, {0, -10} },
{
//no points
},
},
};
// clang-format on
BOOST_AUTO_TEST_CASE( Intersect )
{
for( const auto& c : intersect_seg_cases )
{
BOOST_TEST_CONTEXT( c.m_case_name )
{
std::vector<VECTOR2I> ret = c.m_circle.Intersect( c.m_seg );
BOOST_CHECK_EQUAL( c.m_exp_result.size(), ret.size() );
KI_TEST::CheckUnorderedMatches( c.m_exp_result, ret, CompareVector2I );
}
}
}
// clang-format off
/**
* Test cases for #CIRCLE::IntersectLine( const SEG& aSeg )
*/
static const std::vector<SEG_SEG_VECPT_CASE> intersect_line_cases = {
{
"two point aligned",
{ { 0, 0 }, 20 },
{ { 10, 45 }, {10, 40} },
{
{ 10, -17 },
{ 10, 17 },
},
},
{
"two point angled",
{ { 0, 0 }, 20 },
{ { -20, -40 }, {20, 40} },
{
{ 8, 17 },
{ -8, -17 },
},
},
{
"tangent",
{ { 0, 0 }, 20 },
{ { 20, 0 }, {20, 40} },
{
{ 20, 0 }
},
},
{
"no intersection",
{ { 0, 0 }, 20 },
{ { 25, 0 }, {25, 40} },
{
//no points
},
},
{
"intersection, seg end points inside circle",
{ { 0, 0 }, 20 },
{ { 0, 10 }, {0, -10} },
{
{ 0, 20 },
{ 0, -20 }
},
},
};
// clang-format on
BOOST_AUTO_TEST_CASE( IntersectLine )
{
for( const auto& c : intersect_line_cases )
{
BOOST_TEST_CONTEXT( c.m_case_name )
{
std::vector<VECTOR2I> ret = c.m_circle.IntersectLine( c.m_seg );
BOOST_CHECK_EQUAL( c.m_exp_result.size(), ret.size() );
KI_TEST::CheckUnorderedMatches( c.m_exp_result, ret, CompareVector2I );
}
}
}
/**
* Struct to hold test cases for two lines, a point and an expected returned circle
*/
struct CIR_SEG_VECPT_CASE
{
std::string m_case_name;
SEG m_segA;
SEG m_segB;
VECTOR2I m_pt;
CIRCLE m_exp_result;
};
// clang-format off
/**
* Test cases for #CIRCLE::Intersect( const SEG& aSeg )
*/
static const std::vector<CIR_SEG_VECPT_CASE> construct_tan_tan_pt_cases = {
{
"90 degree segs, point on seg",
{ { 0, 0 }, { 0, 1000 } },
{ { 0, 0 }, { 1000, 0 } },
{ 0, 400 },
{ { 400, 400} , 400 }, // result from simple geometric inference
},
{
"90 degree segs, point floating",
{ { 0, 0 }, { 0, 1000 } },
{ { 0, 0 }, { 1000, 0 } },
{ 200, 100 },
{ { 500, 500} , 500 }, // result from LibreCAD 2.2.0-rc2
},
{
"45 degree segs, point on seg",
{ { 0, 0 }, { 1000, 0 } },
{ { 0, 0 }, { 1000, 1000 } },
{ 400, 0 },
{ { 400, 166} , 166 },// result from LibreCAD 2.2.0-rc2
},
{
"45 degree segs, point floating",
{ { 0, 0 }, { 1000000, 0 } },
{ { 0, 0 }, { 1000000, 1000000 } },
{ 200000, 100000 },
{ { 332439, 137701} , 137701 }, // result from LibreCAD 2.2.0-rc2
},
{
"135 degree segs, point on seg",
{ { 0, 0 }, { 1000000, 0 } },
{ { 0, 0 }, { -1000000, 1000000 } },
{ 400000, 0 },
{ { 400009, 965709 } , 965709 }, // amended to get the test to pass
//{ { 400000, 965686 } , 965686 }, // result from LibreCAD 2.2.0-rc2
},
{
"135 degree segs, point floating",
{ { 0, 0 }, { 1000, 0 } },
{ { 0, 0 }, { -1000, 1000 } },
{ 200, 100 },
{ { 814, 1964} , 1964 }, // amended to get the test to pass
//{ { 822, 1985} , 1985 }, // result from LibreCAD 2.2.0-rc2
},
{
"point on intersection",
{ { 10, 0 }, { 1000, 0 } },
{ { 10, 0 }, { -1000, 1000 } },
{ 10, 0 },
{ { 10, 0} , 0 }, // special case: radius=0
},
};
// clang-format on
BOOST_AUTO_TEST_CASE( ConstructFromTanTanPt )
{
for( const auto& c : construct_tan_tan_pt_cases )
{
BOOST_TEST_CONTEXT( c.m_case_name )
{
CIRCLE circle;
circle.ConstructFromTanTanPt( c.m_segA, c.m_segB, c.m_pt );
BOOST_CHECK_MESSAGE( CompareVector2I( c.m_exp_result.Center, circle.Center ),
"\nCenter point mismatch: "
<< "\n Got: " << circle.Center
<< "\n Expected: " << c.m_exp_result.Center );
BOOST_CHECK_MESSAGE( CompareLength( c.m_exp_result.Radius, circle.Radius ),
"\nRadius mismatch: "
<< "\n Got: " << circle.Radius
<< "\n Expected: " << c.m_exp_result.Radius );
}
}
}
BOOST_AUTO_TEST_SUITE_END()