ADDED: Pcbnew chamfer and extend tools
Using the new ITEM_MODIFICATION_ROUTINE system, drop in two new tools: chamfer and line extend. These are two geometric operations that are relatively common when editing footprints in particular. Chamfer delegates the geometric calculations to a dedicated unit in kimath/geometry.
This commit is contained in:
parent
8e0e9ce752
commit
046d978ba7
|
@ -10,6 +10,7 @@ set( KIMATH_SRCS
|
|||
src/md5_hash.cpp
|
||||
src/trigo.cpp
|
||||
|
||||
src/geometry/chamfer.cpp
|
||||
src/geometry/eda_angle.cpp
|
||||
src/geometry/ellipse.cpp
|
||||
src/geometry/circle.cpp
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 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
|
||||
*/
|
||||
|
||||
#ifndef __CHAMFER_H
|
||||
#define __CHAMFER_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <geometry/seg.h>
|
||||
|
||||
/**
|
||||
* Parameters that define a simple chamfer operation.
|
||||
*/
|
||||
struct CHAMFER_PARAMS
|
||||
{
|
||||
/// Chamfer set-back distance along the first line
|
||||
int m_chamfer_setback_a_IU;
|
||||
/// Chamfer set-back distance along the second line
|
||||
int m_chamfer_setback_b_IU;
|
||||
};
|
||||
|
||||
struct CHAMFER_RESULT
|
||||
{
|
||||
// The chamfer segment
|
||||
SEG m_chamfer;
|
||||
|
||||
// The updated original segments
|
||||
// These can be empty if the chamfer "consumed" "he original segments
|
||||
std::optional<SEG> m_updated_seg_a;
|
||||
std::optional<SEG> m_updated_seg_b;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the chamfer points for a given line pair and chamfer parameters.
|
||||
*/
|
||||
std::optional<CHAMFER_RESULT> ComputeChamferPoints( const SEG aSegA, const SEG& aSegB,
|
||||
const CHAMFER_PARAMS& aChamferParams );
|
||||
|
||||
|
||||
#endif
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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_IU;
|
||||
const int line_b_setback = aChamferParams.m_chamfer_setback_b_IU;
|
||||
|
||||
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 };
|
||||
}
|
|
@ -204,10 +204,11 @@ bool EDIT_TOOL::Init()
|
|||
PCB_ARC_T,
|
||||
PCB_VIA_T };
|
||||
|
||||
static std::vector<KICAD_T> filletTypes = { PCB_SHAPE_LOCATE_POLY_T,
|
||||
PCB_SHAPE_LOCATE_RECT_T,
|
||||
PCB_SHAPE_LOCATE_SEGMENT_T };
|
||||
static std::vector<KICAD_T> filletChamferTypes = { PCB_SHAPE_LOCATE_POLY_T,
|
||||
PCB_SHAPE_LOCATE_RECT_T,
|
||||
PCB_SHAPE_LOCATE_SEGMENT_T };
|
||||
|
||||
static std::vector<KICAD_T> lineExtendTypes = { PCB_SHAPE_LOCATE_SEGMENT_T };
|
||||
|
||||
// Add context menu entries that are displayed when selection tool is active
|
||||
CONDITIONAL_MENU& menu = m_selectionTool->GetToolMenu().GetMenu();
|
||||
|
@ -229,7 +230,10 @@ bool EDIT_TOOL::Init()
|
|||
&& SELECTION_CONDITIONS::OnlyTypes( GENERAL_COLLECTOR::DraggableItems )
|
||||
&& !SELECTION_CONDITIONS::OnlyTypes( { PCB_FOOTPRINT_T } ) );
|
||||
menu.AddItem( PCB_ACTIONS::filletTracks, SELECTION_CONDITIONS::OnlyTypes( trackTypes ) );
|
||||
menu.AddItem( PCB_ACTIONS::filletLines, SELECTION_CONDITIONS::OnlyTypes( filletTypes ) );
|
||||
menu.AddItem( PCB_ACTIONS::filletLines, SELECTION_CONDITIONS::OnlyTypes( filletChamferTypes ) );
|
||||
menu.AddItem( PCB_ACTIONS::chamferLines, SELECTION_CONDITIONS::OnlyTypes( filletChamferTypes ) );
|
||||
menu.AddItem( PCB_ACTIONS::extendLines, SELECTION_CONDITIONS::OnlyTypes( lineExtendTypes )
|
||||
&& SELECTION_CONDITIONS::Count( 2 ) );
|
||||
menu.AddItem( PCB_ACTIONS::rotateCcw, SELECTION_CONDITIONS::NotEmpty );
|
||||
menu.AddItem( PCB_ACTIONS::rotateCw, SELECTION_CONDITIONS::NotEmpty );
|
||||
menu.AddItem( PCB_ACTIONS::flip, SELECTION_CONDITIONS::NotEmpty );
|
||||
|
@ -1048,8 +1052,45 @@ static std::optional<int> GetFilletParams( PCB_BASE_EDIT_FRAME& aFrame, wxString
|
|||
return filletRadiusIU;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user for chamfer parameters
|
||||
*
|
||||
* @param aFrame
|
||||
* @param aErrorMsg filled with an error message if the parameter is invalid somehow
|
||||
* @return std::optional<int> the chamfer parameters or std::nullopt if no
|
||||
* valid fillet specified
|
||||
*/
|
||||
static std::optional<CHAMFER_PARAMS> GetChamferParams( PCB_BASE_EDIT_FRAME& aFrame,
|
||||
wxString& aErrorMsg )
|
||||
{
|
||||
// Non-zero and the KLC default for Fab layer chamfers
|
||||
const int default_setback = pcbIUScale.mmToIU( 1 );
|
||||
// Store last used setback to allow pressing "enter" if repeat chamfer is required
|
||||
static CHAMFER_PARAMS params{ default_setback, default_setback };
|
||||
|
||||
int EDIT_TOOL::FilletLines( const TOOL_EVENT& aEvent )
|
||||
WX_UNIT_ENTRY_DIALOG dia( &aFrame, _( "Enter chamfer setback:" ), _( "Chamfer Lines" ),
|
||||
params.m_chamfer_setback_a_IU );
|
||||
|
||||
if( dia.ShowModal() == wxID_CANCEL )
|
||||
return std::nullopt;
|
||||
|
||||
params.m_chamfer_setback_a_IU = dia.GetValue();
|
||||
// It's hard to easily specify an asymmetric chamfer (which line gets the longer
|
||||
// setbeck?), so we just use the same setback for each
|
||||
params.m_chamfer_setback_b_IU = params.m_chamfer_setback_a_IU;
|
||||
|
||||
// Some technically-valid chamfers are not useful to actually do
|
||||
if( params.m_chamfer_setback_a_IU == 0 )
|
||||
{
|
||||
aErrorMsg = _( "A setback of zero was entered.\n"
|
||||
"The chamfer operation was not performed." );
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
|
||||
{
|
||||
PCB_SELECTION& selection = m_selectionTool->RequestSelection(
|
||||
[]( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
|
||||
|
@ -1183,6 +1224,30 @@ int EDIT_TOOL::FilletLines( const TOOL_EVENT& aEvent )
|
|||
*filletRadiusIU );
|
||||
}
|
||||
}
|
||||
else if( aEvent.IsAction( &PCB_ACTIONS::chamferLines ) )
|
||||
{
|
||||
const std::optional<CHAMFER_PARAMS> chamfer_params =
|
||||
GetChamferParams( *frame(), error_message );
|
||||
|
||||
if( chamfer_params.has_value() )
|
||||
{
|
||||
pairwise_line_routine = std::make_unique<LINE_CHAMFER_ROUTINE>(
|
||||
frame()->GetModel(), item_creation_handler, item_modification_handler,
|
||||
*chamfer_params );
|
||||
}
|
||||
}
|
||||
else if( aEvent.IsAction( &PCB_ACTIONS::extendLines ) )
|
||||
{
|
||||
if( selection.CountType( PCB_SHAPE_LOCATE_SEGMENT_T ) != 2 )
|
||||
{
|
||||
error_message = _( "Exactly two lines must be selected to extend them." );
|
||||
}
|
||||
else
|
||||
{
|
||||
pairwise_line_routine = std::make_unique<LINE_EXTENSION_ROUTINE>(
|
||||
frame()->GetModel(), item_creation_handler, item_modification_handler );
|
||||
}
|
||||
}
|
||||
|
||||
if( !pairwise_line_routine )
|
||||
{
|
||||
|
@ -2493,7 +2558,9 @@ void EDIT_TOOL::setTransitions()
|
|||
Go( &EDIT_TOOL::PackAndMoveFootprints, PCB_ACTIONS::packAndMoveFootprints.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ChangeTrackWidth, PCB_ACTIONS::changeTrackWidth.MakeEvent() );
|
||||
Go( &EDIT_TOOL::FilletTracks, PCB_ACTIONS::filletTracks.MakeEvent() );
|
||||
Go( &EDIT_TOOL::FilletLines, PCB_ACTIONS::filletLines.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::filletLines.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::chamferLines.MakeEvent() );
|
||||
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::extendLines.MakeEvent() );
|
||||
|
||||
Go( &EDIT_TOOL::copyToClipboard, ACTIONS::copy.MakeEvent() );
|
||||
Go( &EDIT_TOOL::copyToClipboard, PCB_ACTIONS::copyWithReference.MakeEvent() );
|
||||
|
|
|
@ -121,9 +121,10 @@ public:
|
|||
int FilletTracks( const TOOL_EVENT& aEvent );
|
||||
|
||||
/**
|
||||
* Fillet (i.e. adds an arc tangent to) all selected straight lines by a user defined radius.
|
||||
* "Modify" graphical lines. This includes operations such as filleting, chamfering,
|
||||
* extending to meet.
|
||||
*/
|
||||
int FilletLines( const TOOL_EVENT& aEvent );
|
||||
int ModifyLines( const TOOL_EVENT& aEvent );
|
||||
|
||||
/**
|
||||
* Delete currently selected items.
|
||||
|
|
|
@ -23,6 +23,18 @@
|
|||
|
||||
#include "item_modification_routine.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* Check if two segments share an endpoint (can be at either end of either segment)
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
wxString LINE_FILLET_ROUTINE::GetCommitDescription() const
|
||||
{
|
||||
|
@ -129,4 +141,130 @@ void LINE_FILLET_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB
|
|||
aLineB.SetEnd( seg_b.B );
|
||||
|
||||
AddSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
wxString LINE_CHAMFER_ROUTINE::GetCommitDescription() const
|
||||
{
|
||||
return _( "Chamfer Lines" );
|
||||
}
|
||||
|
||||
wxString LINE_CHAMFER_ROUTINE::GetCompleteFailureMessage() const
|
||||
{
|
||||
return _( "Unable to chamfer the selected lines." );
|
||||
}
|
||||
|
||||
wxString LINE_CHAMFER_ROUTINE::GetSomeFailuresMessage() const
|
||||
{
|
||||
return _( "Some of the lines could not be chamfered." );
|
||||
}
|
||||
|
||||
void LINE_CHAMFER_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() );
|
||||
|
||||
// If the segments share an endpoint, we won't try to chamfer them
|
||||
// (we could extend to the intersection point, but this gets complicated
|
||||
// and inconsistent when you select more than two lines)
|
||||
if( !SegmentsShareEndpoint( seg_a, seg_b ) )
|
||||
{
|
||||
// not an error, lots of lines in a 2+ line selection will not intersect
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<CHAMFER_RESULT> chamfer_result =
|
||||
ComputeChamferPoints( seg_a, seg_b, m_chamferParams );
|
||||
|
||||
if( !chamfer_result )
|
||||
{
|
||||
AddFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
auto tSegment = std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::SEGMENT );
|
||||
|
||||
tSegment->SetStart( chamfer_result->m_chamfer.A );
|
||||
tSegment->SetEnd( chamfer_result->m_chamfer.B );
|
||||
|
||||
// Copy properties from one of the source lines
|
||||
tSegment->SetWidth( aLineA.GetWidth() );
|
||||
tSegment->SetLayer( aLineA.GetLayer() );
|
||||
tSegment->SetLocked( aLineA.IsLocked() );
|
||||
|
||||
AddNewItem( std::move( tSegment ) );
|
||||
|
||||
MarkItemModified( aLineA );
|
||||
MarkItemModified( aLineB );
|
||||
|
||||
// Shorten the original lines
|
||||
aLineA.SetStart( chamfer_result->m_updated_seg_a->A );
|
||||
aLineA.SetEnd( chamfer_result->m_updated_seg_a->B );
|
||||
aLineB.SetStart( chamfer_result->m_updated_seg_b->A );
|
||||
aLineB.SetEnd( chamfer_result->m_updated_seg_b->B );
|
||||
|
||||
AddSuccess();
|
||||
}
|
||||
|
||||
|
||||
wxString LINE_EXTENSION_ROUTINE::GetCommitDescription() const
|
||||
{
|
||||
return _( "Extend Lines to Meet" );
|
||||
}
|
||||
|
||||
wxString LINE_EXTENSION_ROUTINE::GetCompleteFailureMessage() const
|
||||
{
|
||||
return _( "Unable to extend the selected lines to meet." );
|
||||
}
|
||||
|
||||
wxString LINE_EXTENSION_ROUTINE::GetSomeFailuresMessage() const
|
||||
{
|
||||
return _( "Some of the lines could not be extended to meet." );
|
||||
}
|
||||
|
||||
void LINE_EXTENSION_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() );
|
||||
|
||||
if( seg_a.Intersects( seg_b ) )
|
||||
{
|
||||
// already intersecting, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
OPT_VECTOR2I intersection = seg_a.IntersectLines( seg_b );
|
||||
|
||||
if( !intersection )
|
||||
{
|
||||
// This might be an error, but it's also possible that the lines are
|
||||
// parallel and don't intersect. We'll just ignore this case.
|
||||
return;
|
||||
}
|
||||
|
||||
const auto line_extender = [&]( const SEG& aSeg, PCB_SHAPE& aLine )
|
||||
{
|
||||
// If the intersection point is not already n the line, we'll extend to it
|
||||
if( !aSeg.Contains( *intersection ) )
|
||||
{
|
||||
const int dist_start = ( *intersection - aSeg.A ).EuclideanNorm();
|
||||
const int dist_end = ( *intersection - aSeg.B ).EuclideanNorm();
|
||||
|
||||
const VECTOR2I& furthest_pt = ( dist_start < dist_end ) ? aSeg.B : aSeg.A;
|
||||
|
||||
MarkItemModified( aLine );
|
||||
aLine.SetStart( furthest_pt );
|
||||
aLine.SetEnd( *intersection );
|
||||
}
|
||||
};
|
||||
|
||||
line_extender( seg_a, aLineA );
|
||||
line_extender( seg_b, aLineB );
|
||||
|
||||
AddSuccess();
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
#include <board_item.h>
|
||||
#include <pcb_shape.h>
|
||||
|
||||
#include <geometry/chamfer.h>
|
||||
|
||||
/**
|
||||
* @brief An object that has the ability to modify items on a board
|
||||
*
|
||||
|
@ -47,7 +49,7 @@ public:
|
|||
/*
|
||||
* Handlers for receiving changes from the tool
|
||||
*
|
||||
* These is used to allow the tool's caller to make changes to
|
||||
* These are used to allow the tool's caller to make changes to
|
||||
* affected board items using extra information that the tool
|
||||
* does not have access to (e.g. is this an FP editor, was
|
||||
* the line created from a rectangle and needs to be added, not
|
||||
|
@ -55,11 +57,20 @@ public:
|
|||
*
|
||||
* We can't store them up until the end, because modifications
|
||||
* need the old state to be known.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handler for creating a new item on the board
|
||||
*
|
||||
* @param PCB_SHAPE& the line to modify
|
||||
* @param bool true if the shape was created, false if it was only modified
|
||||
* @param PCB_SHAPE& the shape to add
|
||||
*/
|
||||
using CREATION_HANDLER = std::function<void( std::unique_ptr<PCB_SHAPE> )>;
|
||||
|
||||
/**
|
||||
* Handler for modifying an existing item on the board
|
||||
*
|
||||
* @param PCB_SHAPE& the shape to modify
|
||||
*/
|
||||
using MODIFICATION_HANDLER = std::function<void( PCB_SHAPE& )>;
|
||||
|
||||
ITEM_MODIFICATION_ROUTINE( BOARD_ITEM* aBoard, CREATION_HANDLER aCreationHandler,
|
||||
|
@ -171,4 +182,47 @@ private:
|
|||
int m_filletRadiusIU;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pairwise line tool that adds a chamfer between the lines.
|
||||
*/
|
||||
class LINE_CHAMFER_ROUTINE : public PAIRWISE_LINE_ROUTINE
|
||||
{
|
||||
public:
|
||||
LINE_CHAMFER_ROUTINE( BOARD_ITEM* aBoard, CREATION_HANDLER aCreationHandler,
|
||||
MODIFICATION_HANDLER aModificationHandler,
|
||||
CHAMFER_PARAMS aChamferParams ) :
|
||||
PAIRWISE_LINE_ROUTINE( aBoard, aCreationHandler, aModificationHandler ),
|
||||
m_chamferParams( std::move( aChamferParams ) )
|
||||
{
|
||||
}
|
||||
|
||||
wxString GetCommitDescription() const override;
|
||||
wxString GetCompleteFailureMessage() const override;
|
||||
wxString GetSomeFailuresMessage() const override;
|
||||
|
||||
void ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB ) override;
|
||||
|
||||
private:
|
||||
const CHAMFER_PARAMS m_chamferParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pairwise extend to corner or meeting tool
|
||||
*/
|
||||
class LINE_EXTENSION_ROUTINE : public PAIRWISE_LINE_ROUTINE
|
||||
{
|
||||
public:
|
||||
LINE_EXTENSION_ROUTINE( BOARD_ITEM* aBoard, CREATION_HANDLER aCreationHandler,
|
||||
MODIFICATION_HANDLER aModificationHandler ) :
|
||||
PAIRWISE_LINE_ROUTINE( aBoard, aCreationHandler, aModificationHandler )
|
||||
{
|
||||
}
|
||||
|
||||
wxString GetCommitDescription() const override;
|
||||
wxString GetCompleteFailureMessage() const override;
|
||||
wxString GetSomeFailuresMessage() const override;
|
||||
|
||||
void ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB ) override;
|
||||
};
|
||||
|
||||
#endif /* ITEM_MODIFICATION_ROUTINE_H_ */
|
|
@ -403,6 +403,16 @@ TOOL_ACTION PCB_ACTIONS::filletLines( "pcbnew.InteractiveEdit.filletLines",
|
|||
AS_GLOBAL, 0, "",
|
||||
_( "Fillet Lines" ), _( "Adds arcs tangent to the selected lines" ) );
|
||||
|
||||
TOOL_ACTION PCB_ACTIONS::chamferLines( "pcbnew.InteractiveEdit.chamferLines",
|
||||
AS_GLOBAL, 0, "",
|
||||
_( "Chamfer Lines" ),
|
||||
_( "Cut away corners between selected lines" ) );
|
||||
|
||||
TOOL_ACTION PCB_ACTIONS::extendLines( "pcbnew.InteractiveEdit.extendLines",
|
||||
AS_GLOBAL, 0, "",
|
||||
_( "Extend Lines to Meet" ),
|
||||
_( "Extend lines to meet each other" ) );
|
||||
|
||||
TOOL_ACTION PCB_ACTIONS::deleteFull( TOOL_ACTION_ARGS()
|
||||
.Name( "pcbnew.InteractiveEdit.deleteFull" )
|
||||
.Scope( AS_GLOBAL )
|
||||
|
|
|
@ -152,7 +152,13 @@ public:
|
|||
|
||||
/// Fillet (i.e. adds an arc tangent to) all selected straight tracks by a user defined radius
|
||||
static TOOL_ACTION filletTracks;
|
||||
|
||||
/// Fillet (i.e. adds an arc tangent to) all selected straight lines by a user defined radius
|
||||
static TOOL_ACTION filletLines;
|
||||
/// Chamfer (i.e. adds a straight line) all selected straight lines by a user defined setback
|
||||
static TOOL_ACTION chamferLines;
|
||||
/// Extend selected lines to meet at a point
|
||||
static TOOL_ACTION extendLines;
|
||||
|
||||
/// Activation of the edit tool
|
||||
static TOOL_ACTION properties;
|
||||
|
|
|
@ -27,6 +27,7 @@ set( QA_KIMATH_SRCS
|
|||
|
||||
test_kimath.cpp
|
||||
|
||||
geometry/test_chamfer.cpp
|
||||
geometry/test_eda_angle.cpp
|
||||
geometry/test_ellipse_to_bezier.cpp
|
||||
geometry/test_fillet.cpp
|
||||
|
|
|
@ -313,6 +313,17 @@ inline bool IsPolySetValid( const SHAPE_POLY_SET& aSet )
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check that two SEGs have the same end points, in either order
|
||||
*
|
||||
* That is to say SEG(A, B) == SEG(A, B), but also SEG(A, B) == SEG(B, A)
|
||||
*/
|
||||
inline bool SegmentsHaveSameEndPoints( const SEG& aSeg1, const SEG& aSeg2 )
|
||||
{
|
||||
return ( aSeg1.A == aSeg2.A && aSeg1.B == aSeg2.B )
|
||||
|| ( aSeg1.A == aSeg2.B && aSeg1.B == aSeg2.A );
|
||||
}
|
||||
|
||||
} // namespace GEOM_TEST
|
||||
|
||||
namespace BOOST_TEST_PRINT_NAMESPACE_OPEN
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||
*
|
||||
* Copyright (C) 2018 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 <qa_utils/wx_utils/unit_test_utils.h>
|
||||
|
||||
#include <geometry/chamfer.h>
|
||||
|
||||
#include "geom_test_utils.h"
|
||||
|
||||
struct ChamferFixture
|
||||
{
|
||||
};
|
||||
|
||||
/**
|
||||
* Declares the FilletFixture struct as the boost test fixture.
|
||||
*/
|
||||
BOOST_FIXTURE_TEST_SUITE( Chamfer, ChamferFixture )
|
||||
|
||||
struct TWO_LINE_CHAMFER_TEST_CASE
|
||||
{
|
||||
SEG m_seg_a;
|
||||
SEG m_seg_b;
|
||||
CHAMFER_PARAMS m_params;
|
||||
std::optional<CHAMFER_RESULT> m_expected_result;
|
||||
};
|
||||
|
||||
static void DoChamferTestChecks( const TWO_LINE_CHAMFER_TEST_CASE& aTestCase )
|
||||
{
|
||||
// Actally do the chamfer
|
||||
const std::optional<CHAMFER_RESULT> chamfer_result =
|
||||
ComputeChamferPoints( aTestCase.m_seg_a, aTestCase.m_seg_b, aTestCase.m_params );
|
||||
|
||||
BOOST_REQUIRE_EQUAL( chamfer_result.has_value(), aTestCase.m_expected_result.has_value() );
|
||||
|
||||
if( chamfer_result.has_value() )
|
||||
{
|
||||
const CHAMFER_RESULT& expected_result = aTestCase.m_expected_result.value();
|
||||
const CHAMFER_RESULT& actual_result = chamfer_result.value();
|
||||
|
||||
BOOST_CHECK_PREDICATE( GEOM_TEST::SegmentsHaveSameEndPoints,
|
||||
( actual_result.m_chamfer )( expected_result.m_chamfer ) );
|
||||
|
||||
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_AUTO_TEST_CASE( SimpleChamferAtOrigin )
|
||||
{
|
||||
/* 10
|
||||
* 0,0 +----+-------------> 1000
|
||||
* | /
|
||||
* | /
|
||||
* 10 +
|
||||
* |
|
||||
* v 1000
|
||||
* */
|
||||
|
||||
const TWO_LINE_CHAMFER_TEST_CASE testcase{
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 1000 ) },
|
||||
{ 10, 10 },
|
||||
{ {
|
||||
SEG( VECTOR2I( 10, 0 ), VECTOR2I( 0, 10 ) ), // chamfer
|
||||
{ SEG( VECTOR2I( 10, 0 ), VECTOR2I( 1000, 0 ) ) }, // rest of the line A
|
||||
{ SEG( VECTOR2I( 0, 10 ), VECTOR2I( 0, 1000 ) ) }, // rest of the line B
|
||||
} },
|
||||
};
|
||||
|
||||
DoChamferTestChecks( testcase );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( SimpleChamferNotAtOrigin )
|
||||
{
|
||||
// Same as above but the intersection is not at the origin
|
||||
const TWO_LINE_CHAMFER_TEST_CASE testcase{
|
||||
{ VECTOR2I( 1000, 1000 ), VECTOR2I( 2000, 1000 ) },
|
||||
{ VECTOR2I( 1000, 1000 ), VECTOR2I( 1000, 2000 ) },
|
||||
{ 10, 10 },
|
||||
{ {
|
||||
SEG( VECTOR2I( 1010, 1000 ), VECTOR2I( 1000, 1010 ) ),
|
||||
{ SEG( VECTOR2I( 1010, 1000 ), VECTOR2I( 2000, 1000 ) ) },
|
||||
{ SEG( VECTOR2I( 1000, 1010 ), VECTOR2I( 1000, 2000 ) ) },
|
||||
} },
|
||||
};
|
||||
|
||||
DoChamferTestChecks( testcase );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( AsymmetricChamfer )
|
||||
{
|
||||
// Same as above but the intersection is not at the origin
|
||||
const TWO_LINE_CHAMFER_TEST_CASE testcase{
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 1000 ) },
|
||||
{ 10, 100 },
|
||||
{ {
|
||||
SEG( VECTOR2I( 10, 0 ), VECTOR2I( 0, 100 ) ), // chamfer
|
||||
{ SEG( VECTOR2I( 10, 0 ), VECTOR2I( 1000, 0 ) ) }, // rest of the line A
|
||||
{ SEG( VECTOR2I( 0, 100 ), VECTOR2I( 0, 1000 ) ) }, // rest of the line B
|
||||
} },
|
||||
};
|
||||
|
||||
DoChamferTestChecks( testcase );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( ChamferFullLength )
|
||||
{
|
||||
// Chamfer consumes the entire length of a line
|
||||
const TWO_LINE_CHAMFER_TEST_CASE testcase{
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 100 ) },
|
||||
{ 100, 100 },
|
||||
{ {
|
||||
SEG( VECTOR2I( 100, 0 ), VECTOR2I( 0, 100 ) ), // chamfer
|
||||
{ SEG( VECTOR2I( 100, 0 ), VECTOR2I( 1000, 0 ) ) }, // rest of the line A
|
||||
std::nullopt, // line b no longer exists
|
||||
} },
|
||||
};
|
||||
|
||||
DoChamferTestChecks( testcase );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( ChamferOverFullLength )
|
||||
{
|
||||
// Chamfer consumes the entire length of a line
|
||||
const TWO_LINE_CHAMFER_TEST_CASE testcase{
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
|
||||
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 100 ) },
|
||||
{ 150, 150 }, // > 100
|
||||
std::nullopt,
|
||||
};
|
||||
|
||||
DoChamferTestChecks( testcase );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
Loading…
Reference in New Issue