7
mirror of https://gitlab.com/kicad/code/kicad.git synced 2025-04-07 18:45:32 +00:00

Break out some geom functions from dimension text adjustment

Put these in a separate header - they don't need to be available
whenever VECTOR2 is, but they are generic, reusable functions that
can have the corner cases defined and tested.
This commit is contained in:
John Beard 2024-09-08 21:45:19 +01:00
parent 80abdee90d
commit 5925f9c374
6 changed files with 301 additions and 30 deletions
libs/kimath
pcbnew/tools
qa/tests/libs/kimath

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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