From 046d978ba78e6043e0af2fd8a1a5b29ae125ecf5 Mon Sep 17 00:00:00 2001 From: John Beard Date: Sun, 2 Jul 2023 18:59:04 +0100 Subject: [PATCH] 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. --- libs/kimath/CMakeLists.txt | 1 + libs/kimath/include/geometry/chamfer.h | 62 +++++++ libs/kimath/src/geometry/chamfer.cpp | 107 +++++++++++ pcbnew/tools/edit_tool.cpp | 79 +++++++- pcbnew/tools/edit_tool.h | 5 +- pcbnew/tools/item_modification_routine.cpp | 140 ++++++++++++++- pcbnew/tools/item_modification_routine.h | 60 ++++++- pcbnew/tools/pcb_actions.cpp | 10 ++ pcbnew/tools/pcb_actions.h | 6 + qa/tests/libs/kimath/CMakeLists.txt | 1 + .../libs/kimath/geometry/geom_test_utils.h | 11 ++ .../libs/kimath/geometry/test_chamfer.cpp | 170 ++++++++++++++++++ 12 files changed, 640 insertions(+), 12 deletions(-) create mode 100644 libs/kimath/include/geometry/chamfer.h create mode 100644 libs/kimath/src/geometry/chamfer.cpp create mode 100644 qa/tests/libs/kimath/geometry/test_chamfer.cpp diff --git a/libs/kimath/CMakeLists.txt b/libs/kimath/CMakeLists.txt index bb3aab30b8..ab3468c12d 100644 --- a/libs/kimath/CMakeLists.txt +++ b/libs/kimath/CMakeLists.txt @@ -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 diff --git a/libs/kimath/include/geometry/chamfer.h b/libs/kimath/include/geometry/chamfer.h new file mode 100644 index 0000000000..77420b08c2 --- /dev/null +++ b/libs/kimath/include/geometry/chamfer.h @@ -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 + * + * 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 + +#include + +/** + * 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 m_updated_seg_a; + std::optional m_updated_seg_b; +}; + +/** + * Compute the chamfer points for a given line pair and chamfer parameters. + */ +std::optional ComputeChamferPoints( const SEG aSegA, const SEG& aSegB, + const CHAMFER_PARAMS& aChamferParams ); + + +#endif \ No newline at end of file diff --git a/libs/kimath/src/geometry/chamfer.cpp b/libs/kimath/src/geometry/chamfer.cpp new file mode 100644 index 0000000000..d879fe3ffd --- /dev/null +++ b/libs/kimath/src/geometry/chamfer.cpp @@ -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 + +std::optional 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 new_a; + if( a_end_pt != chamfer.A ) + { + new_a = SEG{ a_end_pt, chamfer.A }; + } + + std::optional new_b; + if( b_end_pt != chamfer.B ) + { + new_b = SEG{ b_end_pt, chamfer.B }; + } + + return CHAMFER_RESULT{ chamfer, new_a, new_b }; +} \ No newline at end of file diff --git a/pcbnew/tools/edit_tool.cpp b/pcbnew/tools/edit_tool.cpp index 8c59f7498b..1c508a4a32 100644 --- a/pcbnew/tools/edit_tool.cpp +++ b/pcbnew/tools/edit_tool.cpp @@ -204,10 +204,11 @@ bool EDIT_TOOL::Init() PCB_ARC_T, PCB_VIA_T }; - static std::vector filletTypes = { PCB_SHAPE_LOCATE_POLY_T, - PCB_SHAPE_LOCATE_RECT_T, - PCB_SHAPE_LOCATE_SEGMENT_T }; + static std::vector filletChamferTypes = { PCB_SHAPE_LOCATE_POLY_T, + PCB_SHAPE_LOCATE_RECT_T, + PCB_SHAPE_LOCATE_SEGMENT_T }; + static std::vector 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 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 the chamfer parameters or std::nullopt if no + * valid fillet specified + */ +static std::optional 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 = + GetChamferParams( *frame(), error_message ); + + if( chamfer_params.has_value() ) + { + pairwise_line_routine = std::make_unique( + 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( + 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() ); diff --git a/pcbnew/tools/edit_tool.h b/pcbnew/tools/edit_tool.h index 556d54daff..8dab351ebd 100644 --- a/pcbnew/tools/edit_tool.h +++ b/pcbnew/tools/edit_tool.h @@ -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. diff --git a/pcbnew/tools/item_modification_routine.cpp b/pcbnew/tools/item_modification_routine.cpp index ee90b4015a..40e99e76b2 100644 --- a/pcbnew/tools/item_modification_routine.cpp +++ b/pcbnew/tools/item_modification_routine.cpp @@ -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(); -} \ No newline at end of file +} + +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 = + ComputeChamferPoints( seg_a, seg_b, m_chamferParams ); + + if( !chamfer_result ) + { + AddFailure(); + return; + } + + auto tSegment = std::make_unique( 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(); +} diff --git a/pcbnew/tools/item_modification_routine.h b/pcbnew/tools/item_modification_routine.h index a1b2369670..3fba69a89f 100644 --- a/pcbnew/tools/item_modification_routine.h +++ b/pcbnew/tools/item_modification_routine.h @@ -32,6 +32,8 @@ #include #include +#include + /** * @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 )>; + + /** + * Handler for modifying an existing item on the board + * + * @param PCB_SHAPE& the shape to modify + */ using MODIFICATION_HANDLER = std::function; 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_ */ \ No newline at end of file diff --git a/pcbnew/tools/pcb_actions.cpp b/pcbnew/tools/pcb_actions.cpp index fa4670af0f..c349b6e31a 100644 --- a/pcbnew/tools/pcb_actions.cpp +++ b/pcbnew/tools/pcb_actions.cpp @@ -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 ) diff --git a/pcbnew/tools/pcb_actions.h b/pcbnew/tools/pcb_actions.h index c31dabf24f..40e1cc8f2d 100644 --- a/pcbnew/tools/pcb_actions.h +++ b/pcbnew/tools/pcb_actions.h @@ -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; diff --git a/qa/tests/libs/kimath/CMakeLists.txt b/qa/tests/libs/kimath/CMakeLists.txt index f220d0ca11..e1ddaf412c 100644 --- a/qa/tests/libs/kimath/CMakeLists.txt +++ b/qa/tests/libs/kimath/CMakeLists.txt @@ -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 diff --git a/qa/tests/libs/kimath/geometry/geom_test_utils.h b/qa/tests/libs/kimath/geometry/geom_test_utils.h index 7c38571b4c..32b4e62119 100644 --- a/qa/tests/libs/kimath/geometry/geom_test_utils.h +++ b/qa/tests/libs/kimath/geometry/geom_test_utils.h @@ -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 diff --git a/qa/tests/libs/kimath/geometry/test_chamfer.cpp b/qa/tests/libs/kimath/geometry/test_chamfer.cpp new file mode 100644 index 0000000000..0ffff54b8a --- /dev/null +++ b/qa/tests/libs/kimath/geometry/test_chamfer.cpp @@ -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 + +#include + +#include + +#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 m_expected_result; +}; + +static void DoChamferTestChecks( const TWO_LINE_CHAMFER_TEST_CASE& aTestCase ) +{ + // Actally do the chamfer + const std::optional 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& updated_seg, const std::optional& 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()