diff --git a/libs/kimath/CMakeLists.txt b/libs/kimath/CMakeLists.txt index c5605fb040..fc90d1c8e4 100644 --- a/libs/kimath/CMakeLists.txt +++ b/libs/kimath/CMakeLists.txt @@ -10,6 +10,7 @@ set( KIMATH_SRCS src/geometry/convex_hull.cpp src/geometry/direction_45.cpp src/geometry/geometry_utils.cpp + src/geometry/oval.cpp src/geometry/seg.cpp src/geometry/shape.cpp src/geometry/shape_arc.cpp diff --git a/libs/kimath/include/geometry/oval.h b/libs/kimath/include/geometry/oval.h new file mode 100644 index 0000000000..fffcf1e2cc --- /dev/null +++ b/libs/kimath/include/geometry/oval.h @@ -0,0 +1,63 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 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 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 GEOMETRY_OVAL_H_ +#define GEOMETRY_OVAL_H_ + +#include + +#include +#include + +enum OVAL_KEY_POINTS +{ + OVAL_CENTER = 1 << 0, + OVAL_CAP_TIPS = 1 << 1, + OVAL_CAP_CENTERS = 1 << 2, + OVAL_SIDE_MIDPOINTS = 1 << 3, + OVAL_SIDE_ENDS = 1 << 4, + OVAL_CARDINAL_EXTREMES = 1 << 5, + OVAL_ALL_KEY_POINTS = 0xFF +}; + +using OVAL_KEY_POINT_FLAGS = unsigned int; + +/** + * @brief Get a list of interesting points on an oval (rectangle + * with semicircular end caps) + * + * This may includes: + * - The middles of the sides + * - The tips of the end caps + * - The extreme cardinal points of the whole oval (if rotated non-cardinally) + * + * @param aOvalSize - The size of the oval (overall length and width) + * @param aRotation - The rotation of the oval + * @param aFlags - The flags indicating which points to return + * + * @return std::vector - The list of points + */ +std::vector GetOvalKeyPoints( const VECTOR2I& aOvalSize, const EDA_ANGLE& aRotation, + OVAL_KEY_POINT_FLAGS aFlags ); + +#endif /* GEOMETRY_OVAL_H_ */ \ No newline at end of file diff --git a/libs/kimath/src/geometry/oval.cpp b/libs/kimath/src/geometry/oval.cpp new file mode 100644 index 0000000000..22063e3ce0 --- /dev/null +++ b/libs/kimath/src/geometry/oval.cpp @@ -0,0 +1,134 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2014 CERN + * Copyright (C) 2018-2023 KiCad Developers, see AUTHORS.txt for contributors. + * @author Tomasz Wlostowski + * + * 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 "geometry/oval.h" + +#include // for RotatePoint + + +std::vector GetOvalKeyPoints( const VECTOR2I& aOvalSize, const EDA_ANGLE& aRotation, + OVAL_KEY_POINT_FLAGS aFlags ) +{ + const VECTOR2I half_size = aOvalSize / 2; + const int half_width = std::min( half_size.x, half_size.y ); + const int half_len = std::max( half_size.x, half_size.y ); + + // Points on a non-rotated pad at the origin, long-axis is y + // (so for now, width is left/right, len is up/down) + std::vector pts; + + if ( aFlags & OVAL_CENTER ) + { + // Centre is easy + pts.emplace_back( 0, 0 ); + }; + + if ( aFlags & OVAL_SIDE_MIDPOINTS ) + { + // Side midpoints + pts.emplace_back( half_width, 0 ); + pts.emplace_back( -half_width, 0 ); + } + + if ( aFlags & OVAL_CAP_TIPS ) + { + // Cap ends + pts.emplace_back( 0, half_len ); + pts.emplace_back( 0, -half_len ); + } + + // Distance from centre to cap centres + const int d_centre_to_cap_centre = half_len - half_width; + + if ( aFlags & OVAL_CAP_CENTERS ) + { + // Cap centres + pts.emplace_back( 0, d_centre_to_cap_centre ); + pts.emplace_back( 0, -d_centre_to_cap_centre ); + } + + if ( aFlags & OVAL_SIDE_ENDS ) + { + // End points of flat sides (always vertical) + pts.emplace_back( half_width, d_centre_to_cap_centre ); + pts.emplace_back( half_width, -d_centre_to_cap_centre ); + pts.emplace_back( -half_width, d_centre_to_cap_centre ); + pts.emplace_back( -half_width, -d_centre_to_cap_centre ); + } + + // If the pad is horizontal (i.e. x > y), we'll rotate the whole thing + // 90 degrees and work with it as if it was vertical + const bool swap_xy = half_size.x > half_size.y; + const EDA_ANGLE rotation = aRotation + ( swap_xy ? -ANGLE_90 : ANGLE_0 ); + + // Add the quadrant points to the caps only if rotated + // (otherwise they're just the tips) + if( ( aFlags & OVAL_CARDINAL_EXTREMES ) && !rotation.IsCardinal() ) + { + // We need to find two perpendicular lines from the centres + // of each cap to the cap edge, which will hit the points + // where the cap is tangent to H/V lines when rotated into place. + // + // Because we know the oval is always vertical, this means the + // two lines are formed between _|, through \/ to |_ + // where the apex is the cap centre. + + // The vector from a cap centre to the tip (i.e. vertical) + const VECTOR2I cap_radial = { 0, half_width }; + + // Rotate in the opposite direction to the oval's rotation + // (that will be unwound later) + EDA_ANGLE radial_line_rotation = -rotation; + + radial_line_rotation.Normalize90(); + + VECTOR2I cap_radial_to_x_axis = cap_radial; + RotatePoint( cap_radial_to_x_axis, radial_line_rotation ); + + // Find the other line - it's 90 degrees away, but re-normalise + // as it could be to the left or right + radial_line_rotation -= ANGLE_90; + radial_line_rotation.Normalize90(); + + VECTOR2I cap_radial_to_y_axis = cap_radial; + RotatePoint( cap_radial_to_y_axis, radial_line_rotation ); + + // The quadrant points are then the relevant offsets from each cap centre + pts.emplace_back( VECTOR2I{ 0, d_centre_to_cap_centre } + cap_radial_to_y_axis ); + pts.emplace_back( VECTOR2I{ 0, d_centre_to_cap_centre } + cap_radial_to_x_axis ); + // The opposite cap offsets go from the other cap centre, the other way + pts.emplace_back( VECTOR2I{ 0, -d_centre_to_cap_centre } - cap_radial_to_y_axis ); + pts.emplace_back( VECTOR2I{ 0, -d_centre_to_cap_centre } - cap_radial_to_x_axis ); + } + + for( VECTOR2I& pt : pts ) + { + // Transform to the actual orientation + // Already includes the extra 90 to swap x/y if needed + RotatePoint( pt, rotation ); + } + + return pts; +} \ No newline at end of file diff --git a/pcbnew/tools/pcb_grid_helper.cpp b/pcbnew/tools/pcb_grid_helper.cpp index 9a205b3b73..82446578f3 100644 --- a/pcbnew/tools/pcb_grid_helper.cpp +++ b/pcbnew/tools/pcb_grid_helper.cpp @@ -23,7 +23,10 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ +#include "pcb_grid_helper.h" + #include + #include #include #include @@ -31,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -43,8 +47,6 @@ #include #include #include -#include "pcb_grid_helper.h" - PCB_GRID_HELPER::PCB_GRID_HELPER( TOOL_MANAGER* aToolMgr, MAGNETIC_SETTINGS* aMagneticSettings ) : GRID_HELPER( aToolMgr ), @@ -447,6 +449,22 @@ void PCB_GRID_HELPER::computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos const std::set& activeLayers = settings->GetHighContrastLayers(); bool isHighContrast = settings->GetHighContrast(); + // As defaults, these are probably reasonable to avoid spamming key points + const OVAL_KEY_POINT_FLAGS ovalKeyPointFlags = + OVAL_CENTER | OVAL_CAP_TIPS | OVAL_SIDE_MIDPOINTS | OVAL_CARDINAL_EXTREMES; + + // The key points of a circle centred around (0, 0) with the given radius + const auto getCircleKeyPoints = [] ( int radius ) + { + return std::vector{ + {0, 0}, + { -radius, 0 }, + { radius, 0 }, + { 0, -radius }, + { 0, radius } + }; + }; + auto handlePadShape = [&]( PAD* aPad ) { @@ -463,39 +481,26 @@ void PCB_GRID_HELPER::computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos int r = aPad->GetSizeX() / 2; VECTOR2I center = aPad->ShapePos(); - addAnchor( center + VECTOR2I( -r, 0 ), OUTLINE | SNAPPABLE, aPad ); - addAnchor( center + VECTOR2I( r, 0 ), OUTLINE | SNAPPABLE, aPad ); - addAnchor( center + VECTOR2I( 0, -r ), OUTLINE | SNAPPABLE, aPad ); - addAnchor( center + VECTOR2I( 0, r ), OUTLINE | SNAPPABLE, aPad ); + const std::vector circle_pts = getCircleKeyPoints( r ); + + for ( const VECTOR2I& pt: circle_pts ) { + // Transform to the pad positon + addAnchor( center + pt, OUTLINE | SNAPPABLE, aPad ); + } break; } case PAD_SHAPE::OVAL: { - VECTOR2I pos = aPad->ShapePos(); - VECTOR2I half_size = aPad->GetSize() / 2; - int half_width = std::min( half_size.x, half_size.y ); - VECTOR2I half_len( half_size.x - half_width, half_size.y - half_width ); + const VECTOR2I pos = aPad->ShapePos(); - RotatePoint( half_len, aPad->GetOrientation() ); + const std::vector oval_pts = GetOvalKeyPoints( + aPad->GetSize(), aPad->GetOrientation(), ovalKeyPointFlags ); - VECTOR2I a( pos - half_len ); - VECTOR2I b( pos + half_len ); - VECTOR2I normal = b - a; - normal.Resize( half_width ); - RotatePoint( normal, ANGLE_90 ); - - addAnchor( a + normal, OUTLINE | SNAPPABLE, aPad ); - addAnchor( a - normal, OUTLINE | SNAPPABLE, aPad ); - addAnchor( b + normal, OUTLINE | SNAPPABLE, aPad ); - addAnchor( b - normal, OUTLINE | SNAPPABLE, aPad ); - addAnchor( pos + normal, OUTLINE | SNAPPABLE, aPad ); - addAnchor( pos - normal, OUTLINE | SNAPPABLE, aPad ); - - RotatePoint( normal, -ANGLE_90 ); - - addAnchor( a - normal, OUTLINE | SNAPPABLE, aPad ); - addAnchor( b + normal, OUTLINE | SNAPPABLE, aPad ); + for ( const VECTOR2I& pt: oval_pts ) { + // Transform to the pad positon + addAnchor( pos + pt, OUTLINE | SNAPPABLE, aPad ); + } break; } diff --git a/qa/unittests/libs/kimath/CMakeLists.txt b/qa/unittests/libs/kimath/CMakeLists.txt index 6f78930f55..2a8516b19c 100644 --- a/qa/unittests/libs/kimath/CMakeLists.txt +++ b/qa/unittests/libs/kimath/CMakeLists.txt @@ -31,6 +31,7 @@ set( QA_KIMATH_SRCS geometry/test_ellipse_to_bezier.cpp geometry/test_fillet.cpp geometry/test_circle.cpp + geometry/test_oval.cpp geometry/test_segment.cpp geometry/test_shape_compound_collision.cpp geometry/test_shape_arc.cpp diff --git a/qa/unittests/libs/kimath/geometry/geom_test_utils.h b/qa/unittests/libs/kimath/geometry/geom_test_utils.h index 7c38571b4c..21f45237c8 100644 --- a/qa/unittests/libs/kimath/geometry/geom_test_utils.h +++ b/qa/unittests/libs/kimath/geometry/geom_test_utils.h @@ -332,6 +332,7 @@ struct print_log_value os << "]"; } }; + } BOOST_TEST_PRINT_NAMESPACE_CLOSE diff --git a/qa/unittests/libs/kimath/geometry/test_oval.cpp b/qa/unittests/libs/kimath/geometry/test_oval.cpp new file mode 100644 index 0000000000..1ca8c225cb --- /dev/null +++ b/qa/unittests/libs/kimath/geometry/test_oval.cpp @@ -0,0 +1,177 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 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 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 "geom_test_utils.h" + +/** + * @brief Check that two collections contain the same elements, ignoring order. + * + * I.e. expected contains everything in actual and vice versa. + * + * The collections lengths are also checked to weed out unexpected duplicates. + * + * @param expected a collection of expected elements + * @param actual a collection of actual elements + */ +template +void CHECK_COLLECTIONS_SAME_UNORDERED(const T& expected, const T& actual) { + + for( const auto& p : expected ) + { + BOOST_CHECK_MESSAGE( std::find( actual.begin(), actual.end(), p ) != actual.end(), + "Expected item not found: " << p ); + } + + for( const auto& p : actual ) + { + BOOST_CHECK_MESSAGE( std::find( expected.begin(), expected.end(), p ) != expected.end(), + "Unexpected item: " << p ); + } + + BOOST_CHECK_EQUAL( expected.size(), actual.size() ); +} + +struct OvalFixture +{ +}; + +BOOST_FIXTURE_TEST_SUITE( Oval, OvalFixture ) + +struct OVAL_POINTS_TEST_CASE +{ + VECTOR2I m_size; + EDA_ANGLE m_rotation; + std::vector m_expected_points; +}; + +void DoOvalPointTestChecks( const OVAL_POINTS_TEST_CASE& testcase ) +{ + const auto sort_vectors_x_then_y = []( const VECTOR2I& a, const VECTOR2I& b ) { + return LexicographicalCompare( a, b ) > 0; + }; + + std::vector expected_points = testcase.m_expected_points; + std::vector actual_points = + GetOvalKeyPoints( testcase.m_size, testcase.m_rotation, OVAL_ALL_KEY_POINTS ); + + CHECK_COLLECTIONS_SAME_UNORDERED( expected_points, actual_points ); +} + +BOOST_AUTO_TEST_CASE( SimpleOvalVertical ) +{ + const OVAL_POINTS_TEST_CASE testcase + { + { 1000, 3000 }, + { 0, DEGREES_T }, + { + { 0, 0 }, + // Main points + { 0, 1500 }, + { 0, -1500 }, + { 500, 0 }, + { -500, 0 }, + // Cap centres + { 0, 1000 }, + { 0, -1000 }, + // Side segment ends + { 500, 1000 }, + { 500, -1000 }, + { -500, 1000 }, + { -500, -1000 }, + }, + }; + + DoOvalPointTestChecks( testcase ); +} + +BOOST_AUTO_TEST_CASE( SimpleOvalHorizontal ) +{ + const OVAL_POINTS_TEST_CASE testcase + { + { 3000, 1000 }, + { 0, DEGREES_T }, + { + { 0, 0 }, + // Main points + { 0, 500 }, + { 0, -500 }, + { 1500, 0 }, + { -1500, 0 }, + // Cap centres + { 1000, 0 }, + { -1000, 0 }, + // Side segment ends + { 1000, 500 }, + { 1000, -500 }, + { -1000, 500 }, + { -1000, -500 }, + }, + }; + + DoOvalPointTestChecks( testcase ); +} + +BOOST_AUTO_TEST_CASE( SimpleOval45Degrees ) +{ + // In this case, it's useful to keep in mind the hypotenuse of + // isoceles right-angled triangles is sqrt(2) times the length of the sides + // 500 / sqrt(2) = 354 + // 1000 / sqrt(2) = 707 + // 1500 / sqrt(2) = 1061 + // 2000 / sqrt(2) = 1414 + + const OVAL_POINTS_TEST_CASE testcase + { + { 4000, 1000 }, + { 45, DEGREES_T }, + { + { 0, 0 }, + // Main points + { 1414, -1414 }, + { -1414, 1414 }, + { 354, 354 }, + { -354, -354 }, + // Side segment ends + { -1414, 707 }, + { 1414, -707 }, + { -707, 1414 }, + { 707, -1414 }, + // Cap centres + { 1061, -1061 }, + { -1061, 1061 }, + // Extremum points (always one of NSEW of a cap centre because 45 degrees) + { -1061 - 500, 1061 }, + { -1061, 1061 + 500 }, + { 1061 + 500, -1061 }, + { 1061, -1061 - 500 }, + }, + }; + + DoOvalPointTestChecks( testcase ); +} + +BOOST_AUTO_TEST_SUITE_END()