Fix oval pad snapping

Previously the snap points computed for oval pads didn't get all the
points correct. This breaks out the "find snap points for ovals"
into a function, reworks the logic, adds some tests.

Also adds "extremum points" when the oval isn't exactly H/V.

(cherry picked from commit 78c8de9b08)

Fixes: https://gitlab.com/kicad/code/kicad/-/issues/15594
This commit is contained in:
John Beard 2023-09-10 00:49:03 +01:00
parent 81ebb1030a
commit c7a4ea97c3
7 changed files with 410 additions and 28 deletions

View File

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

View File

@ -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 <vector>
#include <math/vector2d.h>
#include <geometry/eda_angle.h>
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<VECTOR2I> - The list of points
*/
std::vector<VECTOR2I> GetOvalKeyPoints( const VECTOR2I& aOvalSize, const EDA_ANGLE& aRotation,
OVAL_KEY_POINT_FLAGS aFlags );
#endif /* GEOMETRY_OVAL_H_ */

View File

@ -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 <tomasz.wlostowski@cern.ch>
*
* 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 <trigo.h> // for RotatePoint
std::vector<VECTOR2I> 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<VECTOR2I> 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;
}

View File

@ -23,7 +23,10 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "pcb_grid_helper.h"
#include <functional>
#include <pcb_dimension.h>
#include <fp_shape.h>
#include <footprint.h>
@ -31,6 +34,7 @@
#include <pcb_group.h>
#include <pcb_track.h>
#include <zone.h>
#include <geometry/oval.h>
#include <geometry/shape_circle.h>
#include <geometry/shape_line_chain.h>
#include <geometry/shape_rect.h>
@ -43,8 +47,6 @@
#include <tool/tool_manager.h>
#include <tools/pcb_tool_base.h>
#include <view/view.h>
#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<int>& 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<VECTOR2I>{
{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<VECTOR2I> 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<VECTOR2I> 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;
}

View File

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

View File

@ -332,6 +332,7 @@ struct print_log_value<SHAPE_LINE_CHAIN>
os << "]";
}
};
}
BOOST_TEST_PRINT_NAMESPACE_CLOSE

View File

@ -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 <boost/test/unit_test.hpp>
#include <geometry/oval.h>
#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 <typename T>
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<VECTOR2I> 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<VECTOR2I::coord_type>( a, b ) > 0;
};
std::vector<VECTOR2I> expected_points = testcase.m_expected_points;
std::vector<VECTOR2I> 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()