diff --git a/libs/kimath/CMakeLists.txt b/libs/kimath/CMakeLists.txt index ff22ec2d27..991aa0c7bb 100644 --- a/libs/kimath/CMakeLists.txt +++ b/libs/kimath/CMakeLists.txt @@ -31,6 +31,7 @@ set( KIMATH_SRCS src/geometry/shape_rect.cpp src/geometry/shape_compound.cpp src/geometry/shape_segment.cpp + src/geometry/vector_utils.cpp src/geometry/vertex_set.cpp diff --git a/libs/kimath/include/geometry/vector_utils.h b/libs/kimath/include/geometry/vector_utils.h new file mode 100644 index 0000000000..c89ca04273 --- /dev/null +++ b/libs/kimath/include/geometry/vector_utils.h @@ -0,0 +1,118 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2024 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 <math/vector2d.h> + +class SEG; + +namespace KIGEOM +{ + +/** + * @file vector_utils.h + * + * Supplemental functions for working with vectors and + * objects that interact with vectors. + */ + +/* + * Determine if a point is in a given direction from another point. + * + * This returns true if the vector from aFrom to aPoint is within + * 90 degrees of aDirection. + * + * ------> aDirection + * + * /-- aFrom + * O /-- aPoint + * O + * + * If the point is perpendicular to the direction, it is considered + * to NOT be in the direction (e.g. both the direction and the + * reversed direction would return false). + * + * @param aPoint The point to test. + * @param aDirection The direction vector. + * @param aFrom The point to test from. + * + * @return true if the point is in the direction. + */ +template <typename T> +bool PointIsInDirection( const VECTOR2<T>& aPoint, const VECTOR2<T>& aDirection, + const VECTOR2<T>& aFrom ) +{ + return ( aPoint - aFrom ).Dot( aDirection ) > 0; +} + + +/** + * Determine if a segment's vector is within 90 degrees of a given direction. + */ +bool SegIsInDirection( const SEG& aSeg, const VECTOR2I& aDirection ); + + +/** + * Determine if a point projects onto a segment. + * + * /--- projects /--- does not project + * o o + * | | + * |<------------>| x + * aSeg + */ +bool PointProjectsOntoSegment( const VECTOR2I& aPoint, const SEG& aSeg ); + + +/** + * Get the ratio of the vector to a point from the segment's start, + * compared to the segment's length. + * /--- aPoint + * A<---+-------->B <-- Length L + * | | + * >|----|<-- Length R + * + * The point doesn't have to lie on the segment. + */ +double GetLengthRatioFromStart( const VECTOR2I& aPoint, const SEG& aSeg ); + +/** + * Get the ratio of the vector to a point projected onto a segment + * from the start, relative to the segment's length. + * + * /--- projects + * o + * | + * A<---+-------->B <-- Length L + * | | + * >|----|<-- Length R + * + * The ratio is R / L. IF 0, the point is at A. If 1, the point is at B. + * It assumes the point projects onto the segment. + */ +double GetProjectedPointLengthRatio( const VECTOR2I& aPoint, const SEG& aSeg ); + + +/** + * Get the nearest end of a segment to a point. + * + * If equidistant, the start point is returned. + */ +const VECTOR2I& GetNearestEndpoint( const SEG& aSeg, const VECTOR2I& aPoint ); + +} // namespace KIGEOM diff --git a/libs/kimath/src/geometry/vector_utils.cpp b/libs/kimath/src/geometry/vector_utils.cpp new file mode 100644 index 0000000000..0277ff0c07 --- /dev/null +++ b/libs/kimath/src/geometry/vector_utils.cpp @@ -0,0 +1,67 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2024 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 "geometry/vector_utils.h" + +#include <geometry/seg.h> + +using namespace KIGEOM; + +bool KIGEOM::SegIsInDirection( const SEG& aSeg, const VECTOR2I& aDirection ) +{ + return PointIsInDirection( aSeg.B, aDirection, aSeg.A ); +}; + + +bool KIGEOM::PointProjectsOntoSegment( const VECTOR2I& aPoint, const SEG& aSeg ) +{ + // SEG::NearestPoint returns the end points if the projection is + // outside the segment + const VECTOR2I projected = aSeg.NearestPoint( aPoint ); + return projected != aSeg.A && projected != aSeg.B; +} + + +double KIGEOM::GetLengthRatioFromStart( const VECTOR2I& aPoint, const SEG& aSeg ) +{ + const double length = aSeg.Length(); + const double projected_length = ( aPoint - aSeg.A ).EuclideanNorm(); + return projected_length / length; +} + + +double KIGEOM::GetProjectedPointLengthRatio( const VECTOR2I& aPoint, const SEG& aSeg ) +{ + const VECTOR2I projected = aSeg.NearestPoint( aPoint ); + + if( projected == aSeg.A ) + return 0.0; + if( projected == aSeg.B ) + return 1.0; + + return GetLengthRatioFromStart( projected, aSeg ); +} + + +const VECTOR2I& KIGEOM::GetNearestEndpoint( const SEG& aSeg, const VECTOR2I& aPoint ) +{ + const double distToCBStart = aSeg.A.Distance( aPoint ); + const double distToCBEnd = aSeg.B.Distance( aPoint ); + return ( distToCBStart <= distToCBEnd ) ? aSeg.A : aSeg.B; +} \ No newline at end of file diff --git a/pcbnew/tools/pcb_point_editor.cpp b/pcbnew/tools/pcb_point_editor.cpp index f894b9ca05..daa468b3ad 100644 --- a/pcbnew/tools/pcb_point_editor.cpp +++ b/pcbnew/tools/pcb_point_editor.cpp @@ -33,6 +33,7 @@ using namespace std::placeholders; #include <view/view_controls.h> #include <gal/graphics_abstraction_layer.h> #include <geometry/seg.h> +#include <geometry/vector_utils.h> #include <confirm.h> #include <tools/pcb_actions.h> #include <tools/pcb_selection_tool.h> @@ -1277,20 +1278,21 @@ private: const EDA_ANGLE rotation = oldAngle - newAngle; // There are two modes - when the text is between the crossbar points, and when it's not. - if( !textIsOverCrossBar( m_oldCrossBar, m_originalTextPos ) ) + if( !KIGEOM::PointProjectsOntoSegment( m_originalTextPos, m_oldCrossBar ) ) { VECTOR2I rotTextOffsetFromCbCenter = m_originalTextPos - m_oldCrossBar.Center(); RotatePoint( rotTextOffsetFromCbCenter, rotation ); VECTOR2I rotTextOffsetFromCbEnd = - getTextOffsetFromCrossbarNearestEnd( m_oldCrossBar, m_originalTextPos ); + m_originalTextPos + - KIGEOM::GetNearestEndpoint( m_oldCrossBar, m_originalTextPos ); RotatePoint( rotTextOffsetFromCbEnd, rotation ); // Which of the two crossbar points is now in the right direction? They could be swapped over now. // If zero-length, doesn't matter, they're the same thing const bool startIsInOffsetDirection = - pointIsInDirection( m_dimension.GetCrossbarStart(), rotTextOffsetFromCbCenter, - newCrossBar.Center() ); + KIGEOM::PointIsInDirection( m_dimension.GetCrossbarStart(), + rotTextOffsetFromCbCenter, newCrossBar.Center() ); const VECTOR2I& newCbRefPt = startIsInOffsetDirection ? m_dimension.GetCrossbarStart() : m_dimension.GetCrossbarEnd(); @@ -1303,8 +1305,8 @@ private: // good place for it. Keep it the same distance from the crossbar line, but rotated as needed. const VECTOR2I origTextPointProjected = m_oldCrossBar.NearestPoint( m_originalTextPos ); - const double oldRatio = ( origTextPointProjected - m_oldCrossBar.A ).EuclideanNorm() - / double( m_oldCrossBar.Length() ); + const double oldRatio = + KIGEOM::GetLengthRatioFromStart( origTextPointProjected, m_oldCrossBar ); // Perpendicular from the crossbar line to the text position // We need to keep this length constant @@ -1315,30 +1317,6 @@ private: return newProjected + rotCbNormalToText; } - static bool pointIsInDirection( const VECTOR2I& aPoint, const VECTOR2I& aDirection, - const VECTOR2I& aFrom ) - { - return ( aPoint - aFrom ).Dot( aDirection ) > 0; - }; - - static bool textIsOverCrossBar( const SEG& aCrossbar, const VECTOR2I& aTextPos ) - { - const VECTOR2I projected = aCrossbar.NearestPoint( aTextPos ); - return projected != aCrossbar.A && projected != aCrossbar.B; - } - - static VECTOR2I getTextOffsetFromCrossbarNearestEnd( const SEG& aCrossbar, - const VECTOR2I& aTextPos ) - { - const int distToCBStart = aCrossbar.A.Distance( aTextPos ); - const int distToCBEnd = aCrossbar.B.Distance( aTextPos ); - - const bool isNearerStart = distToCBStart < distToCBEnd; - - // This is the offset of the text from the nearer crossbar point - return aTextPos - ( isNearerStart ? aCrossbar.A : aCrossbar.B ); - } - PCB_DIM_ALIGNED& m_dimension; const VECTOR2I m_originalTextPos; const SEG m_oldCrossBar; diff --git a/qa/tests/libs/kimath/CMakeLists.txt b/qa/tests/libs/kimath/CMakeLists.txt index b05ac88851..82d03be9bd 100644 --- a/qa/tests/libs/kimath/CMakeLists.txt +++ b/qa/tests/libs/kimath/CMakeLists.txt @@ -46,6 +46,7 @@ set( QA_KIMATH_SRCS geometry/test_shape_poly_set_iterator.cpp geometry/test_shape_line_chain.cpp geometry/test_shape_line_chain_collision.cpp + geometry/test_vector_utils.cpp math/test_box2.cpp math/test_matrix3x3.cpp diff --git a/qa/tests/libs/kimath/geometry/test_vector_utils.cpp b/qa/tests/libs/kimath/geometry/test_vector_utils.cpp new file mode 100644 index 0000000000..1418de0669 --- /dev/null +++ b/qa/tests/libs/kimath/geometry/test_vector_utils.cpp @@ -0,0 +1,106 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2024 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. + * + */ + +#include <qa_utils/wx_utils/unit_test_utils.h> + +#include <geometry/vector_utils.h> +#include <geometry/seg.h> + + +BOOST_AUTO_TEST_SUITE( VectorUtils ) + +BOOST_AUTO_TEST_CASE( PointIsInDirection ) +{ + const VECTOR2I p0( 1000, 1000 ); + + const VECTOR2I ne( 100, 100 ); + const VECTOR2I n( 0, 100 ); + const VECTOR2I s( 0, -100 ); + + // To the east, so not directly inline with ne from p0 + const VECTOR2I p1_east = p0 + VECTOR2I( 1000, 0 ); + const VECTOR2I p1_west = p0 - VECTOR2I( 1000, 0 ); + + BOOST_TEST( KIGEOM::PointIsInDirection( p1_east, ne, p0 ) ); + BOOST_TEST( !KIGEOM::PointIsInDirection( p1_west, ne, p0 ) ); + + // Test the perpendicular corner case + + // Points on both sides are not in the direction + BOOST_TEST( !KIGEOM::PointIsInDirection( p1_east, n, p0 ) ); + BOOST_TEST( !KIGEOM::PointIsInDirection( p1_west, n, p0 ) ); + + // And they're also not in the opposite direction + BOOST_TEST( !KIGEOM::PointIsInDirection( p1_east, s, p0 ) ); + BOOST_TEST( !KIGEOM::PointIsInDirection( p1_west, s, p0 ) ); +} + +BOOST_AUTO_TEST_CASE( ProjectsOntoSeg ) +{ + const SEG seg( VECTOR2I( 0, 0 ), VECTOR2I( 100, 0 ) ); + + BOOST_TEST( KIGEOM::PointProjectsOntoSegment( VECTOR2I( 50, 1000 ), seg ) ); + BOOST_TEST( !KIGEOM::PointProjectsOntoSegment( VECTOR2I( 150, 1000 ), seg ) ); +} + +BOOST_AUTO_TEST_CASE( LengthRatio ) +{ + const SEG seg( VECTOR2I( 0, 0 ), VECTOR2I( 100, 0 ) ); + + BOOST_TEST( KIGEOM::GetLengthRatioFromStart( VECTOR2I( 0, 0 ), seg ) == 0.0 ); + BOOST_TEST( KIGEOM::GetLengthRatioFromStart( VECTOR2I( 100, 0 ), seg ) == 1.0 ); + BOOST_TEST( KIGEOM::GetLengthRatioFromStart( VECTOR2I( 50, 0 ), seg ) == 0.5 ); + + // Points not on the segment also work + BOOST_CHECK_CLOSE( KIGEOM::GetLengthRatioFromStart( VECTOR2I( 0, 100 ), seg ), 1.0, 0.0001 ); + BOOST_CHECK_CLOSE( KIGEOM::GetLengthRatioFromStart( VECTOR2I( 0, 50 ), seg ), 0.5, 0.0001 ); +} + +BOOST_AUTO_TEST_CASE( NearestEndpoint ) +{ + struct PtCase + { + VECTOR2I Point; + bool ExpectedStart; + }; + + const SEG seg( VECTOR2I( 0, 0 ), VECTOR2I( 100, 0 ) ); + + const std::vector<PtCase> cases{ + { { -100, 0 }, true }, + { { 0, 0 }, true }, + { { 0, 50 }, true }, + // Make sure the tie breaks predictably + // Equidistant points -> start + { { 50, 0 }, true }, + { { 50, 50 }, true }, + { { 50, -50 }, true }, + // End points + { { 200, 0 }, false }, + { { 100, 0 }, false }, + { { 100, 50 }, false }, + }; + + for( const PtCase& pc : cases ) + { + BOOST_TEST_INFO( "Point: " << pc.Point ); + BOOST_TEST( KIGEOM::GetNearestEndpoint( seg, pc.Point ) + == ( pc.ExpectedStart ? seg.A : seg.B ) ); + } +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file