diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 1ca7f7ed63..28a46bcf74 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -180,6 +180,8 @@ set( COMMON_DLG_SRCS dialogs/dialog_print_generic.cpp dialogs/dialog_print_generic_base.cpp dialogs/dialog_text_entry.cpp + dialogs/dialog_unit_entry.cpp + dialogs/dialog_unit_entry_base.cpp dialogs/eda_list_dialog.cpp dialogs/eda_list_dialog_base.cpp dialogs/eda_view_switcher.cpp diff --git a/common/dialogs/dialog_unit_entry.cpp b/common/dialogs/dialog_unit_entry.cpp new file mode 100644 index 0000000000..f139abbb62 --- /dev/null +++ b/common/dialogs/dialog_unit_entry.cpp @@ -0,0 +1,42 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2020 Roberto Fernandez Bautista + * Copyright (C) 2020 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 "dialog_unit_entry.h" + + +WX_UNIT_ENTRY_DIALOG::WX_UNIT_ENTRY_DIALOG( EDA_DRAW_FRAME* aParent, const wxString& aLabel, + const wxString& aCaption, const long long& aDefaultValue ) + : WX_UNIT_ENTRY_DIALOG_BASE( ( wxWindow* ) aParent, wxID_ANY, aCaption ), + m_unit_binder( aParent, m_label, m_textCtrl, m_unit_label, true ) +{ + m_label->SetLabel( aLabel ); + m_unit_binder.SetValue( aDefaultValue ); + m_sdbSizer1OK->SetDefault(); +} + + +long long WX_UNIT_ENTRY_DIALOG::GetValue() +{ + return m_unit_binder.GetValue(); +} diff --git a/common/dialogs/dialog_unit_entry.h b/common/dialogs/dialog_unit_entry.h new file mode 100644 index 0000000000..5733ef9095 --- /dev/null +++ b/common/dialogs/dialog_unit_entry.h @@ -0,0 +1,56 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2020 Roberto Fernandez Bautista + * Copyright (C) 2020 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 + */ + + +/** + * An extension of WX_TEXT_ENTRY_DIALOG that uses UNIT_BINDER to request a dimension + * (e.g. mm, inches, etc) from the user according to the selected units + */ + + +#ifndef _DIALOG_UNIT_ENTRY_H_ +#define _DIALOG_UNIT_ENTRY_H_ + + +#include + +#include "dialog_unit_entry_base.h" + +class WX_UNIT_ENTRY_DIALOG : public WX_UNIT_ENTRY_DIALOG_BASE +{ +public: + WX_UNIT_ENTRY_DIALOG( EDA_DRAW_FRAME* aParent, const wxString& aLabel, const wxString& aCaption, + const long long& aDefaultValue = 0 ); + + + /** + * Returns the value in internal units + */ + long long GetValue(); + +private: + UNIT_BINDER m_unit_binder; +}; + +#endif // _DIALOG_UNIT_ENTRY_H_ diff --git a/common/dialogs/dialog_unit_entry_base.cpp b/common/dialogs/dialog_unit_entry_base.cpp new file mode 100644 index 0000000000..f1e4c5566f --- /dev/null +++ b/common/dialogs/dialog_unit_entry_base.cpp @@ -0,0 +1,63 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Oct 26 2018) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "dialog_unit_entry_base.h" + +/////////////////////////////////////////////////////////////////////////// + +WX_UNIT_ENTRY_DIALOG_BASE::WX_UNIT_ENTRY_DIALOG_BASE( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : DIALOG_SHIM( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + + wxBoxSizer* bSizerMain; + bSizerMain = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizerContent; + bSizerContent = new wxBoxSizer( wxVERTICAL ); + + m_label = new wxStaticText( this, wxID_ANY, _("MyLabel"), wxDefaultPosition, wxDefaultSize, 0 ); + m_label->Wrap( -1 ); + bSizerContent->Add( m_label, 0, wxALL|wxEXPAND, 5 ); + + wxBoxSizer* bSizerTextAndUnit; + bSizerTextAndUnit = new wxBoxSizer( wxHORIZONTAL ); + + m_textCtrl = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); + m_textCtrl->SetMinSize( wxSize( 300,-1 ) ); + + bSizerTextAndUnit->Add( m_textCtrl, 0, wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND, 5 ); + + m_unit_label = new wxStaticText( this, wxID_ANY, _("unit"), wxDefaultPosition, wxDefaultSize, 0 ); + m_unit_label->Wrap( -1 ); + bSizerTextAndUnit->Add( m_unit_label, 0, wxALL, 5 ); + + + bSizerContent->Add( bSizerTextAndUnit, 1, wxEXPAND, 5 ); + + + bSizerMain->Add( bSizerContent, 1, wxALL|wxEXPAND, 5 ); + + m_sdbSizer1 = new wxStdDialogButtonSizer(); + m_sdbSizer1OK = new wxButton( this, wxID_OK ); + m_sdbSizer1->AddButton( m_sdbSizer1OK ); + m_sdbSizer1Cancel = new wxButton( this, wxID_CANCEL ); + m_sdbSizer1->AddButton( m_sdbSizer1Cancel ); + m_sdbSizer1->Realize(); + + bSizerMain->Add( m_sdbSizer1, 0, wxALL|wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizerMain ); + this->Layout(); + bSizerMain->Fit( this ); + + this->Centre( wxBOTH ); +} + +WX_UNIT_ENTRY_DIALOG_BASE::~WX_UNIT_ENTRY_DIALOG_BASE() +{ +} diff --git a/common/dialogs/dialog_unit_entry_base.fbp b/common/dialogs/dialog_unit_entry_base.fbp new file mode 100644 index 0000000000..8d052dd226 --- /dev/null +++ b/common/dialogs/dialog_unit_entry_base.fbp @@ -0,0 +1,290 @@ + + + + + + C++ + 1 + source_name + 0 + 0 + res + UTF-8 + connect + dialog_unit_entry_base + 1000 + none + + 1 + dialog_unit_entry_base + + . + + 1 + 1 + 1 + 1 + UI + 0 + 0 + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + WX_UNIT_ENTRY_DIALOG_BASE + + -1,-1 + wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER + DIALOG_SHIM; dialog_shim.h + + + + + + + + bSizerMain + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + + bSizerContent + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + MyLabel + 0 + + 0 + + + 0 + + 1 + m_label + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + + bSizerTextAndUnit + wxHORIZONTAL + none + + 5 + wxBOTTOM|wxRIGHT|wxLEFT|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + + 0 + 300,-1 + 1 + m_textCtrl + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + unit + 0 + + 0 + + + 0 + + 1 + m_unit_label + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + + + + + 5 + wxALL|wxALIGN_RIGHT + 0 + + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + + m_sdbSizer1 + protected + + + + + + diff --git a/common/dialogs/dialog_unit_entry_base.h b/common/dialogs/dialog_unit_entry_base.h new file mode 100644 index 0000000000..e620c880bf --- /dev/null +++ b/common/dialogs/dialog_unit_entry_base.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Oct 26 2018) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "dialog_shim.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////// + + +/////////////////////////////////////////////////////////////////////////////// +/// Class WX_UNIT_ENTRY_DIALOG_BASE +/////////////////////////////////////////////////////////////////////////////// +class WX_UNIT_ENTRY_DIALOG_BASE : public DIALOG_SHIM +{ + private: + + protected: + wxStaticText* m_label; + wxTextCtrl* m_textCtrl; + wxStaticText* m_unit_label; + wxStdDialogButtonSizer* m_sdbSizer1; + wxButton* m_sdbSizer1OK; + wxButton* m_sdbSizer1Cancel; + + public: + + WX_UNIT_ENTRY_DIALOG_BASE( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1,-1 ), long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + ~WX_UNIT_ENTRY_DIALOG_BASE(); + +}; + diff --git a/libs/kimath/include/geometry/shape_arc.h b/libs/kimath/include/geometry/shape_arc.h index b4eb41b46f..75548fa3a8 100644 --- a/libs/kimath/include/geometry/shape_arc.h +++ b/libs/kimath/include/geometry/shape_arc.h @@ -34,6 +34,11 @@ class SHAPE_LINE_CHAIN; class SHAPE_ARC : public SHAPE { public: + /** + * @brief This is the minimum precision for all the points in the arc shape. + */ + static const int MIN_PRECISION_IU = 4; + SHAPE_ARC() : SHAPE( SH_ARC ), m_width( 0 ) {}; @@ -56,6 +61,16 @@ public: */ SHAPE_ARC( const VECTOR2I& aArcStart, const VECTOR2I& aArcMid, const VECTOR2I& aArcEnd, int aWidth ); + + /** + * SHAPE_ARC ctor. + * Builds a SHAPE_ARC which is tangent to two segments and a given radius + * @param aSegmentA is the first segment + * @param aSegmentB is the second segment + * @param aRadius is the arc radius + * @param aWidth is the arc line thickness + */ + SHAPE_ARC( const SEG& aSegmentA, const SEG& aSegmentB, int aRadius, int aWidth = 0 ); SHAPE_ARC( const SHAPE_ARC& aOther ); diff --git a/libs/kimath/src/geometry/shape_arc.cpp b/libs/kimath/src/geometry/shape_arc.cpp index 9922d7a5dc..f3b7337fe1 100644 --- a/libs/kimath/src/geometry/shape_arc.cpp +++ b/libs/kimath/src/geometry/shape_arc.cpp @@ -54,6 +54,109 @@ SHAPE_ARC::SHAPE_ARC( const VECTOR2I& aArcStart, const VECTOR2I& aArcMid, } +SHAPE_ARC::SHAPE_ARC( const SEG& aSegmentA, const SEG& aSegmentB, int aRadius, int aWidth ) + : SHAPE( SH_ARC ) +{ + /* + * Construct an arc that is tangent to two segments with a given radius. + * + * p + * A + * A \ + * / \ + * / , * , \ segB + * /* *\ + * segA / c \ + * / B + * / + * / + * B + * + * + * segA is the fist segment (with its points A and B) + * segB is the second segment (with its points A and B) + * p is the point at which segA and segB would intersect if they were projected + * c is the centre of the arc to be constructed + * rad is the radius of the arc to be constructed + * + * We can create two vectors, betweeen point p and segA /segB + * pToA = p - segA.B //< note that segA.A would also be valid as it is colinear + * pToB = p - segB.B //< note that segB.A would also be valid as it is colinear + * + * Let the angle formed by segA and segB be called 'alpha': + * alpha = angle( pToA ) - angle( pToB ) + * + * The distance PC can be computed as + * distPC = rad / abs( sin( alpha / 2 ) ) + * + * The polar angle of the vector PC can be computed as: + * anglePC = angle( pToA ) + alpha / 2 + * + * Therefore: + * C.x = P.x + distPC*cos( anglePC ) + * C.y = P.y + distPC*sin( anglePC ) + */ + + OPT_VECTOR2I p = aSegmentA.Intersect( aSegmentB, true, true ); + + if( !p || aSegmentA.Length() == 0 || aSegmentB.Length() == 0 ) + { + // Catch bugs in debug + wxASSERT_MSG( false, "The input segments do not intersect or one is zero length." ); + + // Make a 180 degree arc around aSegmentA in case we end up here in release + m_start = aSegmentA.A; + m_end = aSegmentA.B; + m_mid = m_start; + + VECTOR2I arcCenter = aSegmentA.Center(); + RotatePoint( m_mid, arcCenter, 900.0 ); // mid point at 90 degrees + } + else + { + VECTOR2I pToA = aSegmentA.B - p.get(); + VECTOR2I pToB = aSegmentB.B - p.get(); + + if( pToA.EuclideanNorm() == 0 ) + pToA = aSegmentA.A - p.get(); + + if( pToB.EuclideanNorm() == 0 ) + pToB = aSegmentB.A - p.get(); + + double pToAangle = ArcTangente( pToA.y, pToA.x ); + double pToBangle = ArcTangente( pToB.y, pToB.x ); + + double alpha = NormalizeAngle180( pToAangle - pToBangle ); + + double distPC = (double) aRadius / abs( sin( DECIDEG2RAD( alpha / 2 ) ) ); + double angPC = pToAangle - alpha / 2; + + VECTOR2I arcCenter; + + arcCenter.x = p.get().x + KiROUND( distPC * cos( DECIDEG2RAD( angPC ) ) ); + arcCenter.y = p.get().y + KiROUND( distPC * sin( DECIDEG2RAD( angPC ) ) ); + + // The end points of the arc are the orthogonal projected lines from the line segments + // to the center of the arc + m_start = aSegmentA.LineProject( arcCenter ); + m_end = aSegmentB.LineProject( arcCenter ); + + //The mid point is rotated start point around center, half the angle of the arc. + VECTOR2I startVector = m_start - arcCenter; + VECTOR2I endVector = m_end - arcCenter; + + double startAngle = ArcTangente( startVector.y, startVector.x ); + double endAngle = ArcTangente( endVector.y, endVector.x ); + + double midPointRotAngle = NormalizeAngle180( startAngle - endAngle ) / 2; + m_mid = m_start; + RotatePoint( m_mid, arcCenter, midPointRotAngle ); + } + + update_bbox(); +} + + SHAPE_ARC::SHAPE_ARC( const SHAPE_ARC& aOther ) : SHAPE( SH_ARC ) { diff --git a/pcbnew/tools/edit_tool.cpp b/pcbnew/tools/edit_tool.cpp index a3487e51a2..834edc5296 100644 --- a/pcbnew/tools/edit_tool.cpp +++ b/pcbnew/tools/edit_tool.cpp @@ -56,6 +56,7 @@ using namespace std::placeholders; #include #include #include +#include #include #include @@ -193,6 +194,7 @@ bool EDIT_TOOL::Init() && SELECTION_CONDITIONS::OnlyTypes( GENERAL_COLLECTOR::Tracks ) ); menu.AddItem( PCB_ACTIONS::drag45Degree, SELECTION_CONDITIONS::OnlyTypes( GENERAL_COLLECTOR::Tracks ) ); menu.AddItem( PCB_ACTIONS::dragFreeAngle, SELECTION_CONDITIONS::OnlyTypes( GENERAL_COLLECTOR::Tracks ) ); + menu.AddItem( PCB_ACTIONS::filletTracks, SELECTION_CONDITIONS::OnlyTypes( GENERAL_COLLECTOR::Tracks ) ); menu.AddItem( PCB_ACTIONS::rotateCcw, SELECTION_CONDITIONS::NotEmpty ); menu.AddItem( PCB_ACTIONS::rotateCw, SELECTION_CONDITIONS::NotEmpty ); menu.AddItem( PCB_ACTIONS::flip, SELECTION_CONDITIONS::NotEmpty ); @@ -704,6 +706,182 @@ int EDIT_TOOL::ChangeTrackWidth( const TOOL_EVENT& aEvent ) } +int EDIT_TOOL::FilletTracks( const TOOL_EVENT& aEvent ) +{ + // Store last used fillet radius to allow pressing "enter" if repeat fillet is required + static long long filletRadiusIU = 0; + + auto& selection = m_selectionTool->RequestSelection( + []( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, SELECTION_TOOL* sTool ) + { + EditToolSelectionFilter( + aCollector, EXCLUDE_LOCKED | EXCLUDE_LOCKED_PADS | EXCLUDE_TRANSIENTS, sTool ); + }, + nullptr, !m_dragging ); + + if( selection.Size() < 2 ) + { + m_statusPopup.reset( new STATUS_TEXT_POPUP( frame() ) ); + m_statusPopup->SetText( _( "A minimum of two straight track segments must be selected." ) ); + m_statusPopup->Move( wxGetMousePosition() + wxPoint( 20, 20 ) ); + m_statusPopup->PopupFor( 2000 ); + return 0; + } + + WX_UNIT_ENTRY_DIALOG dia( + frame(), _( "Enter fillet radius:" ), _( "Fillet Tracks" ), filletRadiusIU ); + + if( dia.ShowModal() == wxID_CANCEL ) + return 0; + + filletRadiusIU = dia.GetValue(); + + if( filletRadiusIU == 0 ) + { + m_statusPopup.reset( new STATUS_TEXT_POPUP( frame() ) ); + m_statusPopup->SetText( _( "A radius of zero was entered.\n" + "The fillet operation was not performed." ) ); + m_statusPopup->Move( wxGetMousePosition() + wxPoint( 20, 20 ) ); + m_statusPopup->PopupFor( 2000 ); + return 0; + } + + bool operationPerformedOnAtLeastOne = false; + bool didOneAttemptFail = false; + + std::vector itemsToAddToSelection; + + for( auto it = selection.begin(); it != selection.end(); it++ ) + { + TRACK* track1 = dyn_cast( *it ); + + if( !track1 || track1->Type() != PCB_TRACE_T || track1->IsLocked() + || track1->GetLength() == 0 ) + { + continue; + } + + for( auto it2 = it + 1; it2 != selection.end(); it2++ ) + { + TRACK* track2 = dyn_cast( *it2 ); + + if( !track2 || track2->Type() != PCB_TRACE_T || track2->IsLocked() + || track2->GetLength() == 0 ) + { + continue; + } + + bool trackOnStart = track1->IsPointOnEnds( track2->GetStart() ); + bool trackOnEnd = track1->IsPointOnEnds( track2->GetEnd() ); + + if( trackOnStart && trackOnEnd ) + continue; // Ignore duplicate tracks + + if( ( trackOnStart || trackOnEnd ) && track1->GetLayer() == track2->GetLayer() ) + { + SEG t1Seg( track1->GetStart(), track1->GetEnd() ); + SEG t2Seg( track2->GetStart(), track2->GetEnd() ); + + if( t1Seg.ApproxCollinear( t2Seg ) ) + continue; + + SHAPE_ARC sArc( t1Seg, t2Seg, filletRadiusIU ); + + wxPoint t1newPoint, t2newPoint; + + auto setIfPointOnSeg = + []( wxPoint& aPointToSet, SEG aSegment, VECTOR2I aVecToTest ) + { + // Find out if we are within the segment + if( ( aSegment.NearestPoint( aVecToTest ) - aVecToTest ).EuclideanNorm() + < SHAPE_ARC::MIN_PRECISION_IU ) + { + aPointToSet.x = aVecToTest.x; + aPointToSet.y = aVecToTest.y; + return true; + } + + return false; + }; + + //Do not draw a fillet if the end points of the arc are not within the track segments + if( !setIfPointOnSeg( t1newPoint, t1Seg, sArc.GetP0() ) + && !setIfPointOnSeg( t2newPoint, t2Seg, sArc.GetP0() ) ) + { + didOneAttemptFail = true; + continue; + } + + if( !setIfPointOnSeg( t1newPoint, t1Seg, sArc.GetP1() ) + && !setIfPointOnSeg( t2newPoint, t2Seg, sArc.GetP1() ) ) + { + didOneAttemptFail = true; + continue; + } + + ARC* tArc = new ARC( frame()->GetBoard(), &sArc ); + tArc->SetLayer( track1->GetLayer() ); + tArc->SetWidth( track1->GetWidth() ); + tArc->SetNet( track1->GetNet() ); + m_commit->Add( tArc ); + itemsToAddToSelection.push_back( tArc ); + + m_commit->Modify( track1 ); + m_commit->Modify( track2 ); + + if( track1->GetStart() == track2->GetStart() ) + { + track1->SetStart( t1newPoint ); + track2->SetStart( t2newPoint ); + } + else if( track1->GetStart() == track2->GetEnd() ) + { + track1->SetStart( t1newPoint ); + track2->SetEnd( t2newPoint ); + } + else if( track1->GetEnd() == track2->GetEnd() ) + { + track1->SetEnd( t1newPoint ); + track2->SetEnd( t2newPoint ); + } + else + { + track1->SetEnd( t1newPoint ); + track2->SetStart( t2newPoint ); + } + + operationPerformedOnAtLeastOne = true; + } + } + } + + m_commit->Push( _( "Fillet Tracks" ) ); + + //select the newly created arcs + for( BOARD_ITEM* item : itemsToAddToSelection ) + { + m_selectionTool->AddItemToSel( item ); + } + + if( !operationPerformedOnAtLeastOne ) + { + m_statusPopup.reset( new STATUS_TEXT_POPUP( frame() ) ); + m_statusPopup->SetText( _( "Unable to fillet the selected track segments." ) ); + m_statusPopup->Move( wxGetMousePosition() + wxPoint( 20, 20 ) ); + m_statusPopup->PopupFor( 2000 ); + } + else if( didOneAttemptFail ) + { + m_statusPopup.reset( new STATUS_TEXT_POPUP( frame() ) ); + m_statusPopup->SetText( _( "Some of the track segments could not be filleted." ) ); + m_statusPopup->Move( wxGetMousePosition() + wxPoint( 20, 20 ) ); + m_statusPopup->PopupFor( 2000 ); + } + + return 0; +} + + int EDIT_TOOL::Properties( const TOOL_EVENT& aEvent ) { PCB_BASE_EDIT_FRAME* editFrame = getEditFrame(); @@ -1692,6 +1870,7 @@ void EDIT_TOOL::setTransitions() Go( &EDIT_TOOL::CreateArray, PCB_ACTIONS::createArray.MakeEvent() ); Go( &EDIT_TOOL::Mirror, PCB_ACTIONS::mirror.MakeEvent() ); Go( &EDIT_TOOL::ChangeTrackWidth, PCB_ACTIONS::changeTrackWidth.MakeEvent() ); + Go( &EDIT_TOOL::FilletTracks, PCB_ACTIONS::filletTracks.MakeEvent() ); Go( &EDIT_TOOL::copyToClipboard, ACTIONS::copy.MakeEvent() ); Go( &EDIT_TOOL::cutToClipboard, ACTIONS::cut.MakeEvent() ); diff --git a/pcbnew/tools/edit_tool.h b/pcbnew/tools/edit_tool.h index e3e2cc2912..5fdb0273ca 100644 --- a/pcbnew/tools/edit_tool.h +++ b/pcbnew/tools/edit_tool.h @@ -123,6 +123,12 @@ public: int ChangeTrackWidth( const TOOL_EVENT& aEvent ); + /** + * Function FilletTracks() + * Fillets (i.e. adds an arc tangent to) all selected straight tracks by a user defined radius + */ + int FilletTracks( const TOOL_EVENT& aEvent ); + /** * Function Remove() * Deletes currently selected items. The rotation point is the current cursor position. diff --git a/pcbnew/tools/pcb_actions.cpp b/pcbnew/tools/pcb_actions.cpp index f79ae70260..cec5f143cc 100644 --- a/pcbnew/tools/pcb_actions.cpp +++ b/pcbnew/tools/pcb_actions.cpp @@ -292,6 +292,10 @@ TOOL_ACTION PCB_ACTIONS::changeTrackWidth( "pcbnew.InteractiveEdit.changeTrackWi AS_GLOBAL, 0, "", _( "Change Track Width" ), _( "Updates selected track & via sizes" ) ); +TOOL_ACTION PCB_ACTIONS::filletTracks( "pcbnew.InteractiveEdit.filletTracks", + AS_GLOBAL, 0, "", + _( "Fillet Tracks" ), _( "Adds arcs tangent to the selected straight track segments" ) ); + TOOL_ACTION PCB_ACTIONS::deleteFull( "pcbnew.InteractiveEdit.deleteFull", AS_GLOBAL, MD_SHIFT + WXK_DELETE, LEGACY_HK_NAME( "Delete Full Track" ), diff --git a/pcbnew/tools/pcb_actions.h b/pcbnew/tools/pcb_actions.h index 5c40a6d5e0..6d70791337 100644 --- a/pcbnew/tools/pcb_actions.h +++ b/pcbnew/tools/pcb_actions.h @@ -110,6 +110,9 @@ public: /// Updates selected tracks & vias to the current track & via dimensions static TOOL_ACTION changeTrackWidth; + /// Fillets (i.e. adds an arc tangent to) all selected straight tracks by a user defined radius + static TOOL_ACTION filletTracks; + /// Activation of the edit tool static TOOL_ACTION properties; diff --git a/qa/libs/kimath/geometry/test_shape_arc.cpp b/qa/libs/kimath/geometry/test_shape_arc.cpp index 1dbe2d7dbe..6e4b8144a2 100644 --- a/qa/libs/kimath/geometry/test_shape_arc.cpp +++ b/qa/libs/kimath/geometry/test_shape_arc.cpp @@ -51,8 +51,11 @@ struct ARC_PROPERTIES /** * Check a #SHAPE_ARC against a given set of geometric properties + * @param aArc Arc to test + * @param aProps Properties to test against + * @param aSynErrIU Permitted error for synthetic points and dimensions (currently radius and center) */ -static void CheckArcGeom( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps ) +static void CheckArcGeom( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps, const int aSynErrIU = 1 ) { // Angular error - note this can get quite large for very small arcs, // as the integral position rounding has a relatively greater effect @@ -66,7 +69,7 @@ static void CheckArcGeom( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps ) BOOST_CHECK_PREDICATE( KI_TEST::IsVecWithinTol, ( aArc.GetP1() )( aProps.m_end_point )( pos_tol ) ); BOOST_CHECK_PREDICATE( KI_TEST::IsVecWithinTol, - ( aArc.GetCenter() )( aProps.m_center_point )( pos_tol ) ); + ( aArc.GetCenter() )( aProps.m_center_point )( aSynErrIU ) ); BOOST_CHECK_PREDICATE( KI_TEST::IsWithinWrapped, ( aArc.GetCentralAngle() )( aProps.m_center_angle )( 360.0 )( angle_tol_deg ) ); BOOST_CHECK_PREDICATE( KI_TEST::IsWithinWrapped, @@ -74,7 +77,7 @@ static void CheckArcGeom( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps ) BOOST_CHECK_PREDICATE( KI_TEST::IsWithinWrapped, ( aArc.GetEndAngle() )( aProps.m_end_angle )( 360.0 )( angle_tol_deg ) ); BOOST_CHECK_PREDICATE( - KI_TEST::IsWithin, ( aArc.GetRadius() )( aProps.m_radius )( pos_tol ) ); + KI_TEST::IsWithin, ( aArc.GetRadius() )( aProps.m_radius )( aSynErrIU ) ); /// Check the chord agrees const auto chord = aArc.GetChord(); @@ -96,11 +99,14 @@ static void CheckArcGeom( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps ) /** * Check an arcs geometry and other class functions + * @param aArc Arc to test + * @param aProps Properties to test against + * @param aSynErrIU Permitted error for synthetic points and dimensions (currently radius and center) */ -static void CheckArc( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps ) +static void CheckArc( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps, const int aSynErrIU = 1 ) { // Check the original arc - CheckArcGeom( aArc, aProps ); + CheckArcGeom( aArc, aProps, aSynErrIU ); // Test the Clone function (also tests copy-ctor) std::unique_ptr new_shape{ aArc.Clone() }; @@ -112,7 +118,7 @@ static void CheckArc( const SHAPE_ARC& aArc, const ARC_PROPERTIES& aProps ) BOOST_REQUIRE( new_arc != nullptr ); /// Should have identical geom props - CheckArcGeom( *new_arc, aProps ); + CheckArcGeom( *new_arc, aProps, aSynErrIU ); } /** @@ -328,6 +334,152 @@ BOOST_AUTO_TEST_CASE( BasicCPAGeom ) } + +/** + * Info to set up an arc by tangent to two segments and a radius + */ +struct ARC_TAN_TAN_RADIUS +{ + SEG m_segment_1; + SEG m_segment_2; + int m_radius; +}; + + +struct ARC_TTR_CASE +{ + /// The text context name + std::string m_ctx_name; + + /// Geom of the arc + ARC_TAN_TAN_RADIUS m_geom; + + /// Arc line width + int m_width; + + /// Expected properties + ARC_PROPERTIES m_properties; +}; + + +static const std::vector arc_ttr_cases = { + { + "90 degree segments intersecting", + { + { 0, 0, 0, 1000 }, + { 0, 0, 1000, 0 }, + 1000, + }, + 0, + { + { 1000, 1000 }, + { 0, 1000 }, //start on first segment + { 1000, 0 }, //end on second segment + 90, //positive angle due to start/end + 180, + 270, + 1000, + { { 0, 0 }, { 1000, 1000 } }, + } + }, + { + "45 degree segments intersecting", + { + { 0, 0, 0, 1000 }, + { 0, 0, 1000, 1000 }, + 1000, + }, + 0, + { + { 1000, 2414 }, + { 0, 2414 }, //start on first segment + { 1707, 1707 }, //end on second segment + 135, //positive angle due to start/end + 180, + 225, + 1000, + { { 0, 1414 }, { 1707, 1000 } }, + } + }, + { + "135 degree segments intersecting", + { + { 0, 0, 0, 1000 }, + { 0, 0, 1000, -1000 }, + 1000, + }, + 0, + { + { 1000, 414 }, + { 0, 414 }, //start on first segment ( radius * tan(45 /2) ) + { 293, -293 }, //end on second segment (radius * 1-cos(45)) ) + 45, //positive angle due to start/end + 180, + 225, + 1000, + { { 0, -293 }, { 293, 707 } }, + } + } + + +}; + + +BOOST_AUTO_TEST_CASE( BasicTTRGeom ) +{ + for( const auto& c : arc_ttr_cases ) + { + BOOST_TEST_CONTEXT( c.m_ctx_name ) + { + for( int testCase = 0; testCase < 8; ++testCase ) + { + SEG seg1 = c.m_geom.m_segment_1; + SEG seg2 = c.m_geom.m_segment_2; + ARC_PROPERTIES props = c.m_properties; + + if( testCase > 3 ) + { + //Swap input segments. + seg1 = c.m_geom.m_segment_2; + seg2 = c.m_geom.m_segment_1; + + //The result should swap start and end points and invert the angles: + props.m_end_point = c.m_properties.m_start_point; + props.m_start_point = c.m_properties.m_end_point; + props.m_start_angle = c.m_properties.m_end_angle; + props.m_end_angle = c.m_properties.m_start_angle; + props.m_center_angle = -c.m_properties.m_center_angle; + } + + //Test all combinations of start and end points for the segments + if( ( testCase % 4 ) == 1 || ( testCase % 4 ) == 3 ) + { + //Swap start and end points for seg1 + VECTOR2I temp = seg1.A; + seg1.A = seg1.B; + seg1.B = temp; + } + + if( ( testCase % 4 ) == 2 || ( testCase % 4 ) == 3 ) + { + //Swap start and end points for seg2 + VECTOR2I temp = seg2.A; + seg2.A = seg2.B; + seg2.B = temp; + } + + const auto this_arc = SHAPE_ARC{ seg1, seg2, + c.m_geom.m_radius, c.m_width }; + + // Error of 4 IU permitted for the center and radius calculation + CheckArc( this_arc, props, SHAPE_ARC::MIN_PRECISION_IU ); + } + } + } +} + + + struct ARC_TO_POLYLINE_CASE { std::string m_ctx_name;