kicad-source/pcbnew/tools/convert_tool.cpp
John Beard 299a6c6c7b Netinfo: avoid transitive string_utils.h include
Put the string manipuuation utils in the cpp, and
remove string_utils.h from the includes of netinfo.h.

This spams that header into about 350 files, not all of which
need it. Then go round and tidy up the places (most exporters
and dialogs) where CPP files weren't including string_utils.h
when they used it, as well as some other order-sensitive
include issues that turned up.
2025-08-12 20:00:15 +08:00

1463 lines
49 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* @author Jon Evans <jon@craftyjon.com>
*
* 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 "convert_tool.h"
#include <ranges>
#include <wx/statline.h>
#include <wx/checkbox.h>
#include <wx/button.h>
#include <wx/radiobut.h>
#include <bitmaps.h>
#include <dialog_shim.h>
#include <widgets/unit_binder.h>
#include <board.h>
#include <board_commit.h>
#include <board_design_settings.h>
#include <collectors.h>
#include <confirm.h>
#include <convert_basic_shapes_to_polygon.h>
#include <dialogs/dialog_outset_items.h>
#include <footprint.h>
#include <footprint_edit_frame.h>
#include <geometry/shape_compound.h>
#include <pcb_edit_frame.h>
#include <pcb_shape.h>
#include <pcb_track.h>
#include <pad.h>
#include <string_utils.h>
#include <tool/tool_manager.h>
#include <tools/edit_tool.h>
#include <tools/pcb_actions.h>
#include <tools/pcb_selection_tool.h>
#include <tools/pcb_tool_utils.h>
#include <trigo.h>
#include <macros.h>
#include <zone.h>
class CONVERT_SETTINGS_DIALOG : public DIALOG_SHIM
{
public:
CONVERT_SETTINGS_DIALOG( EDA_DRAW_FRAME* aParent, CONVERT_SETTINGS* aSettings,
bool aShowCopyLineWidthOption, bool aShowCenterlineOption,
bool aShowBoundingHullOption ) :
DIALOG_SHIM( aParent, wxID_ANY, _( "Conversion Settings" ), wxDefaultPosition,
wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ),
m_settings( aSettings )
{
// Allow each distinct version of the dialog to have a different size
m_hash_key = TO_UTF8( wxString::Format( wxS( "%s%c%c%c" ),
GetTitle(),
aShowCopyLineWidthOption,
aShowCenterlineOption,
aShowBoundingHullOption ) );
wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );
wxBoxSizer* topSizer = new wxBoxSizer( wxVERTICAL );
SetSizer( mainSizer );
m_rbMimicLineWidth = new wxRadioButton( this, wxID_ANY, _( "Copy line width of first object" ) );
if( aShowCopyLineWidthOption )
topSizer->Add( m_rbMimicLineWidth, 0, wxLEFT|wxRIGHT, 5 );
else
m_rbMimicLineWidth->Hide();
m_rbCenterline = new wxRadioButton( this, wxID_ANY, _( "Use centerlines" ) );
if( aShowCenterlineOption )
{
topSizer->AddSpacer( 6 );
topSizer->Add( m_rbCenterline, 0, wxLEFT|wxRIGHT, 5 );
}
else
{
m_rbCenterline->Hide();
}
m_rbBoundingHull = new wxRadioButton( this, wxID_ANY, _( "Create bounding hull" ) );
m_gapLabel = new wxStaticText( this, wxID_ANY, _( "Gap:" ) );
m_gapCtrl = new wxTextCtrl( this, wxID_ANY );
m_gapUnits = new wxStaticText( this, wxID_ANY, _( "mm" ) );
m_gap = new UNIT_BINDER( aParent, m_gapLabel, m_gapCtrl, m_gapUnits );
m_widthLabel = new wxStaticText( this, wxID_ANY, _( "Line width:" ) );
m_widthCtrl = new wxTextCtrl( this, wxID_ANY );
m_widthUnits = new wxStaticText( this, wxID_ANY, _( "mm" ) );
m_width = new UNIT_BINDER( aParent, m_widthLabel, m_widthCtrl, m_widthUnits );
if( aShowBoundingHullOption )
{
topSizer->AddSpacer( 6 );
topSizer->Add( m_rbBoundingHull, 0, wxLEFT|wxRIGHT, 5 );
wxBoxSizer* hullParamsSizer = new wxBoxSizer( wxHORIZONTAL );
hullParamsSizer->Add( m_gapLabel, 0, wxALIGN_CENTRE_VERTICAL, 5 );
hullParamsSizer->Add( m_gapCtrl, 1, wxALIGN_CENTRE_VERTICAL|wxLEFT|wxRIGHT, 3 );
hullParamsSizer->Add( m_gapUnits, 0, wxALIGN_CENTRE_VERTICAL, 5 );
hullParamsSizer->AddSpacer( 18 );
hullParamsSizer->Add( m_widthLabel, 0, wxALIGN_CENTRE_VERTICAL, 5 );
hullParamsSizer->Add( m_widthCtrl, 1, wxALIGN_CENTRE_VERTICAL|wxLEFT|wxRIGHT, 3 );
hullParamsSizer->Add( m_widthUnits, 0, wxALIGN_CENTRE_VERTICAL, 5 );
topSizer->AddSpacer( 2 );
topSizer->Add( hullParamsSizer, 0, wxLEFT, 26 );
topSizer->AddSpacer( 15 );
}
else
{
m_rbBoundingHull->Hide();
m_gap->Show( false, true );
m_width->Show( false, true );
}
m_cbDeleteOriginals = new wxCheckBox( this, wxID_ANY, _( "Delete source objects after conversion" ) );
topSizer->Add( m_cbDeleteOriginals, 0, wxALL, 5 );
mainSizer->Add( topSizer, 1, wxALL|wxEXPAND, 10 );
wxBoxSizer* buttonsSizer = new wxBoxSizer( wxHORIZONTAL );
buttonsSizer->AddStretchSpacer();
wxStdDialogButtonSizer* sdbSizer = new wxStdDialogButtonSizer();
wxButton* sdbSizerOK = new wxButton( this, wxID_OK );
sdbSizer->AddButton( sdbSizerOK );
wxButton* sdbSizerCancel = new wxButton( this, wxID_CANCEL );
sdbSizer->AddButton( sdbSizerCancel );
sdbSizer->Realize();
buttonsSizer->Add( sdbSizer, 1, 0, 5 );
mainSizer->Add( buttonsSizer, 0, wxALL|wxEXPAND, 5 );
SetupStandardButtons();
m_rbMimicLineWidth->Connect( wxEVT_COMMAND_RADIOBUTTON_SELECTED,
wxCommandEventHandler( CONVERT_SETTINGS_DIALOG::onRadioButton ),
nullptr, this );
m_rbCenterline->Connect( wxEVT_COMMAND_RADIOBUTTON_SELECTED,
wxCommandEventHandler( CONVERT_SETTINGS_DIALOG::onRadioButton ),
nullptr, this );
m_rbBoundingHull->Connect( wxEVT_COMMAND_RADIOBUTTON_SELECTED,
wxCommandEventHandler( CONVERT_SETTINGS_DIALOG::onRadioButton ),
nullptr, this );
finishDialogSettings();
}
~CONVERT_SETTINGS_DIALOG()
{
delete m_gap;
delete m_width;
};
protected:
bool TransferDataToWindow() override
{
switch( m_settings->m_Strategy )
{
case COPY_LINEWIDTH: m_rbMimicLineWidth->SetValue( true ); break;
case CENTERLINE: m_rbCenterline->SetValue( true ); break;
case BOUNDING_HULL: m_rbBoundingHull->SetValue( true ); break;
}
m_gap->Enable( m_rbBoundingHull->GetValue() );
m_width->Enable( m_rbBoundingHull->GetValue() );
m_gap->SetValue( m_settings->m_Gap );
m_width->SetValue( m_settings->m_LineWidth );
m_cbDeleteOriginals->SetValue( m_settings->m_DeleteOriginals );
return true;
}
bool TransferDataFromWindow() override
{
if( m_rbBoundingHull->GetValue() )
m_settings->m_Strategy = BOUNDING_HULL;
else if( m_rbCenterline->GetValue() )
m_settings->m_Strategy = CENTERLINE;
else
m_settings->m_Strategy = COPY_LINEWIDTH;
m_settings->m_Gap = m_gap->GetIntValue();
m_settings->m_LineWidth = m_width->GetIntValue();
m_settings->m_DeleteOriginals = m_cbDeleteOriginals->GetValue();
return true;
}
void onRadioButton( wxCommandEvent& aEvent )
{
m_gap->Enable( m_rbBoundingHull->GetValue() );
m_width->Enable( m_rbBoundingHull->GetValue() );
}
private:
CONVERT_SETTINGS* m_settings;
wxRadioButton* m_rbMimicLineWidth;
wxRadioButton* m_rbCenterline;
wxRadioButton* m_rbBoundingHull;
wxStaticText* m_gapLabel;
wxTextCtrl* m_gapCtrl;
wxStaticText* m_gapUnits;
UNIT_BINDER* m_gap;
wxStaticText* m_widthLabel;
wxTextCtrl* m_widthCtrl;
wxStaticText* m_widthUnits;
UNIT_BINDER* m_width;
wxCheckBox* m_cbDeleteOriginals;
};
CONVERT_TOOL::CONVERT_TOOL() :
PCB_TOOL_BASE( "pcbnew.Convert" ),
m_selectionTool( nullptr ),
m_menu( nullptr ),
m_frame( nullptr )
{
initUserSettings();
}
CONVERT_TOOL::~CONVERT_TOOL()
{
delete m_menu;
}
using S_C = SELECTION_CONDITIONS;
using P_S_C = PCB_SELECTION_CONDITIONS;
bool CONVERT_TOOL::Init()
{
m_selectionTool = m_toolMgr->GetTool<PCB_SELECTION_TOOL>();
m_frame = getEditFrame<PCB_BASE_FRAME>();
// Create a context menu and make it available through selection tool
m_menu = new CONDITIONAL_MENU( this );
m_menu->SetIcon( BITMAPS::convert );
m_menu->SetTitle( _( "Create from Selection" ) );
static const std::vector<KICAD_T> padTypes = { PCB_PAD_T };
static const std::vector<KICAD_T> toArcTypes = { PCB_ARC_T,
PCB_TRACE_T,
PCB_SHAPE_LOCATE_SEGMENT_T };
static const std::vector<KICAD_T> shapeTypes = { PCB_SHAPE_LOCATE_SEGMENT_T,
PCB_SHAPE_LOCATE_RECT_T,
PCB_SHAPE_LOCATE_CIRCLE_T,
PCB_SHAPE_LOCATE_ARC_T,
PCB_SHAPE_LOCATE_BEZIER_T,
PCB_FIELD_T,
PCB_TEXT_T };
static const std::vector<KICAD_T> trackTypes = { PCB_TRACE_T,
PCB_ARC_T,
PCB_VIA_T };
static const std::vector<KICAD_T> toTrackTypes = { PCB_SHAPE_LOCATE_SEGMENT_T,
PCB_SHAPE_LOCATE_ARC_T };
static const std::vector<KICAD_T> polyTypes = { PCB_ZONE_T,
PCB_SHAPE_LOCATE_POLY_T,
PCB_SHAPE_LOCATE_RECT_T };
static const std::vector<KICAD_T> outsetTypes = { PCB_PAD_T, PCB_SHAPE_T };
auto shapes = S_C::OnlyTypes( shapeTypes ) && P_S_C::SameLayer();
auto graphicToTrack = S_C::OnlyTypes( toTrackTypes );
auto anyTracks = S_C::MoreThan( 0 ) && S_C::OnlyTypes( trackTypes ) && P_S_C::SameLayer();
auto anyPolys = S_C::OnlyTypes( polyTypes );
auto anyPads = S_C::OnlyTypes( padTypes );
auto canCreateArcs = S_C::Count( 1 ) && S_C::OnlyTypes( toArcTypes );
auto canCreateArray = S_C::MoreThan( 0 );
auto canCreatePoly = shapes || anyPolys || anyTracks;
auto canCreateOutset = S_C::OnlyTypes( outsetTypes );
if( m_frame->IsType( FRAME_FOOTPRINT_EDITOR ) )
canCreatePoly = shapes || anyPolys || anyTracks || anyPads;
auto canCreateLines = anyPolys;
auto canCreateTracks = anyPolys || graphicToTrack;
auto canCreate = canCreatePoly
|| canCreateLines
|| canCreateTracks
|| canCreateArcs
|| canCreateArray
|| canCreateOutset;
m_menu->AddItem( PCB_ACTIONS::convertToPoly, canCreatePoly );
if( m_frame->IsType( FRAME_PCB_EDITOR ) )
m_menu->AddItem( PCB_ACTIONS::convertToZone, canCreatePoly );
m_menu->AddItem( PCB_ACTIONS::convertToKeepout, canCreatePoly );
m_menu->AddItem( PCB_ACTIONS::convertToLines, canCreateLines );
m_menu->AddItem( PCB_ACTIONS::outsetItems, canCreateOutset );
m_menu->AddSeparator();
// Currently the code exists, but tracks are not really existing in footprints
// only segments on copper layers
if( m_frame->IsType( FRAME_PCB_EDITOR ) )
m_menu->AddItem( PCB_ACTIONS::convertToTracks, canCreateTracks );
m_menu->AddItem( PCB_ACTIONS::convertToArc, canCreateArcs );
m_menu->AddSeparator();
m_menu->AddItem( PCB_ACTIONS::createArray, canCreateArray );
CONDITIONAL_MENU& selToolMenu = m_selectionTool->GetToolMenu().GetMenu();
selToolMenu.AddMenu( m_menu, canCreate, 100 );
return true;
}
void CONVERT_TOOL::initUserSettings()
{
m_userSettings.m_Strategy = CENTERLINE;
m_userSettings.m_Gap = 0;
m_userSettings.m_LineWidth = 0;
m_userSettings.m_DeleteOriginals = true;
}
int CONVERT_TOOL::CreatePolys( const TOOL_EVENT& aEvent )
{
BOARD_DESIGN_SETTINGS& bds = m_frame->GetBoard()->GetDesignSettings();
std::vector<SHAPE_POLY_SET> polys;
PCB_LAYER_ID destLayer = m_frame->GetActiveLayer();
FOOTPRINT* parentFootprint = nullptr;
PCB_SELECTION& selection = m_selectionTool->RequestSelection(
[]( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
{
} );
if( selection.Empty() )
return 0;
auto getPolys =
[&]( CONVERT_SETTINGS cfg )
{
polys.clear();
for( EDA_ITEM* item : selection )
item->ClearTempFlags();
SHAPE_POLY_SET polySet;
polySet.Append( makePolysFromClosedGraphics( selection.GetItems(), cfg.m_Strategy ) );
if( cfg.m_Strategy == BOUNDING_HULL )
{
polySet.Append( makePolysFromOpenGraphics( selection.GetItems(), 0 ) );
polySet.ClearArcs();
polySet.Simplify();
// Now inflate the bounding hull by cfg.m_Gap
polySet.Inflate( cfg.m_Gap, CORNER_STRATEGY::ROUND_ALL_CORNERS, bds.m_MaxError,
ERROR_OUTSIDE );
}
else
{
polySet.Append( makePolysFromChainedSegs( selection.GetItems(), cfg.m_Strategy ) );
}
if( polySet.IsEmpty() )
return false;
for( int ii = 0; ii < polySet.OutlineCount(); ++ii )
{
polys.emplace_back( SHAPE_POLY_SET( polySet.COutline( ii ) ) );
for( int jj = 0; jj < polySet.HoleCount( ii ); ++jj )
polys.back().AddHole( polySet.Hole( ii, jj ) );
}
return true;
};
// Pre-flight getPolys() to see if there's anything to convert.
CONVERT_SETTINGS preflightSettings = m_userSettings;
preflightSettings.m_Strategy = BOUNDING_HULL;
if( !getPolys( preflightSettings ) )
return 0;
if( selection.Front() && selection.Front()->IsBOARD_ITEM() )
parentFootprint = static_cast<BOARD_ITEM*>( selection.Front() )->GetParentFootprint();
PCB_LAYER_ID layer = m_frame->GetActiveLayer();
BOARD_COMMIT commit( m_frame );
if( aEvent.IsAction( &PCB_ACTIONS::convertToPoly ) )
{
bool showCopyLineWidth = true;
// No copy-line-width option for pads
if( selection.Front()->Type() == PCB_PAD_T )
{
if( m_userSettings.m_Strategy == COPY_LINEWIDTH )
m_userSettings.m_Strategy = CENTERLINE;
showCopyLineWidth = false;
}
CONVERT_SETTINGS previousSettings = m_userSettings;
CONVERT_SETTINGS_DIALOG dlg( m_frame, &m_userSettings, showCopyLineWidth, true, true );
if( dlg.ShowModal() != wxID_OK )
return 0;
CONVERT_SETTINGS resolvedSettings = m_userSettings;
if( resolvedSettings.m_Strategy != CENTERLINE )
{
if( resolvedSettings.m_LineWidth == 0 )
resolvedSettings.m_LineWidth = bds.m_LineThickness[ bds.GetLayerClass( layer ) ];
}
if( resolvedSettings.m_Strategy == BOUNDING_HULL )
{
if( resolvedSettings.m_Gap > 0 )
resolvedSettings.m_Gap += KiROUND( (double) resolvedSettings.m_LineWidth / 2.0 );
}
if( !getPolys( resolvedSettings ) )
{
wxString msg;
if( resolvedSettings.m_Strategy == BOUNDING_HULL )
msg = _( "Resulting polygon would be empty" );
else
msg = _( "Objects must form a closed shape" );
DisplayErrorMessage( m_frame, _( "Could not convert selection" ), msg );
m_userSettings = previousSettings;
return 0;
}
for( const SHAPE_POLY_SET& poly : polys )
{
PCB_SHAPE* graphic = new PCB_SHAPE( parentFootprint );
if( resolvedSettings.m_Strategy == COPY_LINEWIDTH )
{
BOARD_ITEM* topLeftItem = nullptr;
VECTOR2I pos;
for( EDA_ITEM* item : selection )
{
if( !item->IsBOARD_ITEM() )
continue;
BOARD_ITEM* candidate = static_cast<BOARD_ITEM*>( item );
if( candidate->HasLineStroke() )
{
pos = candidate->GetPosition();
if( !topLeftItem
|| ( pos.x < topLeftItem->GetPosition().x )
|| ( topLeftItem->GetPosition().x == pos.x
&& pos.y < topLeftItem->GetPosition().y ) )
{
topLeftItem = candidate;
resolvedSettings.m_LineWidth = topLeftItem->GetStroke().GetWidth();
}
}
}
}
graphic->SetShape( SHAPE_T::POLY );
graphic->SetStroke( STROKE_PARAMS( resolvedSettings.m_LineWidth, LINE_STYLE::SOLID,
COLOR4D::UNSPECIFIED ) );
graphic->SetFilled( resolvedSettings.m_Strategy == CENTERLINE );
graphic->SetLayer( destLayer );
graphic->SetPolyShape( poly );
commit.Add( graphic );
}
}
else
{
// Creating zone or keepout
PCB_BASE_EDIT_FRAME* frame = getEditFrame<PCB_BASE_EDIT_FRAME>();
BOARD_ITEM_CONTAINER* parent = frame->GetModel();
ZONE_SETTINGS zoneInfo = bds.GetDefaultZoneSettings();
bool nonCopper = IsNonCopperLayer( destLayer );
zoneInfo.m_Layers.reset().set( destLayer );
zoneInfo.m_Name.Empty();
int ret;
// No copy-line-width option for zones/keepouts
if( m_userSettings.m_Strategy == COPY_LINEWIDTH )
m_userSettings.m_Strategy = CENTERLINE;
if( aEvent.IsAction( &PCB_ACTIONS::convertToKeepout ) )
{
zoneInfo.SetIsRuleArea( true );
ret = InvokeRuleAreaEditor( frame, &zoneInfo, board(), &m_userSettings );
}
else if( nonCopper )
{
zoneInfo.SetIsRuleArea( false );
ret = InvokeNonCopperZonesEditor( frame, &zoneInfo, &m_userSettings );
}
else
{
zoneInfo.SetIsRuleArea( false );
ret = InvokeCopperZonesEditor( frame, &zoneInfo, &m_userSettings );
}
if( ret == wxID_CANCEL )
return 0;
if( !getPolys( m_userSettings ) )
return 0;
for( const SHAPE_POLY_SET& poly : polys )
{
ZONE* zone = new ZONE( parent );
*zone->Outline() = poly;
zone->HatchBorder();
zoneInfo.ExportSetting( *zone );
commit.Add( zone );
}
}
if( m_userSettings.m_DeleteOriginals )
{
PCB_SELECTION selectionCopy = selection;
m_selectionTool->ClearSelection();
for( EDA_ITEM* item : selectionCopy )
{
if( item->GetFlags() & SKIP_STRUCT )
commit.Remove( item );
}
}
if( aEvent.IsAction( &PCB_ACTIONS::convertToPoly ) )
{
if( m_userSettings.m_DeleteOriginals )
commit.Push( _( "Convert to Polygon" ) );
else
commit.Push( _( "Create Polygon" ) );
}
else
{
if( m_userSettings.m_DeleteOriginals )
commit.Push( _( "Convert to Zone" ) );
else
commit.Push( _( "Create Zone" ) );
}
return 0;
}
SHAPE_POLY_SET CONVERT_TOOL::makePolysFromChainedSegs( const std::deque<EDA_ITEM*>& aItems,
CONVERT_STRATEGY aStrategy )
{
// TODO: This code has a somewhat-similar purpose to ConvertOutlineToPolygon but is slightly
// different, so this remains a separate algorithm. It might be nice to analyze the dfiferences
// in requirements and refactor this.
// Using a large epsilon here to allow for sloppy drawing can cause the algorithm to miss very
// short segments in a converted bezier. So use an epsilon only large enough to cover for
// rouding errors in the conversion.
int chainingEpsilon = 100; // max dist from one endPt to next startPt in IU
SHAPE_POLY_SET poly;
// Stores pairs of (anchor, item) where anchor == 0 -> SEG.A, anchor == 1 -> SEG.B
std::map<VECTOR2I, std::vector<std::pair<int, EDA_ITEM*>>> connections;
std::deque<EDA_ITEM*> toCheck;
auto closeEnough =
[]( const VECTOR2I& aLeft, const VECTOR2I& aRight, int aLimit )
{
return ( aLeft - aRight ).SquaredEuclideanNorm() <= SEG::Square( aLimit );
};
auto findInsertionPoint =
[&]( const VECTOR2I& aPoint ) -> VECTOR2I
{
if( connections.count( aPoint ) )
return aPoint;
for( const auto& candidatePair : connections )
{
if( closeEnough( aPoint, candidatePair.first, chainingEpsilon ) )
return candidatePair.first;
}
return aPoint;
};
for( EDA_ITEM* item : aItems )
{
if( std::optional<SEG> seg = getStartEndPoints( item ) )
{
toCheck.push_back( item );
connections[findInsertionPoint( seg->A )].emplace_back( std::make_pair( 0, item ) );
connections[findInsertionPoint( seg->B )].emplace_back( std::make_pair( 1, item ) );
}
}
while( !toCheck.empty() )
{
std::vector<BOARD_ITEM*> insertedItems;
EDA_ITEM* candidate = toCheck.front();
toCheck.pop_front();
if( candidate->GetFlags() & SKIP_STRUCT )
continue;
SHAPE_LINE_CHAIN outline;
auto insert =
[&]( EDA_ITEM* aItem, const VECTOR2I& aAnchor, bool aDirection )
{
if( aItem->IsType( { PCB_ARC_T, PCB_SHAPE_LOCATE_ARC_T } ) )
{
SHAPE_ARC arc;
if( aItem->Type() == PCB_ARC_T )
{
PCB_ARC* pcb_arc = static_cast<PCB_ARC*>( aItem );
arc = *static_cast<SHAPE_ARC*>( pcb_arc->GetEffectiveShape().get() );
}
else
{
PCB_SHAPE* pcb_shape = static_cast<PCB_SHAPE*>( aItem );
arc = SHAPE_ARC( pcb_shape->GetStart(), pcb_shape->GetArcMid(),
pcb_shape->GetEnd(), pcb_shape->GetWidth() );
}
if( aDirection )
outline.Append( aAnchor == arc.GetP0() ? arc : arc.Reversed() );
else
outline.Insert( 0, aAnchor == arc.GetP0() ? arc : arc.Reversed() );
insertedItems.push_back( static_cast<BOARD_ITEM*>( aItem ) );
}
else if( aItem->IsType( { PCB_SHAPE_LOCATE_BEZIER_T } ) )
{
PCB_SHAPE* graphic = static_cast<PCB_SHAPE*>( aItem );
if( aAnchor == graphic->GetStart() )
{
for( const VECTOR2I& pt : graphic->GetBezierPoints() )
{
if( aDirection )
outline.Append( pt );
else
outline.Insert( 0, pt );
}
}
else
{
for( const VECTOR2I& pt : std::ranges::reverse_view( graphic->GetBezierPoints() ) )
{
if( aDirection )
outline.Append( pt );
else
outline.Insert( 0, pt );
}
}
insertedItems.push_back( static_cast<BOARD_ITEM*>( aItem ) );
}
else if( std::optional<SEG> nextSeg = getStartEndPoints( aItem ) )
{
VECTOR2I& point = ( aAnchor == nextSeg->A ) ? nextSeg->B : nextSeg->A;
if( aDirection )
outline.Append( point );
else
outline.Insert( 0, point );
insertedItems.push_back( static_cast<BOARD_ITEM*>( aItem ) );
}
};
// aDirection == true for walking "right" and appending to the end of points
// false for walking "left" and prepending to the beginning
std::function<void( EDA_ITEM*, const VECTOR2I&, bool )> process =
[&]( EDA_ITEM* aItem, const VECTOR2I& aAnchor, bool aDirection )
{
if( aItem->GetFlags() & SKIP_STRUCT )
return;
aItem->SetFlags( SKIP_STRUCT );
insert( aItem, aAnchor, aDirection );
std::optional<SEG> anchors = getStartEndPoints( aItem );
wxASSERT( anchors );
VECTOR2I nextAnchor = ( aAnchor == anchors->A ) ? anchors->B : anchors->A;
for( std::pair<int, EDA_ITEM*> pair : connections[nextAnchor] )
{
if( pair.second == aItem )
continue;
process( pair.second, nextAnchor, aDirection );
}
};
std::optional<SEG> anchors = getStartEndPoints( candidate );
wxASSERT( anchors );
// Start with the first object and walk "right"
// Note if the first object is an arc, we don't need to insert its first point here, the
// whole arc will be inserted at anchor B inside process()
if( !candidate->IsType( { PCB_ARC_T, PCB_SHAPE_LOCATE_ARC_T } ) )
insert( candidate, anchors->A, true );
process( candidate, anchors->B, true );
// check for any candidates on the "left"
EDA_ITEM* left = nullptr;
for( std::pair<int, EDA_ITEM*> possibleLeft : connections[anchors->A] )
{
if( possibleLeft.second != candidate )
{
left = possibleLeft.second;
break;
}
}
if( left )
process( left, anchors->A, false );
if( outline.PointCount() < 3
|| !closeEnough( outline.GetPoint( 0 ), outline.GetPoint( -1 ), chainingEpsilon ) )
{
for( EDA_ITEM* item : insertedItems )
item->ClearFlags( SKIP_STRUCT );
continue;
}
outline.SetClosed( true );
poly.AddOutline( outline );
if( aStrategy == BOUNDING_HULL )
{
for( BOARD_ITEM* item : insertedItems )
item->TransformShapeToPolygon( poly, UNDEFINED_LAYER, 0, item->GetMaxError(), ERROR_INSIDE );
}
insertedItems.clear();
}
return poly;
}
SHAPE_POLY_SET CONVERT_TOOL::makePolysFromOpenGraphics( const std::deque<EDA_ITEM*>& aItems, int aGap )
{
SHAPE_POLY_SET poly;
for( EDA_ITEM* item : aItems )
{
if( item->GetFlags() & SKIP_STRUCT )
continue;
switch( item->Type() )
{
case PCB_SHAPE_T:
{
PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
if( shape->IsClosed() )
continue;
shape->TransformShapeToPolygon( poly, UNDEFINED_LAYER, aGap, shape->GetMaxError(), ERROR_INSIDE );
shape->SetFlags( SKIP_STRUCT );
break;
}
case PCB_TRACE_T:
case PCB_ARC_T:
case PCB_VIA_T:
{
PCB_TRACK* track = static_cast<PCB_TRACK*>( item );
track->TransformShapeToPolygon( poly, UNDEFINED_LAYER, aGap, track->GetMaxError(), ERROR_INSIDE );
track->SetFlags( SKIP_STRUCT );
break;
}
default:
continue;
}
}
return poly;
}
SHAPE_POLY_SET CONVERT_TOOL::makePolysFromClosedGraphics( const std::deque<EDA_ITEM*>& aItems,
CONVERT_STRATEGY aStrategy )
{
SHAPE_POLY_SET poly;
for( EDA_ITEM* item : aItems )
{
if( item->GetFlags() & SKIP_STRUCT )
continue;
switch( item->Type() )
{
case PCB_SHAPE_T:
{
PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
FILL_T wasFilled = shape->GetFillMode();
if( !shape->IsClosed() )
continue;
if( shape->GetShape() == SHAPE_T::CIRCLE && aStrategy != BOUNDING_HULL )
{
VECTOR2I c = shape->GetCenter();
int R = shape->GetRadius();
SHAPE_ARC arc( c - VECTOR2I( R, 0 ), c + VECTOR2I( R, 0 ), c - VECTOR2I( R, 0 ), 0 );
poly.NewOutline();
poly.Append( arc );
}
else
{
if( aStrategy != BOUNDING_HULL )
shape->SetFilled( true );
shape->TransformShapeToPolygon( poly, UNDEFINED_LAYER, 0, shape->GetMaxError(), ERROR_INSIDE,
aStrategy != BOUNDING_HULL );
if( aStrategy != BOUNDING_HULL )
shape->SetFillMode( wasFilled );
}
shape->SetFlags( SKIP_STRUCT );
break;
}
case PCB_ZONE_T:
poly.Append( *static_cast<ZONE*>( item )->Outline() );
item->SetFlags( SKIP_STRUCT );
break;
case PCB_FIELD_T:
case PCB_TEXT_T:
{
PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
text->TransformTextToPolySet( poly, 0, text->GetMaxError(), ERROR_INSIDE );
text->SetFlags( SKIP_STRUCT );
break;
}
case PCB_PAD_T:
{
PAD* pad = static_cast<PAD*>( item );
pad->Padstack().ForEachUniqueLayer(
[&]( PCB_LAYER_ID aLayer )
{
SHAPE_POLY_SET layerPoly;
pad->TransformShapeToPolygon( layerPoly, aLayer, 0, pad->GetMaxError(), ERROR_INSIDE );
poly.BooleanAdd( layerPoly );
} );
pad->SetFlags( SKIP_STRUCT );
break;
}
default:
continue;
}
}
return poly;
}
int CONVERT_TOOL::CreateLines( const TOOL_EVENT& aEvent )
{
auto& selection = m_selectionTool->RequestSelection(
[]( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
{
for( int i = aCollector.GetCount() - 1; i >= 0; --i )
{
BOARD_ITEM* item = aCollector[i];
switch( item->Type() )
{
case PCB_SHAPE_T:
switch( static_cast<PCB_SHAPE*>( item )->GetShape() )
{
case SHAPE_T::SEGMENT:
case SHAPE_T::ARC:
case SHAPE_T::POLY:
case SHAPE_T::RECTANGLE:
break;
default:
aCollector.Remove( item );
}
break;
case PCB_ZONE_T:
break;
default:
aCollector.Remove( item );
}
}
} );
if( selection.Empty() )
return 0;
auto getPolySet =
[]( EDA_ITEM* aItem )
{
SHAPE_POLY_SET set;
switch( aItem->Type() )
{
case PCB_ZONE_T:
set = *static_cast<ZONE*>( aItem )->Outline();
break;
case PCB_SHAPE_T:
{
PCB_SHAPE* graphic = static_cast<PCB_SHAPE*>( aItem );
if( graphic->GetShape() == SHAPE_T::POLY )
{
set = graphic->GetPolyShape();
}
else if( graphic->GetShape() == SHAPE_T::RECTANGLE )
{
SHAPE_LINE_CHAIN outline;
VECTOR2I start( graphic->GetStart() );
VECTOR2I end( graphic->GetEnd() );
outline.Append( start );
outline.Append( VECTOR2I( end.x, start.y ) );
outline.Append( end );
outline.Append( VECTOR2I( start.x, end.y ) );
outline.SetClosed( true );
set.AddOutline( outline );
}
else
{
wxFAIL_MSG( wxT( "Unhandled graphic shape type in PolyToLines - getPolySet" ) );
}
break;
}
default:
wxFAIL_MSG( wxT( "Unhandled type in PolyToLines - getPolySet" ) );
break;
}
return set;
};
auto getSegList =
[]( SHAPE_POLY_SET& aPoly )
{
std::vector<SEG> segs;
// Our input should be valid polys, so OK to assert here
wxASSERT( aPoly.VertexCount() >= 2 );
for( int i = 1; i < aPoly.VertexCount(); i++ )
segs.emplace_back( SEG( aPoly.CVertex( i - 1 ), aPoly.CVertex( i ) ) );
segs.emplace_back( SEG( aPoly.CVertex( aPoly.VertexCount() - 1 ), aPoly.CVertex( 0 ) ) );
return segs;
};
BOARD_COMMIT commit( m_frame );
PCB_BASE_EDIT_FRAME* frame = getEditFrame<PCB_BASE_EDIT_FRAME>();
FOOTPRINT_EDIT_FRAME* fpEditor = dynamic_cast<FOOTPRINT_EDIT_FRAME*>( m_frame );
FOOTPRINT* footprint = nullptr;
PCB_LAYER_ID targetLayer = m_frame->GetActiveLayer();
BOARD_ITEM_CONTAINER* parent = frame->GetModel();
if( fpEditor )
footprint = fpEditor->GetBoard()->GetFirstFootprint();
auto handleGraphicSeg =
[&]( EDA_ITEM* aItem )
{
if( aItem->Type() != PCB_SHAPE_T )
return false;
PCB_SHAPE* graphic = static_cast<PCB_SHAPE*>( aItem );
if( graphic->GetShape() == SHAPE_T::SEGMENT )
{
PCB_TRACK* track = new PCB_TRACK( parent );
track->SetLayer( targetLayer );
track->SetStart( graphic->GetStart() );
track->SetEnd( graphic->GetEnd() );
track->SetWidth( graphic->GetWidth() );
commit.Add( track );
return true;
}
else if( graphic->GetShape() == SHAPE_T::ARC )
{
PCB_ARC* arc = new PCB_ARC( parent );
arc->SetLayer( targetLayer );
arc->SetStart( graphic->GetStart() );
arc->SetEnd( graphic->GetEnd() );
arc->SetMid( graphic->GetArcMid() );
arc->SetWidth( graphic->GetWidth() );
commit.Add( arc );
return true;
}
return false;
};
if( aEvent.IsAction( &PCB_ACTIONS::convertToTracks ) )
{
if( !IsCopperLayer( targetLayer ) )
{
targetLayer = frame->SelectOneLayer( F_Cu, LSET::AllNonCuMask() );
if( targetLayer == UNDEFINED_LAYER ) // User canceled
return true;
}
}
else
{
CONVERT_SETTINGS_DIALOG dlg( m_frame, &m_userSettings, false, false, false );
if( dlg.ShowModal() != wxID_OK )
return true;
}
for( EDA_ITEM* item : selection )
{
if( !item->IsBOARD_ITEM() )
continue;
if( handleGraphicSeg( item ) )
continue;
BOARD_ITEM& boardItem = static_cast<BOARD_ITEM&>( *item );
SHAPE_POLY_SET polySet = getPolySet( item );
std::vector<SEG> segs = getSegList( polySet );
std::optional<int> itemWidth = GetBoardItemWidth( boardItem );
if( aEvent.IsAction( &PCB_ACTIONS::convertToLines ) )
{
for( SEG& seg : segs )
{
PCB_SHAPE* graphic = new PCB_SHAPE( footprint, SHAPE_T::SEGMENT );
graphic->SetLayer( targetLayer );
graphic->SetStart( VECTOR2I( seg.A ) );
graphic->SetEnd( VECTOR2I( seg.B ) );
// The width can exist but be 0 for filled, unstroked shapes
if( itemWidth && *itemWidth > 0 )
graphic->SetWidth( *itemWidth );
commit.Add( graphic );
}
}
else
{
// I am really unsure converting a polygon to "tracks" (i.e. segments on
// copper layers) make sense for footprints, but anyway this code exists
if( fpEditor )
{
// Creating segments on copper layer
for( SEG& seg : segs )
{
PCB_SHAPE* graphic = new PCB_SHAPE( footprint, SHAPE_T::SEGMENT );
graphic->SetLayer( targetLayer );
graphic->SetStart( VECTOR2I( seg.A ) );
graphic->SetEnd( VECTOR2I( seg.B ) );
if( itemWidth )
graphic->SetWidth( *itemWidth );
commit.Add( graphic );
}
}
else
{
// Creating tracks
for( SEG& seg : segs )
{
PCB_TRACK* track = new PCB_TRACK( parent );
track->SetLayer( targetLayer );
track->SetStart( VECTOR2I( seg.A ) );
track->SetEnd( VECTOR2I( seg.B ) );
commit.Add( track );
}
}
}
}
if( m_userSettings.m_DeleteOriginals )
{
PCB_SELECTION selectionCopy = selection;
m_selectionTool->ClearSelection();
for( EDA_ITEM* item : selectionCopy )
commit.Remove( item );
}
commit.Push( _( "Create Lines" ) );
return 0;
}
int CONVERT_TOOL::SegmentToArc( const TOOL_EVENT& aEvent )
{
auto& selection = m_selectionTool->RequestSelection(
[]( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
{
for( int i = aCollector.GetCount() - 1; i >= 0; --i )
{
BOARD_ITEM* item = aCollector[i];
if( !item->IsType( { PCB_SHAPE_T, PCB_TRACE_T, PCB_ARC_T } ) )
aCollector.Remove( item );
}
} );
if( selection.Empty() || !selection.Front()->IsBOARD_ITEM() )
return -1;
BOARD_ITEM* source = static_cast<BOARD_ITEM*>( selection.Front() );
VECTOR2I start, end, mid;
// Offset the midpoint along the normal a little bit so that it's more obviously an arc
const double offsetRatio = 0.1;
if( std::optional<SEG> seg = getStartEndPoints( source ) )
{
start = seg->A;
end = seg->B;
VECTOR2I normal = ( seg->B - seg->A ).Perpendicular().Resize( offsetRatio * seg->Length() );
mid = seg->Center() + normal;
}
else
{
return -1;
}
PCB_BASE_EDIT_FRAME* frame = getEditFrame<PCB_BASE_EDIT_FRAME>();
BOARD_ITEM_CONTAINER* parent = frame->GetModel();
PCB_LAYER_ID layer = source->GetLayer();
BOARD_COMMIT commit( m_frame );
if( source->Type() == PCB_SHAPE_T && static_cast<PCB_SHAPE*>( source )->GetShape() == SHAPE_T::SEGMENT )
{
PCB_SHAPE* line = static_cast<PCB_SHAPE*>( source );
PCB_SHAPE* arc = new PCB_SHAPE( parent, SHAPE_T::ARC );
VECTOR2I center = CalcArcCenter( start, mid, end );
arc->SetFilled( false );
arc->SetLayer( layer );
arc->SetStroke( line->GetStroke() );
arc->SetCenter( center );
arc->SetStart( start );
arc->SetEnd( end );
commit.Add( arc );
}
else if( source->Type() == PCB_SHAPE_T && static_cast<PCB_SHAPE*>( source )->GetShape() == SHAPE_T::ARC )
{
PCB_SHAPE* source_arc = static_cast<PCB_SHAPE*>( source );
PCB_ARC* arc = new PCB_ARC( parent );
arc->SetLayer( layer );
arc->SetWidth( source_arc->GetWidth() );
arc->SetStart( start );
arc->SetMid( source_arc->GetArcMid() );
arc->SetEnd( end );
commit.Add( arc );
}
else if( source->Type() == PCB_TRACE_T )
{
PCB_TRACK* line = static_cast<PCB_TRACK*>( source );
PCB_ARC* arc = new PCB_ARC( parent );
arc->SetLayer( layer );
arc->SetWidth( line->GetWidth() );
arc->SetStart( start );
arc->SetMid( mid );
arc->SetEnd( end );
commit.Add( arc );
}
else if( source->Type() == PCB_ARC_T )
{
PCB_ARC* source_arc = static_cast<PCB_ARC*>( source );
PCB_SHAPE* arc = new PCB_SHAPE( parent, SHAPE_T::ARC );
arc->SetFilled( false );
arc->SetLayer( layer );
arc->SetWidth( source_arc->GetWidth() );
arc->SetArcGeometry( source_arc->GetStart(), source_arc->GetMid(), source_arc->GetEnd() );
commit.Add( arc );
}
commit.Push( _( "Create Arc" ) );
return 0;
}
std::optional<SEG> CONVERT_TOOL::getStartEndPoints( EDA_ITEM* aItem )
{
switch( aItem->Type() )
{
case PCB_SHAPE_T:
{
PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( aItem );
switch( shape->GetShape() )
{
case SHAPE_T::SEGMENT:
case SHAPE_T::ARC:
case SHAPE_T::POLY:
case SHAPE_T::BEZIER:
if( shape->GetStart() == shape->GetEnd() )
return std::nullopt;
return std::make_optional<SEG>( VECTOR2I( shape->GetStart() ), VECTOR2I( shape->GetEnd() ) );
default:
return std::nullopt;
}
}
case PCB_TRACE_T:
{
PCB_TRACK* line = static_cast<PCB_TRACK*>( aItem );
return std::make_optional<SEG>( VECTOR2I( line->GetStart() ), VECTOR2I( line->GetEnd() ) );
}
case PCB_ARC_T:
{
PCB_ARC* arc = static_cast<PCB_ARC*>( aItem );
return std::make_optional<SEG>( VECTOR2I( arc->GetStart() ), VECTOR2I( arc->GetEnd() ) );
}
default:
return std::nullopt;
}
}
int CONVERT_TOOL::OutsetItems( const TOOL_EVENT& aEvent )
{
PCB_BASE_EDIT_FRAME& frame = *getEditFrame<PCB_BASE_EDIT_FRAME>();
PCB_SELECTION& selection = m_selectionTool->RequestSelection(
[]( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
{
// Iterate from the back so we don't have to worry about removals.
for( int i = aCollector.GetCount() - 1; i >= 0; --i )
{
BOARD_ITEM* item = aCollector[i];
// We've converted the polygon and rectangle to segments, so drop everything
// that isn't a segment at this point
if( !item->IsType( { PCB_PAD_T, PCB_SHAPE_T } ) )
aCollector.Remove( item );
}
},
true /* prompt user regarding locked items */ );
BOARD_COMMIT commit( this );
for( EDA_ITEM* item : selection )
item->ClearFlags( STRUCT_DELETED );
// List of thing to select at the end of the operation
// (doing it as we go will invalidate the iterator)
std::vector<BOARD_ITEM*> items_to_select_on_success;
// Handle modifications to existing items by the routine
// How to deal with this depends on whether we're in the footprint editor or not
// and whether the item was conjured up by decomposing a polygon or rectangle
auto item_modification_handler =
[&]( BOARD_ITEM& aItem )
{
};
bool any_items_created = false;
auto item_creation_handler =
[&]( std::unique_ptr<BOARD_ITEM> aItem )
{
any_items_created = true;
items_to_select_on_success.push_back( aItem.get() );
commit.Add( aItem.release() );
};
auto item_removal_handler =
[&]( BOARD_ITEM& aItem )
{
// If you do an outset on a FP pad, do you really want to delete
// the parent?
if( !aItem.GetParentFootprint() )
{
commit.Remove( &aItem );
}
};
// Combine these callbacks into a CHANGE_HANDLER to inject in the ROUTINE
ITEM_MODIFICATION_ROUTINE::CALLABLE_BASED_HANDLER change_handler( item_creation_handler,
item_modification_handler,
item_removal_handler );
// Persistent settings between dialog invocations
// Init with some sensible defaults
static OUTSET_ROUTINE::PARAMETERS outset_params_fp_edit{
pcbIUScale.mmToIU( 0.25 ), // A common outset value
false,
false,
true,
F_CrtYd,
frame.GetDesignSettings().GetLineThickness( F_CrtYd ),
pcbIUScale.mmToIU( 0.01 ),
false,
};
static OUTSET_ROUTINE::PARAMETERS outset_params_pcb_edit{
pcbIUScale.mmToIU( 1 ),
true,
true,
true,
Edge_Cuts, // Outsets often for slots?
frame.GetDesignSettings().GetLineThickness( Edge_Cuts ),
std::nullopt,
false,
};
OUTSET_ROUTINE::PARAMETERS& outset_params = IsFootprintEditor() ? outset_params_fp_edit
: outset_params_pcb_edit;
{
DIALOG_OUTSET_ITEMS dlg( frame, outset_params );
if( dlg.ShowModal() == wxID_CANCEL )
return 0;
}
OUTSET_ROUTINE outset_routine( frame.GetModel(), change_handler, outset_params );
for( EDA_ITEM* item : selection )
{
if( !item->IsBOARD_ITEM() )
continue;
BOARD_ITEM* board_item = static_cast<BOARD_ITEM*>( item );
outset_routine.ProcessItem( *board_item );
}
// Deselect all the original items
m_selectionTool->ClearSelection();
// Select added and modified items
for( BOARD_ITEM* item : items_to_select_on_success )
m_selectionTool->AddItemToSel( item, true );
if( any_items_created )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
// Notify other tools of the changes
m_toolMgr->ProcessEvent( EVENTS::SelectedItemsModified );
commit.Push( outset_routine.GetCommitDescription() );
if( const std::optional<wxString> msg = outset_routine.GetStatusMessage() )
frame.ShowInfoBarMsg( *msg );
return 0;
}
void CONVERT_TOOL::setTransitions()
{
// clang-format off
Go( &CONVERT_TOOL::CreatePolys, PCB_ACTIONS::convertToPoly.MakeEvent() );
Go( &CONVERT_TOOL::CreatePolys, PCB_ACTIONS::convertToZone.MakeEvent() );
Go( &CONVERT_TOOL::CreatePolys, PCB_ACTIONS::convertToKeepout.MakeEvent() );
Go( &CONVERT_TOOL::CreateLines, PCB_ACTIONS::convertToLines.MakeEvent() );
Go( &CONVERT_TOOL::CreateLines, PCB_ACTIONS::convertToTracks.MakeEvent() );
Go( &CONVERT_TOOL::SegmentToArc, PCB_ACTIONS::convertToArc.MakeEvent() );
Go( &CONVERT_TOOL::OutsetItems, PCB_ACTIONS::outsetItems.MakeEvent() );
// clang-format on
}