mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-04-19 00:21:36 +00:00
Pcbnew: Add dogbone corner tool
This adds circular arcs in corners to allow for the router cutter radius when routing a slot or corner that receives a sharp corner. Fixes: https://gitlab.com/kicad/code/kicad/-/issues/18512
This commit is contained in:
parent
b6734c42f0
commit
7fc367e688
libs/kimath
pcbnew/tools
qa/tests/libs/kimath
@ -11,10 +11,10 @@ set( KIMATH_SRCS
|
||||
src/transform.cpp
|
||||
src/trigo.cpp
|
||||
|
||||
src/geometry/chamfer.cpp
|
||||
src/geometry/corner_operations.cpp
|
||||
src/geometry/distribute.cpp
|
||||
src/geometry/eda_angle.cpp
|
||||
src/geometry/ellipse.cpp
|
||||
src/geometry/eda_angle.cpp
|
||||
src/geometry/ellipse.cpp
|
||||
src/geometry/circle.cpp
|
||||
src/geometry/convex_hull.cpp
|
||||
src/geometry/direction_45.cpp
|
||||
|
@ -58,5 +58,38 @@ struct CHAMFER_RESULT
|
||||
std::optional<CHAMFER_RESULT> ComputeChamferPoints( const SEG& aSegA, const SEG& aSegB,
|
||||
const CHAMFER_PARAMS& aChamferParams );
|
||||
|
||||
struct DOGBONE_RESULT
|
||||
{
|
||||
// For lack of a generic ARC class, we can just use the points
|
||||
// that define the arc
|
||||
VECTOR2I m_arc_start;
|
||||
VECTOR2I m_arc_mid;
|
||||
VECTOR2I m_arc_end;
|
||||
|
||||
// The updated original segments
|
||||
// These can be empty if the dogbone "consumed" the original segments
|
||||
std::optional<SEG> m_updated_seg_a;
|
||||
std::optional<SEG> m_updated_seg_b;
|
||||
|
||||
// If the arc mouth is smaller than the radius
|
||||
bool m_small_arc_mouth;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the dogbone geometry for a given line pair and dogbone parameters.
|
||||
*
|
||||
* This turns a sharp internal corner into a "dogbone" shape, which allows it to be
|
||||
* routed with a cutter that has a radius, and still allows a sharp external corner
|
||||
* to fit into the space.
|
||||
*
|
||||
* _----_
|
||||
* ------/ |
|
||||
* ---------+ /
|
||||
* | |
|
||||
* | |
|
||||
*/
|
||||
std::optional<DOGBONE_RESULT> ComputeDogbone( const SEG& aSegA, const SEG& aSegB,
|
||||
int aDogboneRadius );
|
||||
|
||||
|
||||
#endif
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* 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 <geometry/chamfer.h>
|
||||
|
||||
std::optional<CHAMFER_RESULT> ComputeChamferPoints( const SEG& aSegA, const SEG& aSegB,
|
||||
const CHAMFER_PARAMS& aChamferParams )
|
||||
{
|
||||
const int line_a_setback = aChamferParams.m_chamfer_setback_a;
|
||||
const int line_b_setback = aChamferParams.m_chamfer_setback_b;
|
||||
|
||||
if( line_a_setback == 0 && line_b_setback == 0 )
|
||||
{
|
||||
// No chamfer to do
|
||||
// In theory a chamfer of 0 on one side is kind-of valid (adds a collinear point)
|
||||
// so allow it (using an and above, not an or)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if( aSegA.Length() < line_a_setback || aSegB.Length() < line_b_setback )
|
||||
{
|
||||
// Chamfer is too big for the line segments
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// We only support the case where the lines intersect at the end points
|
||||
// otherwise we would need to decide which inside corner to chamfer
|
||||
|
||||
// Figure out which end points are the ones at the intersection
|
||||
const VECTOR2I* a_pt = nullptr;
|
||||
const VECTOR2I* b_pt = nullptr;
|
||||
|
||||
if( aSegA.A == aSegB.A )
|
||||
{
|
||||
a_pt = &aSegA.A;
|
||||
b_pt = &aSegB.A;
|
||||
}
|
||||
else if( aSegA.A == aSegB.B )
|
||||
{
|
||||
a_pt = &aSegA.A;
|
||||
b_pt = &aSegB.B;
|
||||
}
|
||||
else if( aSegA.B == aSegB.A )
|
||||
{
|
||||
a_pt = &aSegA.B;
|
||||
b_pt = &aSegB.A;
|
||||
}
|
||||
else if( aSegA.B == aSegB.B )
|
||||
{
|
||||
a_pt = &aSegA.B;
|
||||
b_pt = &aSegB.B;
|
||||
}
|
||||
|
||||
if( !a_pt || !b_pt )
|
||||
{
|
||||
// No intersection found, so no chamfer to do
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// These are the other existing line points (the ones that are not the intersection)
|
||||
const VECTOR2I& a_end_pt = ( &aSegA.A == a_pt ) ? aSegA.B : aSegA.A;
|
||||
const VECTOR2I& b_end_pt = ( &aSegB.A == b_pt ) ? aSegB.B : aSegB.A;
|
||||
|
||||
// Now, construct segment of the set-back lengths, that begins
|
||||
// at the intersection point and is parallel to each line segments
|
||||
SEG setback_a( *a_pt, *b_pt + VECTOR2I( a_end_pt - *a_pt ).Resize( line_a_setback ) );
|
||||
SEG setback_b( *b_pt, *b_pt + VECTOR2I( b_end_pt - *b_pt ).Resize( line_b_setback ) );
|
||||
|
||||
// The chamfer segment then goes between the end points of the set-back segments
|
||||
SEG chamfer( setback_a.B, setback_b.B );
|
||||
|
||||
// The adjusted segments go from the old end points to the chamfer ends
|
||||
|
||||
std::optional<SEG> new_a;
|
||||
|
||||
if( a_end_pt != chamfer.A )
|
||||
new_a = SEG{ a_end_pt, chamfer.A };
|
||||
|
||||
std::optional<SEG> new_b;
|
||||
|
||||
if( b_end_pt != chamfer.B )
|
||||
new_b = SEG{ b_end_pt, chamfer.B };
|
||||
|
||||
return CHAMFER_RESULT{ chamfer, new_a, new_b };
|
||||
}
|
219
libs/kimath/src/geometry/corner_operations.cpp
Normal file
219
libs/kimath/src/geometry/corner_operations.cpp
Normal file
@ -0,0 +1,219 @@
|
||||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2023-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 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/corner_operations.h"
|
||||
|
||||
#include <geometry/circle.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the shared endpoint of two segments, if it exists, or std::nullopt
|
||||
* if the segments are not connected end-to-end.
|
||||
*/
|
||||
std::optional<VECTOR2I> GetSharedEndpoint( const SEG& aSegA, const SEG& aSegB )
|
||||
{
|
||||
if( aSegA.A == aSegB.A || aSegA.A == aSegB.B )
|
||||
{
|
||||
return aSegA.A;
|
||||
}
|
||||
else if( aSegA.B == aSegB.A || aSegA.B == aSegB.B )
|
||||
{
|
||||
return aSegA.B;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the end point of the segment that is _not_ the given point.
|
||||
*/
|
||||
VECTOR2I GetOtherPoint( const SEG& aSeg, const VECTOR2I& aPoint )
|
||||
{
|
||||
return ( aSeg.A == aPoint ) ? aSeg.B : aSeg.A;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the bisector of two segments that join at a corner.
|
||||
*/
|
||||
SEG GetBisectorOfCornerSegments( const SEG& aSegA, const SEG& aSegB, int aLength )
|
||||
{
|
||||
// Use the "parallelogram" method to find the bisector
|
||||
|
||||
// The intersection point of the two lines is the one that is shared by both segments
|
||||
const std::optional<VECTOR2I> corner = GetSharedEndpoint( aSegA, aSegB );
|
||||
|
||||
// Get the vector of a segment pointing away from a point
|
||||
const auto getSegVectorPointingAwayFrom = []( const SEG& aSeg,
|
||||
const VECTOR2I& aPoint ) -> VECTOR2I
|
||||
{
|
||||
const int distA = ( aSeg.A - aPoint ).EuclideanNorm();
|
||||
const int distB = ( aSeg.B - aPoint ).EuclideanNorm();
|
||||
|
||||
// If A is closer the segment is already pointing away from the corner
|
||||
// If B is closer, we need to reverse the segment
|
||||
return ( distA < distB ) ? ( aSeg.B - aSeg.A ) : ( aSeg.A - aSeg.B );
|
||||
};
|
||||
|
||||
// The vectors have to be the same length
|
||||
const int maxLen = std::max( aSegA.Length(), aSegB.Length() );
|
||||
const VECTOR2I aVecOutward = getSegVectorPointingAwayFrom( aSegA, *corner ).Resize( maxLen );
|
||||
const VECTOR2I bVecOutward = getSegVectorPointingAwayFrom( aSegB, *corner ).Resize( maxLen );
|
||||
const VECTOR2I bisectorOutward = aVecOutward + bVecOutward;
|
||||
|
||||
return SEG( *corner, *corner + bisectorOutward.Resize( aLength ) );
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<CHAMFER_RESULT> ComputeChamferPoints( const SEG& aSegA, const SEG& aSegB,
|
||||
const CHAMFER_PARAMS& aChamferParams )
|
||||
{
|
||||
const int line_a_setback = aChamferParams.m_chamfer_setback_a;
|
||||
const int line_b_setback = aChamferParams.m_chamfer_setback_b;
|
||||
|
||||
if( line_a_setback == 0 && line_b_setback == 0 )
|
||||
{
|
||||
// No chamfer to do
|
||||
// In theory a chamfer of 0 on one side is kind-of valid (adds a collinear point)
|
||||
// so allow it (using an and above, not an or)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if( aSegA.Length() < line_a_setback || aSegB.Length() < line_b_setback )
|
||||
{
|
||||
// Chamfer is too big for the line segments
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// We only support the case where the lines intersect at the end points
|
||||
// otherwise we would need to decide which inside corner to chamfer
|
||||
|
||||
// Figure out which end points are the ones at the intersection
|
||||
const std::optional<VECTOR2I> corner = GetSharedEndpoint( aSegA, aSegB );
|
||||
|
||||
if( !corner )
|
||||
{
|
||||
// The lines are not coterminous
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// These are the other existing line points (the ones that are not the intersection)
|
||||
const VECTOR2I a_end_pt = GetOtherPoint( aSegA, *corner );
|
||||
const VECTOR2I b_end_pt = GetOtherPoint( aSegB, *corner );
|
||||
|
||||
// Now, construct segment of the set-back lengths, that begins
|
||||
// at the intersection point and is parallel to each line segments
|
||||
SEG setback_a( *corner, *corner + VECTOR2I( a_end_pt - *corner ).Resize( line_a_setback ) );
|
||||
SEG setback_b( *corner, *corner + VECTOR2I( b_end_pt - *corner ).Resize( line_b_setback ) );
|
||||
|
||||
// The chamfer segment then goes between the end points of the set-back segments
|
||||
SEG chamfer( setback_a.B, setback_b.B );
|
||||
|
||||
// The adjusted segments go from the old end points to the chamfer ends
|
||||
|
||||
std::optional<SEG> new_a;
|
||||
|
||||
if( a_end_pt != chamfer.A )
|
||||
new_a = SEG{ a_end_pt, chamfer.A };
|
||||
|
||||
std::optional<SEG> new_b;
|
||||
|
||||
if( b_end_pt != chamfer.B )
|
||||
new_b = SEG{ b_end_pt, chamfer.B };
|
||||
|
||||
return CHAMFER_RESULT{ chamfer, new_a, new_b };
|
||||
}
|
||||
|
||||
|
||||
std::optional<DOGBONE_RESULT> ComputeDogbone( const SEG& aSegA, const SEG& aSegB,
|
||||
int aDogboneRadius )
|
||||
{
|
||||
const std::optional<VECTOR2I> corner = GetSharedEndpoint( aSegA, aSegB );
|
||||
|
||||
// Cannot handle parallel lines
|
||||
if( !corner || aSegA.Angle( aSegB ).IsHorizontal() )
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const SEG bisector = GetBisectorOfCornerSegments( aSegA, aSegB, aDogboneRadius );
|
||||
|
||||
// The dogbone center is the end of the bisector
|
||||
const VECTOR2I dogboneCenter = bisector.B;
|
||||
|
||||
// We can find the ends of the arc by considering the corner angle,
|
||||
// but it's easier to just intersect the circle with the original segments.
|
||||
|
||||
const CIRCLE circle( dogboneCenter, aDogboneRadius );
|
||||
|
||||
const std::vector<VECTOR2I> segAIntersections = circle.Intersect( aSegA );
|
||||
const std::vector<VECTOR2I> segBIntersections = circle.Intersect( aSegB );
|
||||
|
||||
// There can be a little bit of error in the intersection calculation
|
||||
static int s_epsilon = 8;
|
||||
|
||||
const auto getPointNotOnCorner =
|
||||
[&]( const std::vector<VECTOR2I>& aIntersections, const VECTOR2I& aCorner )
|
||||
{
|
||||
std::optional<VECTOR2I> result;
|
||||
for( const VECTOR2I& pt : aIntersections )
|
||||
{
|
||||
if( aCorner.Distance( pt ) > s_epsilon )
|
||||
{
|
||||
result = pt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const std::optional<VECTOR2I> ptOnSegA = getPointNotOnCorner( segAIntersections, *corner );
|
||||
const std::optional<VECTOR2I> ptOnSegB = getPointNotOnCorner( segBIntersections, *corner );
|
||||
|
||||
// The arc doesn't cross one or both of the segments
|
||||
if( !ptOnSegA || !ptOnSegB )
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const int mouth_width = SEG( *ptOnSegA, *ptOnSegB ).Length();
|
||||
|
||||
const VECTOR2I aOtherPtA = GetOtherPoint( aSegA, *corner );
|
||||
const VECTOR2I aOtherPtB = GetOtherPoint( aSegB, *corner );
|
||||
|
||||
// See if we need to update the original segments
|
||||
// or if the dogbone consumed them
|
||||
std::optional<SEG> new_a, new_b;
|
||||
if( aOtherPtA != *ptOnSegA )
|
||||
new_a = SEG{ aOtherPtA, *ptOnSegA };
|
||||
|
||||
if( aOtherPtB != *ptOnSegB )
|
||||
new_b = SEG{ aOtherPtB, *ptOnSegB };
|
||||
|
||||
return DOGBONE_RESULT{
|
||||
*ptOnSegA, *corner, *ptOnSegB, new_a, new_b, mouth_width < aDogboneRadius,
|
||||
};
|
||||
}
|
@ -182,6 +182,7 @@ static std::shared_ptr<CONDITIONAL_MENU> makeShapeModificationMenu( TOOL_INTERAC
|
||||
menu->AddItem( PCB_ACTIONS::simplifyPolygons, SELECTION_CONDITIONS::HasTypes( polygonSimplifyTypes ) );
|
||||
menu->AddItem( PCB_ACTIONS::filletLines, SELECTION_CONDITIONS::OnlyTypes( filletChamferTypes ) );
|
||||
menu->AddItem( PCB_ACTIONS::chamferLines, SELECTION_CONDITIONS::OnlyTypes( filletChamferTypes ) );
|
||||
menu->AddItem( PCB_ACTIONS::dogboneCorners, SELECTION_CONDITIONS::OnlyTypes( filletChamferTypes ) );
|
||||
menu->AddItem( PCB_ACTIONS::extendLines, SELECTION_CONDITIONS::OnlyTypes( lineExtendTypes )
|
||||
&& SELECTION_CONDITIONS::Count( 2 ) );
|
||||
menu->AddItem( PCB_ACTIONS::pointEditorMoveCorner, hasCornerCondition );
|
||||
@ -1183,26 +1184,25 @@ int EDIT_TOOL::FilletTracks( const TOOL_EVENT& aEvent )
|
||||
|
||||
|
||||
/**
|
||||
* Prompt the user for the fillet radius and return it.
|
||||
* Prompt the user for a radius and return it.
|
||||
*
|
||||
* @param aFrame
|
||||
* @param aErrorMsg filled with an error message if the parameter is invalid somehow
|
||||
* @return std::optional<int> the fillet radius or std::nullopt if no
|
||||
* valid fillet specified
|
||||
* @param aTitle the title of the dialog
|
||||
* @param aPersitentRadius the last used radius
|
||||
* @return std::optional<int> the radius or std::nullopt if no
|
||||
* valid radius specified
|
||||
*/
|
||||
static std::optional<int> GetFilletParams( PCB_BASE_EDIT_FRAME& aFrame )
|
||||
static std::optional<int> GetRadiusParams( PCB_BASE_EDIT_FRAME& aFrame, const wxString& aTitle,
|
||||
int& aPersitentRadius )
|
||||
{
|
||||
// Store last used fillet radius to allow pressing "enter" if repeat fillet is required
|
||||
static int filletRadius = 0;
|
||||
|
||||
WX_UNIT_ENTRY_DIALOG dlg( &aFrame, _( "Fillet Lines" ), _( "Radius:" ), filletRadius );
|
||||
WX_UNIT_ENTRY_DIALOG dlg( &aFrame, aTitle, _( "Radius:" ), aPersitentRadius );
|
||||
|
||||
if( dlg.ShowModal() == wxID_CANCEL || dlg.GetValue() == 0 )
|
||||
return std::nullopt;
|
||||
|
||||
filletRadius = dlg.GetValue();
|
||||
aPersitentRadius = dlg.GetValue();
|
||||
|
||||
return filletRadius;
|
||||
return aPersitentRadius;
|
||||
}
|
||||
|
||||
|
||||
@ -1401,13 +1401,26 @@ int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
|
||||
|
||||
if( aEvent.IsAction( &PCB_ACTIONS::filletLines ) )
|
||||
{
|
||||
std::optional<int> filletRadiusIU = GetFilletParams( *frame() );
|
||||
static int s_filletRadius = pcbIUScale.mmToIU( 1 );
|
||||
std::optional<int> filletRadiusIU =
|
||||
GetRadiusParams( *frame(), _( "Fillet Lines" ), s_filletRadius );
|
||||
|
||||
if( filletRadiusIU.has_value() )
|
||||
{
|
||||
pairwise_line_routine = std::make_unique<LINE_FILLET_ROUTINE>( frame()->GetModel(),
|
||||
change_handler,
|
||||
*filletRadiusIU );
|
||||
pairwise_line_routine = std::make_unique<LINE_FILLET_ROUTINE>(
|
||||
frame()->GetModel(), change_handler, *filletRadiusIU );
|
||||
}
|
||||
}
|
||||
else if( aEvent.IsAction( &PCB_ACTIONS::dogboneCorners ) )
|
||||
{
|
||||
static int s_dogBoneRadius = pcbIUScale.mmToIU( 1 );
|
||||
std::optional<int> radiusIU =
|
||||
GetRadiusParams( *frame(), _( "Dogbone Corners" ), s_dogBoneRadius );
|
||||
|
||||
if( radiusIU.has_value() )
|
||||
{
|
||||
pairwise_line_routine = std::make_unique<DOGBONE_CORNER_ROUTINE>(
|
||||
frame()->GetModel(), change_handler, *radiusIU );
|
||||
}
|
||||
}
|
||||
else if( aEvent.IsAction( &PCB_ACTIONS::chamferLines ) )
|
||||
@ -3173,6 +3186,7 @@ void EDIT_TOOL::rebuildConnectivity()
|
||||
}
|
||||
|
||||
|
||||
// clang-format off
|
||||
void EDIT_TOOL::setTransitions()
|
||||
{
|
||||
Go( &EDIT_TOOL::GetAndPlace, PCB_ACTIONS::getAndPlace.MakeEvent() );
|
||||
@ -3199,6 +3213,7 @@ void EDIT_TOOL::setTransitions()
|
||||
Go( &EDIT_TOOL::FilletTracks, PCB_ACTIONS::filletTracks.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::filletLines.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::chamferLines.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::dogboneCorners.MakeEvent() );
|
||||
Go( &EDIT_TOOL::SimplifyPolygons, PCB_ACTIONS::simplifyPolygons.MakeEvent() );
|
||||
Go( &EDIT_TOOL::HealShapes, PCB_ACTIONS::healShapes.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::extendLines.MakeEvent() );
|
||||
@ -3213,3 +3228,4 @@ void EDIT_TOOL::setTransitions()
|
||||
Go( &EDIT_TOOL::copyToClipboard, PCB_ACTIONS::copyWithReference.MakeEvent() );
|
||||
Go( &EDIT_TOOL::cutToClipboard, ACTIONS::cut.MakeEvent() );
|
||||
}
|
||||
// clang-format on
|
@ -22,7 +22,9 @@
|
||||
*/
|
||||
|
||||
#include "item_modification_routine.h"
|
||||
|
||||
#include <geometry/geometry_utils.h>
|
||||
#include <geometry/circle.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
@ -35,21 +37,47 @@ bool SegmentsShareEndpoint( const SEG& aSegA, const SEG& aSegB )
|
||||
return ( aSegA.A == aSegB.A || aSegA.A == aSegB.B || aSegA.B == aSegB.A || aSegA.B == aSegB.B );
|
||||
}
|
||||
|
||||
|
||||
std::pair<VECTOR2I*, VECTOR2I*> GetSharedEndpoints( SEG& aSegA, SEG& aSegB )
|
||||
{
|
||||
std::pair<VECTOR2I*, VECTOR2I*> result = { nullptr, nullptr };
|
||||
|
||||
if( aSegA.A == aSegB.A )
|
||||
{
|
||||
result = { &aSegA.A, &aSegB.A };
|
||||
}
|
||||
else if( aSegA.A == aSegB.B )
|
||||
{
|
||||
result = { &aSegA.A, &aSegB.B };
|
||||
}
|
||||
else if( aSegA.B == aSegB.A )
|
||||
{
|
||||
result = { &aSegA.B, &aSegB.A };
|
||||
}
|
||||
else if( aSegA.B == aSegB.B )
|
||||
{
|
||||
result = { &aSegA.B, &aSegB.B };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
bool ITEM_MODIFICATION_ROUTINE::ModifyLineOrDeleteIfZeroLength( PCB_SHAPE& aLine, const SEG& aSeg )
|
||||
bool ITEM_MODIFICATION_ROUTINE::ModifyLineOrDeleteIfZeroLength( PCB_SHAPE& aLine,
|
||||
const std::optional<SEG>& aSeg )
|
||||
{
|
||||
wxASSERT_MSG( aLine.GetShape() == SHAPE_T::SEGMENT, "Can only modify segments" );
|
||||
|
||||
const bool removed = aSeg.Length() == 0;
|
||||
const bool removed = !aSeg.has_value() || aSeg->Length() == 0;
|
||||
|
||||
if( !removed )
|
||||
{
|
||||
// Mark modified, then change it
|
||||
GetHandler().MarkItemModified( aLine );
|
||||
aLine.SetStart( aSeg.A );
|
||||
aLine.SetEnd( aSeg.B );
|
||||
aLine.SetStart( aSeg->A );
|
||||
aLine.SetEnd( aSeg->B );
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -88,32 +116,15 @@ void LINE_FILLET_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB
|
||||
|
||||
SEG seg_a( aLineA.GetStart(), aLineA.GetEnd() );
|
||||
SEG seg_b( aLineB.GetStart(), aLineB.GetEnd() );
|
||||
VECTOR2I* a_pt;
|
||||
VECTOR2I* b_pt;
|
||||
|
||||
if( seg_a.A == seg_b.A )
|
||||
auto [a_pt, b_pt] = GetSharedEndpoints( seg_a, seg_b );
|
||||
|
||||
if( !a_pt || !b_pt )
|
||||
{
|
||||
a_pt = &seg_a.A;
|
||||
b_pt = &seg_b.A;
|
||||
}
|
||||
else if( seg_a.A == seg_b.B )
|
||||
{
|
||||
a_pt = &seg_a.A;
|
||||
b_pt = &seg_b.B;
|
||||
}
|
||||
else if( seg_a.B == seg_b.A )
|
||||
{
|
||||
a_pt = &seg_a.B;
|
||||
b_pt = &seg_b.A;
|
||||
}
|
||||
else if( seg_a.B == seg_b.B )
|
||||
{
|
||||
a_pt = &seg_a.B;
|
||||
b_pt = &seg_b.B;
|
||||
}
|
||||
else
|
||||
// Nothing to do
|
||||
// The lines do not share an endpoint, so we can't fillet them
|
||||
AddFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
if( seg_a.Angle( seg_b ).IsHorizontal() )
|
||||
return;
|
||||
@ -240,6 +251,98 @@ void LINE_CHAMFER_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB
|
||||
}
|
||||
|
||||
|
||||
wxString DOGBONE_CORNER_ROUTINE::GetCommitDescription() const
|
||||
{
|
||||
return _( "Dogbone Corners" );
|
||||
}
|
||||
|
||||
|
||||
std::optional<wxString> DOGBONE_CORNER_ROUTINE::GetStatusMessage() const
|
||||
{
|
||||
wxString msg;
|
||||
|
||||
if( GetSuccesses() == 0 )
|
||||
{
|
||||
msg += _( "Unable to add dogbone corners to the selected lines." );
|
||||
}
|
||||
else if( GetFailures() > 0 )
|
||||
{
|
||||
msg += _( "Some of the lines could not have dogbone corners added." );
|
||||
}
|
||||
|
||||
if( m_haveNarrowMouths )
|
||||
{
|
||||
if( !msg.empty() )
|
||||
msg += " ";
|
||||
|
||||
msg += _( "Some of the dogbone corners are too narrow to fit a "
|
||||
"cutter of the specified radius." );
|
||||
}
|
||||
|
||||
if( msg.empty() )
|
||||
return std::nullopt;
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
||||
void DOGBONE_CORNER_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB )
|
||||
{
|
||||
if( aLineA.GetLength() == 0.0 || aLineB.GetLength() == 0.0 )
|
||||
return;
|
||||
|
||||
SEG seg_a( aLineA.GetStart(), aLineA.GetEnd() );
|
||||
SEG seg_b( aLineB.GetStart(), aLineB.GetEnd() );
|
||||
|
||||
auto [a_pt, b_pt] = GetSharedEndpoints( seg_a, seg_b );
|
||||
|
||||
if( !a_pt || !b_pt )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cannot handle parallel lines
|
||||
if( seg_a.Angle( seg_b ).IsHorizontal() )
|
||||
{
|
||||
AddFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<DOGBONE_RESULT> dogbone_result =
|
||||
ComputeDogbone( seg_a, seg_b, m_dogboneRadiusIU );
|
||||
|
||||
if( !dogbone_result )
|
||||
{
|
||||
AddFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
if( dogbone_result->m_small_arc_mouth )
|
||||
{
|
||||
// The arc is too small to fit the radius
|
||||
m_haveNarrowMouths = true;
|
||||
}
|
||||
|
||||
auto tArc = std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::ARC );
|
||||
|
||||
tArc->SetArcGeometry( dogbone_result->m_arc_start, dogbone_result->m_arc_mid,
|
||||
dogbone_result->m_arc_end );
|
||||
|
||||
// Copy properties from one of the source lines
|
||||
tArc->SetWidth( aLineA.GetWidth() );
|
||||
tArc->SetLayer( aLineA.GetLayer() );
|
||||
tArc->SetLocked( aLineA.IsLocked() );
|
||||
|
||||
CHANGE_HANDLER& handler = GetHandler();
|
||||
handler.AddNewItem( std::move( tArc ) );
|
||||
|
||||
ModifyLineOrDeleteIfZeroLength( aLineA, dogbone_result->m_updated_seg_a );
|
||||
ModifyLineOrDeleteIfZeroLength( aLineB, dogbone_result->m_updated_seg_b );
|
||||
|
||||
AddSuccess();
|
||||
}
|
||||
|
||||
|
||||
wxString LINE_EXTENSION_ROUTINE::GetCommitDescription() const
|
||||
{
|
||||
return _( "Extend Lines to Meet" );
|
||||
|
@ -32,7 +32,7 @@
|
||||
#include <board_item.h>
|
||||
#include <pcb_shape.h>
|
||||
|
||||
#include <geometry/chamfer.h>
|
||||
#include <geometry/corner_operations.h>
|
||||
|
||||
/**
|
||||
* @brief An object that has the ability to modify items on a board
|
||||
@ -198,7 +198,7 @@ protected:
|
||||
* @param aItem the line to modify
|
||||
* @param aSeg the new line geometry
|
||||
*/
|
||||
bool ModifyLineOrDeleteIfZeroLength( PCB_SHAPE& aItem, const SEG& aSeg );
|
||||
bool ModifyLineOrDeleteIfZeroLength( PCB_SHAPE& aItem, const std::optional<SEG>& aSeg );
|
||||
|
||||
/**
|
||||
* @brief Access the handler for making changes to the board
|
||||
@ -301,6 +301,28 @@ public:
|
||||
void ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB ) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pairwise add dogbone corners to an internal corner.
|
||||
*/
|
||||
class DOGBONE_CORNER_ROUTINE : public PAIRWISE_LINE_ROUTINE
|
||||
{
|
||||
public:
|
||||
DOGBONE_CORNER_ROUTINE( BOARD_ITEM* aBoard, CHANGE_HANDLER& aHandler, int aDogboneRadiusIU ) :
|
||||
PAIRWISE_LINE_ROUTINE( aBoard, aHandler ), m_dogboneRadiusIU( aDogboneRadiusIU ),
|
||||
m_haveNarrowMouths( false )
|
||||
{
|
||||
}
|
||||
|
||||
wxString GetCommitDescription() const override;
|
||||
std::optional<wxString> GetStatusMessage() const override;
|
||||
|
||||
void ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB ) override;
|
||||
|
||||
private:
|
||||
int m_dogboneRadiusIU;
|
||||
bool m_haveNarrowMouths;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A routine that modifies polygons using boolean operations
|
||||
|
@ -622,6 +622,12 @@ TOOL_ACTION PCB_ACTIONS::chamferLines( TOOL_ACTION_ARGS()
|
||||
.Tooltip( _( "Cut away corners between selected lines" ) )
|
||||
.Icon( BITMAPS::chamfer ) );
|
||||
|
||||
TOOL_ACTION PCB_ACTIONS::dogboneCorners( TOOL_ACTION_ARGS()
|
||||
.Name( "pcbnew.InteractiveEdit.dogboneCorners" )
|
||||
.Scope( AS_GLOBAL )
|
||||
.FriendlyName( _( "Dogbone Corners..." ) )
|
||||
.Tooltip( _( "Add dogbone corners to selected lines" ) ) );
|
||||
|
||||
TOOL_ACTION PCB_ACTIONS::simplifyPolygons( TOOL_ACTION_ARGS()
|
||||
.Name( "pcbnew.InteractiveEdit.simplifyPolygons" )
|
||||
.Scope( AS_GLOBAL )
|
||||
|
@ -158,6 +158,8 @@ public:
|
||||
static TOOL_ACTION filletLines;
|
||||
/// Chamfer (i.e. adds a straight line) all selected straight lines by a user defined setback
|
||||
static TOOL_ACTION chamferLines;
|
||||
/// Add "dogbone" corners to selected lines to allow routing with a cutter radius
|
||||
static TOOL_ACTION dogboneCorners;
|
||||
/// Connect selected shapes, possibly extending or cutting them, or adding extra geometry
|
||||
static TOOL_ACTION healShapes;
|
||||
/// Extend selected lines to meet at a point
|
||||
|
@ -29,6 +29,7 @@ set( QA_KIMATH_SRCS
|
||||
|
||||
geometry/test_chamfer.cpp
|
||||
geometry/test_distribute.cpp
|
||||
geometry/test_dogbone.cpp
|
||||
geometry/test_eda_angle.cpp
|
||||
geometry/test_ellipse_to_bezier.cpp
|
||||
geometry/test_fillet.cpp
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <geometry/chamfer.h>
|
||||
#include <geometry/corner_operations.h>
|
||||
|
||||
#include "geom_test_utils.h"
|
||||
|
||||
|
120
qa/tests/libs/kimath/geometry/test_dogbone.cpp
Normal file
120
qa/tests/libs/kimath/geometry/test_dogbone.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 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/corner_operations.h>
|
||||
|
||||
#include "geom_test_utils.h"
|
||||
|
||||
struct DogboneFixture
|
||||
{
|
||||
};
|
||||
|
||||
/**
|
||||
* Declares the DogboneFixture struct as the boost test fixture.
|
||||
*/
|
||||
BOOST_FIXTURE_TEST_SUITE( Dogbone, DogboneFixture )
|
||||
|
||||
struct TWO_LINE_CHAMFER_TEST_CASE
|
||||
{
|
||||
SEG m_seg_a;
|
||||
SEG m_seg_b;
|
||||
int m_radius;
|
||||
std::optional<DOGBONE_RESULT> m_expected_result;
|
||||
};
|
||||
|
||||
static void DoDogboneTestChecks( const TWO_LINE_CHAMFER_TEST_CASE& aTestCase )
|
||||
{
|
||||
// Actally do the chamfer
|
||||
const std::optional<DOGBONE_RESULT> dogbone_result =
|
||||
ComputeDogbone( aTestCase.m_seg_a, aTestCase.m_seg_b, aTestCase.m_radius );
|
||||
|
||||
BOOST_REQUIRE_EQUAL( dogbone_result.has_value(), aTestCase.m_expected_result.has_value() );
|
||||
|
||||
if( dogbone_result.has_value() )
|
||||
{
|
||||
const DOGBONE_RESULT& expected_result = aTestCase.m_expected_result.value();
|
||||
const DOGBONE_RESULT& actual_result = dogbone_result.value();
|
||||
|
||||
const SEG expected_arc_chord =
|
||||
SEG( expected_result.m_arc_start, expected_result.m_arc_end );
|
||||
const SEG actual_arc_chord = SEG( actual_result.m_arc_start, actual_result.m_arc_end );
|
||||
|
||||
BOOST_CHECK_PREDICATE( GEOM_TEST::SegmentsHaveSameEndPoints,
|
||||
(actual_arc_chord) ( expected_arc_chord ) );
|
||||
BOOST_CHECK_EQUAL( actual_result.m_arc_mid, expected_result.m_arc_mid );
|
||||
|
||||
const auto check_updated_seg =
|
||||
[&]( const std::optional<SEG>& updated_seg, const std::optional<SEG>& expected_seg )
|
||||
{
|
||||
BOOST_REQUIRE_EQUAL( updated_seg.has_value(), expected_seg.has_value() );
|
||||
|
||||
if( updated_seg.has_value() )
|
||||
{
|
||||
BOOST_CHECK_PREDICATE( GEOM_TEST::SegmentsHaveSameEndPoints,
|
||||
( *updated_seg )( *expected_seg ) );
|
||||
}
|
||||
};
|
||||
|
||||
check_updated_seg( actual_result.m_updated_seg_a, expected_result.m_updated_seg_a );
|
||||
check_updated_seg( actual_result.m_updated_seg_b, expected_result.m_updated_seg_b );
|
||||
|
||||
BOOST_CHECK_EQUAL( actual_result.m_small_arc_mouth, expected_result.m_small_arc_mouth );
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( SimpleRightAngleAtOrigin )
|
||||
{
|
||||
/* /---_
|
||||
* / +----+-------------> 1000
|
||||
* | | \
|
||||
* \| (0,0)
|
||||
* +
|
||||
* |
|
||||
* v
|
||||
*/
|
||||
|
||||
const TWO_LINE_CHAMFER_TEST_CASE testcase{
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 1000 ) },
|
||||
100,
|
||||
// A right angle is an easy one to see, because the end and center points are
|
||||
// all on 45 degree lines from the center
|
||||
{
|
||||
{
|
||||
VECTOR2I( 141, 0 ),
|
||||
VECTOR2I( 0, 0 ),
|
||||
VECTOR2I( 0, 141 ),
|
||||
SEG( VECTOR2I( 141, 0 ), VECTOR2I( 1000, 0 ) ),
|
||||
SEG( VECTOR2I( 0, 141 ), VECTOR2I( 0, 1000 ) ),
|
||||
false,
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
DoDogboneTestChecks( testcase );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
Loading…
Reference in New Issue
Block a user