7
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:
John Beard 2024-08-07 23:01:32 -06:00
parent b6734c42f0
commit 7fc367e688
12 changed files with 570 additions and 153 deletions

View File

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

View File

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

View File

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

View 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,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@
#include <boost/test/unit_test.hpp>
#include <geometry/chamfer.h>
#include <geometry/corner_operations.h>
#include "geom_test_utils.h"

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