Feature: Exact item offset tool

This is a little bit like the bounding hull tool, but the
output is "exact" and it only supports the most common
source items.

By 'exact', this means that rounded corners are real arc
segments rather than polygonal approximations. Obviously,
this is rather tricky in the general case, and especially
for any concave shape or anything with a bezier in it.

Envisioned main uses:

* Creating courtyard and silkscreen offsets in footprints
* Making slots around line or arcs.

The one thing that it does not currently do, but which it might
plausibly do without reimplementing Clipper is convex polygons,
which would bring trapezoidal pad outsets for free. But that
is a stretch goal, and bounding hull can be used.
This commit is contained in:
John Beard 2024-09-12 19:28:13 +01:00
parent 46e7228945
commit 8fdb6d6e88
23 changed files with 2913 additions and 31 deletions

View File

@ -24,6 +24,7 @@ set( KIMATH_SRCS
src/geometry/line.cpp
src/geometry/nearest.cpp
src/geometry/oval.cpp
src/geometry/roundrect.cpp
src/geometry/seg.cpp
src/geometry/shape.cpp
src/geometry/shape_arc.cpp

View File

@ -0,0 +1,84 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#include <geometry/shape_rect.h>
/**
* A round rectangle shape, based on a rectangle and a radius.
*
* For now, not an inheritor of SHAPE as that means implementing a
* lot of Collision logic. Then again, this is a common shape, could be more efficient
* to do the collision using arcs rather than Clipper'ing pad outsets.
*/
class ROUNDRECT
{
public:
ROUNDRECT() : m_rect(), m_radius( 0 ) {}
ROUNDRECT( SHAPE_RECT aRect, int aRadius );
static ROUNDRECT OutsetFrom( const SHAPE_RECT& aRect, int aOutset );
int GetRoundRadius() const { return m_radius; }
/**
* Get the basis rectangle of the roundrect.
*
* This is the rectangle without the rounded corners.
*/
const SHAPE_RECT& GetRect() const { return m_rect; }
/**
* Shortcut for common values
*/
int GetWidth() const { return m_rect.GetWidth(); }
int GetHeight() const { return m_rect.GetHeight(); }
VECTOR2I GetPosition() const { return m_rect.GetPosition(); }
/**
* Get the bounding box of the roundrect.
*
* (This is always the same as the basis rectangle's bounding box.)
*/
BOX2I BBox() const { return m_rect.BBox(); }
/**
* Get the roundrect with the size increased by aOutset in all directions.
* (the radius increases by aOutset as well).
*/
ROUNDRECT GetInflated( int aOutset ) const;
/**
* Get the polygonal representation of the roundrect.
*/
void TransformToPolygon( SHAPE_POLY_SET& aBuffer, int aError, ERROR_LOC aErrorLoc ) const
/*override */;
private:
SHAPE_RECT m_rect;
int m_radius;
};

View File

@ -106,6 +106,19 @@ public:
return bbox;
}
/**
* Return a rectangle that is larger by aOffset in all directions,
* but still centered on the original rectangle.
*/
SHAPE_RECT GetInflated( int aOffset ) const
{
return SHAPE_RECT{
m_p0 - VECTOR2I( aOffset, aOffset ),
m_w + 2 * aOffset,
m_h + 2 * aOffset,
};
}
/**
* Return length of the diagonal of the rectangle.
*
@ -116,6 +129,16 @@ public:
return VECTOR2I( m_w, m_h ).EuclideanNorm();
}
int MajorDimension() const
{
return std::max( m_w, m_h );
}
int MinorDimension() const
{
return std::min( m_w, m_h );
}
bool Collide( const SHAPE* aShape, int aClearance, VECTOR2I* aMTV ) const override
{
return SHAPE::Collide( aShape, aClearance, aMTV );

View File

@ -39,9 +39,12 @@
#include <math/vector2d.h>
#include <math/box2.h>
#include <geometry/direction45.h>
class HALF_LINE;
class LINE;
class SEG;
class SHAPE_RECT;
namespace KIGEOM
{
@ -69,4 +72,39 @@ std::optional<SEG> ClipHalfLineToBox( const HALF_LINE& aRay, const BOX2I& aBox )
*/
std::optional<SEG> ClipLineToBox( const LINE& aLine, const BOX2I& aBox );
/**
* Get a SHAPE_ARC representing a 90-degree arc in the clockwise direction with the
* midpoint in the given direction from the center.
*
* _
* \
* + | <--- This is the NE arc from the + point.
*
* +-->x
* | (So Southerly point are bigger in y)
* v y
*
* @param aCenter is the arc center.
* @param aRadius is the arc radius.
* @param aDir is the direction from the center to the midpoint (only NW, NE, SW, SE are valid).
*/
SHAPE_ARC MakeArcCw90( const VECTOR2I& aCenter, int aRadius, DIRECTION_45::Directions aDir );
/**
* Get a SHAPE_ARC representing a 180-degree arc in the clockwise direction with the
* midpoint in the given direction from the center.
*
* @param aDir is the direction from the center to the midpoint (only N, E, S, W are valid).
*/
SHAPE_ARC MakeArcCw180( const VECTOR2I& aCenter, int aRadius, DIRECTION_45::Directions aDir );
/**
* Get the point on a rectangle that corresponds to a given direction.
*
* For directions N, E, S, W, the point is the center of the side.
* For directions NW, NE, SW, SE, the point is the corner.
*/
VECTOR2I GetPoint( const SHAPE_RECT& aRect, DIRECTION_45::Directions aDir );
} // namespace KIGEOM

View File

@ -116,4 +116,23 @@ double GetProjectedPointLengthRatio( const VECTOR2I& aPoint, const SEG& aSeg );
*/
const VECTOR2I& GetNearestEndpoint( const SEG& aSeg, const VECTOR2I& aPoint );
/**
* Round a vector to the nearest grid point in any direction.
*/
VECTOR2I RoundGrid( const VECTOR2I& aVec, int aGridSize );
/**
* Round a vector to the nearest grid point in the NW direction.
*
* This means x and y are both rounded downwards (regardless of sign).
*/
VECTOR2I RoundNW( const VECTOR2I& aVec, int aGridSize );
/**
* Round a vector to the nearest grid point in the SE direction.
*
* This means x and y are both rounded upwards (regardless of sign).
*/
VECTOR2I RoundSE( const VECTOR2I& aVec, int aGridSize );
} // namespace KIGEOM

View File

@ -76,6 +76,7 @@ SHAPE_LINE_CHAIN KIGEOM::ConvertToChain( const OVAL& aOval )
chain.Append( SHAPE_ARC( seg.A, seg.A - perp, ANGLE_180 ) );
chain.Append( seg.B + perp );
chain.Append( SHAPE_ARC( seg.B, seg.B + perp, ANGLE_180 ) );
chain.SetClosed( true );
return chain;
}

View File

@ -0,0 +1,162 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2017 CERN
* Copyright (C) 2019-2024 KiCad Developers, see AUTHORS.txt for contributors.
* @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "geometry/roundrect.h"
#include <stdexcept>
#include <geometry/shape_poly_set.h>
#include <geometry/shape_utils.h>
namespace
{
SHAPE_ARC MakeCornerArcCw90( const SHAPE_RECT& aRect, int aRadius, DIRECTION_45::Directions aDir )
{
const VECTOR2I center = KIGEOM::GetPoint( aRect, aDir );
return KIGEOM::MakeArcCw90( center, aRadius, aDir );
}
SHAPE_ARC MakeSideArcCw180( const SHAPE_RECT& aRect, int aRadius, DIRECTION_45::Directions aDir )
{
const VECTOR2I center = KIGEOM::GetPoint( aRect, aDir );
return KIGEOM::MakeArcCw180( center, aRadius, aDir );
}
} // namespace
ROUNDRECT::ROUNDRECT( SHAPE_RECT aRect, int aRadius ) :
m_rect( std::move( aRect ) ), m_radius( aRadius )
{
if( m_radius > m_rect.MajorDimension() )
{
throw std::invalid_argument(
"Roundrect radius is larger than the rectangle's major dimension" );
}
if( m_radius < 0 )
{
throw std::invalid_argument( "Roundrect radius must be non-negative" );
}
}
ROUNDRECT ROUNDRECT::OutsetFrom( const SHAPE_RECT& aRect, int aOutset )
{
return ROUNDRECT( aRect.GetInflated( aOutset ), aOutset );
}
ROUNDRECT ROUNDRECT::GetInflated( int aOutset ) const
{
return ROUNDRECT( m_rect.GetInflated( aOutset ), m_radius + aOutset );
}
void ROUNDRECT::TransformToPolygon( SHAPE_POLY_SET& aBuffer, int aError, ERROR_LOC aErrorLoc ) const
{
const int idx = aBuffer.NewOutline();
SHAPE_LINE_CHAIN& outline = aBuffer.Outline( idx );
const int w = m_rect.GetWidth();
const int h = m_rect.GetHeight();
const int x_edge = m_rect.GetWidth() - 2 * m_radius;
const int y_edge = m_rect.GetHeight() - 2 * m_radius;
// This is a class invariant
wxASSERT( x_edge >= 0 );
wxASSERT( y_edge >= 0 );
wxASSERT( m_radius >= 0 );
const VECTOR2I& m_p0 = m_rect.GetPosition();
if( m_radius == 0 )
{
// It's just a rectangle
outline.Append( m_p0 );
outline.Append( m_p0 + VECTOR2I( w, 0 ) );
outline.Append( m_p0 + VECTOR2I( w, h ) );
outline.Append( m_p0 + VECTOR2I( 0, h ) );
}
else if( x_edge == 0 && y_edge == 0 )
{
// It's a circle
outline.Append( SHAPE_ARC( m_p0 + VECTOR2I( m_radius, m_radius ),
m_p0 + VECTOR2I( -m_radius, 0 ), ANGLE_360 ) );
}
else
{
const SHAPE_RECT inner_rect{ m_p0 + VECTOR2I( m_radius, m_radius ), x_edge, y_edge };
if( x_edge > 0 )
{
// Either a normal roundrect or an oval with x_edge > 0
// Start to the right of the top left radius
outline.Append( m_p0 + VECTOR2I( m_radius, 0 ) );
// Top side
outline.Append( m_p0 + VECTOR2I( m_radius + x_edge, 0 ) );
if( y_edge > 0 )
{
outline.Append( MakeCornerArcCw90( inner_rect, m_radius, DIRECTION_45::NE ) );
outline.Append( m_p0 + VECTOR2I( w, m_radius + y_edge ) );
outline.Append( MakeCornerArcCw90( inner_rect, m_radius, DIRECTION_45::SE ) );
}
else
{
outline.Append( MakeSideArcCw180( inner_rect, m_radius, DIRECTION_45::E ) );
}
// Bottom side
outline.Append( m_p0 + VECTOR2I( m_radius, h ) );
if( y_edge > 0 )
{
outline.Append( MakeCornerArcCw90( inner_rect, m_radius, DIRECTION_45::SW ) );
outline.Append( m_p0 + VECTOR2I( 0, m_radius ) );
outline.Append( MakeCornerArcCw90( inner_rect, m_radius, DIRECTION_45::NW ) );
}
else
{
outline.Append( MakeSideArcCw180( inner_rect, m_radius, DIRECTION_45::W ) );
}
}
else
{
// x_edge is 0 but y_edge is not, so it's an oval the other way up
outline.Append( m_p0 + VECTOR2I( 0, m_radius ) );
outline.Append( MakeSideArcCw180( inner_rect, m_radius, DIRECTION_45::N ) );
outline.Append( m_p0 + VECTOR2I( w, m_radius + y_edge ) );
outline.Append( MakeSideArcCw180( inner_rect, m_radius, DIRECTION_45::S ) );
}
}
outline.SetClosed( true );
}

View File

@ -26,6 +26,7 @@
#include <geometry/seg.h>
#include <geometry/half_line.h>
#include <geometry/line.h>
#include <geometry/shape_rect.h>
SEG KIGEOM::NormalisedSeg( const SEG& aSeg )
@ -138,4 +139,94 @@ std::optional<SEG> KIGEOM::ClipLineToBox( const LINE& aLine, const BOX2I& aBox )
}
return std::nullopt;
}
}
SHAPE_ARC KIGEOM::MakeArcCw90( const VECTOR2I& aCenter, int aRadius, DIRECTION_45::Directions aDir )
{
switch( aDir )
{
case DIRECTION_45::NW:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( -aRadius, 0 ),
ANGLE_90,
};
case DIRECTION_45::NE:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( 0, -aRadius ),
ANGLE_90,
};
case DIRECTION_45::SW:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( 0, aRadius ),
ANGLE_90,
};
case DIRECTION_45::SE:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( aRadius, 0 ),
ANGLE_90,
};
default: wxFAIL_MSG( "Invalid direction" ); return SHAPE_ARC();
}
}
SHAPE_ARC KIGEOM::MakeArcCw180( const VECTOR2I& aCenter, int aRadius,
DIRECTION_45::Directions aDir )
{
switch( aDir )
{
case DIRECTION_45::N:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( -aRadius, 0 ),
ANGLE_180,
};
case DIRECTION_45::E:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( 0, -aRadius ),
ANGLE_180,
};
case DIRECTION_45::S:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( aRadius, 0 ),
ANGLE_180,
};
case DIRECTION_45::W:
return SHAPE_ARC{
aCenter,
aCenter + VECTOR2I( 0, aRadius ),
ANGLE_180,
};
default: wxFAIL_MSG( "Invalid direction" );
}
return SHAPE_ARC();
}
VECTOR2I KIGEOM::GetPoint( const SHAPE_RECT& aRect, DIRECTION_45::Directions aDir )
{
const VECTOR2I nw = aRect.GetPosition();
switch( aDir )
{
// clang-format off
case DIRECTION_45::N: return nw + VECTOR2I( aRect.GetWidth() / 2, 0 );
case DIRECTION_45::E: return nw + VECTOR2I( aRect.GetWidth(), aRect.GetHeight() / 2 );
case DIRECTION_45::S: return nw + VECTOR2I( aRect.GetWidth() / 2, aRect.GetHeight() );
case DIRECTION_45::W: return nw + VECTOR2I( 0, aRect.GetHeight() / 2 );
case DIRECTION_45::NW: return nw;
case DIRECTION_45::NE: return nw + VECTOR2I( aRect.GetWidth(), 0 );
case DIRECTION_45::SW: return nw + VECTOR2I( 0, aRect.GetHeight() );
case DIRECTION_45::SE: return nw + VECTOR2I( aRect.GetWidth(), aRect.GetHeight() );
default: wxFAIL_MSG( "Invalid direction" );
// clang-format on
}
return VECTOR2I();
}

View File

@ -64,4 +64,37 @@ const VECTOR2I& KIGEOM::GetNearestEndpoint( const SEG& aSeg, const VECTOR2I& aPo
const double distToCBStart = aSeg.A.Distance( aPoint );
const double distToCBEnd = aSeg.B.Distance( aPoint );
return ( distToCBStart <= distToCBEnd ) ? aSeg.A : aSeg.B;
}
template <typename T>
static constexpr T RoundNearest( T x, T g )
{
return ( x + ( x < 0 ? -g / 2 : g / 2 ) ) / g * g;
}
template <typename T>
static constexpr T RoundDown( T x, T g )
{
return ( ( x < 0 ? ( x - g + 1 ) : x ) / g ) * g;
}
template <typename T>
static constexpr T RoundUp( T x, T g )
{
return ( ( x < 0 ? x : ( x + g - 1 ) ) / g ) * g;
}
VECTOR2I KIGEOM::RoundGrid( const VECTOR2I& aVec, int aGridSize )
{
return VECTOR2I( RoundNearest( aVec.x, aGridSize ), RoundNearest( aVec.y, aGridSize ) );
}
VECTOR2I KIGEOM::RoundNW( const VECTOR2I& aVec, int aGridSize )
{
return VECTOR2I( RoundDown( aVec.x, aGridSize ), RoundDown( aVec.y, aGridSize ) );
}
VECTOR2I KIGEOM::RoundSE( const VECTOR2I& aVec, int aGridSize )
{
return VECTOR2I( RoundUp( aVec.x, aGridSize ), RoundUp( aVec.y, aGridSize ) );
}

View File

@ -119,6 +119,8 @@ set( PCBNEW_DIALOGS
dialogs/dialog_import_netlist_base.cpp
dialogs/dialog_non_copper_zones_properties.cpp
dialogs/dialog_non_copper_zones_properties_base.cpp
dialogs/dialog_outset_items.cpp
dialogs/dialog_outset_items_base.cpp
dialogs/dialog_pad_properties.cpp
dialogs/dialog_pad_properties_base.cpp
dialogs/dialog_plot.cpp

View File

@ -0,0 +1,196 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "dialogs/dialog_outset_items.h"
#include <board.h>
#include <board_design_settings.h>
#include <pcb_layer_box_selector.h>
/**
* Some handy preset values for common outset distances.
*/
static const std::vector<int> s_outsetPresetValue{
// Outsetting a 0.1mm line to touch a 0.1mm line
pcbIUScale.mmToIU( 0.1 ),
// 0.12mm line to touch a 0.1mm line
pcbIUScale.mmToIU( 0.11 ),
// IPC dense courtyard
pcbIUScale.mmToIU( 0.15 ),
// IPC normal courtyard
pcbIUScale.mmToIU( 0.25 ),
// Keep 0.12mm silkscreen line 0.2mm from copper
pcbIUScale.mmToIU( 0.26 ),
// IPC connector courtyard
pcbIUScale.mmToIU( 0.5 ),
// Common router bits
pcbIUScale.mmToIU( 1.0 ),
pcbIUScale.mmToIU( 2.0 ),
};
// Ther user can also get the current board design settings widths
// with the "Layer Default" button.
static const std::vector<int> s_presetLineWidths{
// Courtyard
pcbIUScale.mmToIU( 0.05 ),
pcbIUScale.mmToIU( 0.1 ),
// Silkscreen
pcbIUScale.mmToIU( 0.12 ),
pcbIUScale.mmToIU( 0.15 ),
pcbIUScale.mmToIU( 0.2 ),
};
static const std::vector<int> s_presetGridRounding{
// 0.01 is a common IPC grid round-off value
pcbIUScale.mmToIU( 0.01 ),
};
static int s_gridRoundValuePersist = s_presetGridRounding[0];
static std::vector<int> s_outsetRecentValues;
static std::vector<int> s_lineWidthRecentValues;
static std::vector<int> s_gridRoundingRecentValues;
DIALOG_OUTSET_ITEMS::DIALOG_OUTSET_ITEMS( PCB_BASE_FRAME& aParent,
OUTSET_ROUTINE::PARAMETERS& aParams ) :
DIALOG_OUTSET_ITEMS_BASE( &aParent ), m_parent( aParent ), m_params( aParams ),
m_outset( &aParent, m_outsetLabel, m_outsetEntry, m_outsetUnit ),
m_lineWidth( &aParent, m_lineWidthLabel, m_lineWidthEntry, m_lineWidthUnit ),
m_roundingGrid( &aParent, m_gridRoundingLabel, m_gridRoundingEntry, m_gridRoundingUnit )
{
m_LayerSelectionCtrl->ShowNonActivatedLayers( false );
m_LayerSelectionCtrl->SetLayersHotkeys( false );
m_LayerSelectionCtrl->SetBoardFrame( &aParent );
m_LayerSelectionCtrl->Resync();
const auto fillOptionList = [&]( UNIT_BINDER& aCombo, const std::vector<int>& aPresets,
const std::vector<int>& aRecentPresets )
{
std::vector<long long int> optionList;
optionList.reserve( aPresets.size() + aRecentPresets.size() );
for( const int val : aPresets )
optionList.push_back( val );
for( const int val : aRecentPresets )
optionList.push_back( val );
// Sort the vector and remove duplicates
std::sort( optionList.begin(), optionList.end() );
optionList.erase( std::unique( optionList.begin(), optionList.end() ), optionList.end() );
aCombo.SetOptionsList( optionList );
};
fillOptionList( m_outset, s_outsetPresetValue, s_outsetRecentValues );
fillOptionList( m_lineWidth, s_presetLineWidths, s_lineWidthRecentValues );
fillOptionList( m_roundingGrid, s_presetGridRounding, s_gridRoundingRecentValues );
SetupStandardButtons();
finishDialogSettings();
}
DIALOG_OUTSET_ITEMS::~DIALOG_OUTSET_ITEMS()
{
}
void DIALOG_OUTSET_ITEMS::OnLayerDefaultClick( wxCommandEvent& event )
{
const BOARD_DESIGN_SETTINGS& settings = m_parent.GetBoard()->GetDesignSettings();
const PCB_LAYER_ID selLayer = ToLAYER_ID( m_LayerSelectionCtrl->GetLayerSelection() );
const int defaultWidth = settings.GetLineThickness( selLayer );
m_lineWidth.SetValue( defaultWidth );
}
void DIALOG_OUTSET_ITEMS::OnCopyLayersChecked( wxCommandEvent& event )
{
m_LayerSelectionCtrl->Enable( !m_copyLayers->GetValue() );
}
void DIALOG_OUTSET_ITEMS::OnRoundToGridChecked( wxCommandEvent& event )
{
m_gridRoundingEntry->Enable( m_roundToGrid->IsChecked() );
}
bool DIALOG_OUTSET_ITEMS::TransferDataToWindow()
{
m_LayerSelectionCtrl->SetLayerSelection( m_params.layer );
m_outset.SetValue( m_params.outsetDistance );
m_roundCorners->SetValue( m_params.roundCorners );
m_lineWidth.SetValue( m_params.lineWidth );
m_roundToGrid->SetValue( m_params.gridRounding.has_value() );
m_roundingGrid.SetValue( m_params.gridRounding.value_or( s_gridRoundValuePersist ) );
m_copyLayers->SetValue( m_params.useSourceLayers );
m_copyWidths->SetValue( m_params.useSourceWidths );
m_gridRoundingEntry->Enable( m_roundToGrid->IsChecked() );
m_LayerSelectionCtrl->Enable( !m_copyLayers->GetValue() );
m_deleteSourceItems->SetValue( m_params.deleteSourceItems );
return true;
}
bool DIALOG_OUTSET_ITEMS::TransferDataFromWindow()
{
m_params.layer = ToLAYER_ID( m_LayerSelectionCtrl->GetLayerSelection() );
m_params.outsetDistance = m_outset.GetValue();
m_params.roundCorners = m_roundCorners->GetValue();
m_params.lineWidth = m_lineWidth.GetValue();
m_params.useSourceLayers = m_copyLayers->GetValue();
m_params.useSourceWidths = m_copyWidths->GetValue();
if( m_roundToGrid->IsChecked() )
m_params.gridRounding = m_roundingGrid.GetValue();
else
m_params.gridRounding = std::nullopt;
s_gridRoundValuePersist = m_roundingGrid.GetValue();
m_params.deleteSourceItems = m_deleteSourceItems->GetValue();
// Keep the recent values list up to date
const auto saveRecentValue = []( std::vector<int>& aRecentValues, int aValue )
{
const auto it = std::find( aRecentValues.begin(), aRecentValues.end(), aValue );
// Already have it
if( it != aRecentValues.end() )
return;
aRecentValues.push_back( aValue );
};
saveRecentValue( s_outsetRecentValues, m_params.outsetDistance );
saveRecentValue( s_lineWidthRecentValues, m_params.lineWidth );
if( m_params.gridRounding )
saveRecentValue( s_gridRoundingRecentValues, m_params.gridRounding.value() );
return true;
}

View File

@ -0,0 +1,59 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#include <pcb_base_frame.h>
#include <dialogs/dialog_outset_items_base.h>
#include <widgets/text_ctrl_eval.h>
#include <widgets/unit_binder.h>
#include <tools/item_modification_routine.h>
/**
* DIALOG_OUTSET_ITEMS, derived from DIALOG_OUTSET_ITEMS_BASE,
* created by wxFormBuilder
*/
class DIALOG_OUTSET_ITEMS : public DIALOG_OUTSET_ITEMS_BASE
{
public:
DIALOG_OUTSET_ITEMS( PCB_BASE_FRAME& aParent, OUTSET_ROUTINE::PARAMETERS& aParams );
~DIALOG_OUTSET_ITEMS();
protected:
bool TransferDataToWindow() override;
bool TransferDataFromWindow() override;
void OnLayerDefaultClick( wxCommandEvent& event ) override;
void OnCopyLayersChecked( wxCommandEvent& event ) override;
void OnRoundToGridChecked( wxCommandEvent& event ) override;
private:
PCB_BASE_FRAME& m_parent;
OUTSET_ROUTINE::PARAMETERS& m_params;
UNIT_BINDER m_outset;
UNIT_BINDER m_lineWidth;
UNIT_BINDER m_roundingGrid;
};

View File

@ -0,0 +1,142 @@
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
///////////////////////////////////////////////////////////////////////////
#include "pcb_layer_box_selector.h"
#include "dialog_outset_items_base.h"
///////////////////////////////////////////////////////////////////////////
DIALOG_OUTSET_ITEMS_BASE::DIALOG_OUTSET_ITEMS_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( wxSize( -1,-1 ), wxDefaultSize );
wxBoxSizer* bMainSizer;
bMainSizer = new wxBoxSizer( wxVERTICAL );
wxGridBagSizer* gbSizer1;
gbSizer1 = new wxGridBagSizer( 0, 0 );
gbSizer1->SetFlexibleDirection( wxBOTH );
gbSizer1->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_ALL );
m_outsetLabel = new wxStaticText( this, wxID_ANY, _("Outset:"), wxDefaultPosition, wxSize( -1,-1 ), 0 );
m_outsetLabel->Wrap( -1 );
gbSizer1->Add( m_outsetLabel, wxGBPosition( 0, 0 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL, 5 );
m_outsetEntry = new wxComboBox( this, wxID_ANY, _("0.1"), wxDefaultPosition, wxDefaultSize, 0, NULL, 0 );
gbSizer1->Add( m_outsetEntry, wxGBPosition( 0, 1 ), wxGBSpan( 1, 2 ), wxALL|wxEXPAND, 5 );
m_outsetUnit = new wxStaticText( this, wxID_ANY, _("mm"), wxDefaultPosition, wxDefaultSize, 0 );
m_outsetUnit->Wrap( -1 );
gbSizer1->Add( m_outsetUnit, wxGBPosition( 0, 3 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxALL, 5 );
m_roundToGrid = new wxCheckBox( this, wxID_ANY, _("Round outwards to grid multiples (when possible)"), wxDefaultPosition, wxDefaultSize, 0 );
m_roundToGrid->SetValue(true);
m_roundToGrid->SetToolTip( _("This is only possible for rectangular outsets.") );
gbSizer1->Add( m_roundToGrid, wxGBPosition( 2, 0 ), wxGBSpan( 1, 4 ), wxALL|wxEXPAND, 5 );
m_roundCorners = new wxCheckBox( this, wxID_ANY, _("Round corners (when possible)"), wxDefaultPosition, wxDefaultSize, 0 );
gbSizer1->Add( m_roundCorners, wxGBPosition( 1, 0 ), wxGBSpan( 1, 4 ), wxALL|wxEXPAND, 5 );
m_gridRoundingLabel = new wxStaticText( this, wxID_ANY, _("Grid size:"), wxDefaultPosition, wxSize( -1,-1 ), 0 );
m_gridRoundingLabel->Wrap( -1 );
gbSizer1->Add( m_gridRoundingLabel, wxGBPosition( 3, 0 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL, 5 );
m_gridRoundingEntry = new wxComboBox( this, wxID_ANY, _("0.01"), wxDefaultPosition, wxDefaultSize, 0, NULL, 0 );
gbSizer1->Add( m_gridRoundingEntry, wxGBPosition( 3, 1 ), wxGBSpan( 1, 2 ), wxALL|wxEXPAND, 5 );
m_gridRoundingUnit = new wxStaticText( this, wxID_ANY, _("mm"), wxDefaultPosition, wxDefaultSize, 0 );
m_gridRoundingUnit->Wrap( -1 );
gbSizer1->Add( m_gridRoundingUnit, wxGBPosition( 3, 3 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxALL, 5 );
m_staticline2 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
gbSizer1->Add( m_staticline2, wxGBPosition( 4, 0 ), wxGBSpan( 1, 4 ), wxALL|wxEXPAND, 5 );
m_copyLayers = new wxCheckBox( this, wxID_ANY, _("Copy item layers"), wxDefaultPosition, wxDefaultSize, 0 );
gbSizer1->Add( m_copyLayers, wxGBPosition( 5, 0 ), wxGBSpan( 1, 4 ), wxALL|wxEXPAND, 5 );
m_layerLabel = new wxStaticText( this, wxID_ANY, _("Layer:"), wxDefaultPosition, wxSize( -1,-1 ), 0 );
m_layerLabel->Wrap( -1 );
gbSizer1->Add( m_layerLabel, wxGBPosition( 6, 0 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL, 5 );
m_LayerSelectionCtrl = new PCB_LAYER_BOX_SELECTOR( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, 0 );
gbSizer1->Add( m_LayerSelectionCtrl, wxGBPosition( 6, 1 ), wxGBSpan( 1, 3 ), wxALL|wxEXPAND, 5 );
m_copyWidths = new wxCheckBox( this, wxID_ANY, _("Copy item widths (if possible)"), wxDefaultPosition, wxDefaultSize, 0 );
m_copyWidths->SetToolTip( _("This is not possible for items like pads, which will still use the value below.") );
gbSizer1->Add( m_copyWidths, wxGBPosition( 7, 0 ), wxGBSpan( 1, 4 ), wxALL, 5 );
m_lineWidthLabel = new wxStaticText( this, wxID_ANY, _("Line width:"), wxDefaultPosition, wxSize( -1,-1 ), 0 );
m_lineWidthLabel->Wrap( -1 );
gbSizer1->Add( m_lineWidthLabel, wxGBPosition( 8, 0 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL, 5 );
m_lineWidthEntry = new wxComboBox( this, wxID_ANY, _("0.1"), wxDefaultPosition, wxDefaultSize, 0, NULL, 0 );
gbSizer1->Add( m_lineWidthEntry, wxGBPosition( 8, 1 ), wxGBSpan( 1, 1 ), wxALL|wxEXPAND, 5 );
m_lineWidthUnit = new wxStaticText( this, wxID_ANY, _("mm"), wxDefaultPosition, wxDefaultSize, 0 );
m_lineWidthUnit->Wrap( -1 );
gbSizer1->Add( m_lineWidthUnit, wxGBPosition( 8, 2 ), wxGBSpan( 1, 1 ), wxALIGN_CENTER_VERTICAL|wxALL, 5 );
m_layerDefaultBtn = new wxButton( this, wxID_ANY, _("Layer default"), wxDefaultPosition, wxDefaultSize, 0 );
gbSizer1->Add( m_layerDefaultBtn, wxGBPosition( 8, 3 ), wxGBSpan( 1, 1 ), wxALL, 5 );
m_staticline21 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
gbSizer1->Add( m_staticline21, wxGBPosition( 9, 0 ), wxGBSpan( 1, 4 ), wxEXPAND | wxALL, 5 );
m_deleteSourceItems = new wxCheckBox( this, wxID_ANY, _("Delete source items after outset"), wxDefaultPosition, wxDefaultSize, 0 );
m_deleteSourceItems->SetToolTip( _("This is not possible for items like pads, which will still use the value below.") );
gbSizer1->Add( m_deleteSourceItems, wxGBPosition( 10, 0 ), wxGBSpan( 1, 4 ), wxALL, 5 );
gbSizer1->AddGrowableCol( 1 );
bMainSizer->Add( gbSizer1, 1, wxEXPAND, 5 );
wxBoxSizer* bSizerBottom;
bSizerBottom = new wxBoxSizer( wxHORIZONTAL );
bSizerBottom->Add( 40, 0, 1, wxEXPAND, 5 );
m_stdButtons = new wxStdDialogButtonSizer();
m_stdButtonsOK = new wxButton( this, wxID_OK );
m_stdButtons->AddButton( m_stdButtonsOK );
m_stdButtonsCancel = new wxButton( this, wxID_CANCEL );
m_stdButtons->AddButton( m_stdButtonsCancel );
m_stdButtons->Realize();
bSizerBottom->Add( m_stdButtons, 0, wxBOTTOM|wxTOP, 5 );
bMainSizer->Add( bSizerBottom, 0, wxEXPAND|wxTOP, 5 );
this->SetSizer( bMainSizer );
this->Layout();
bMainSizer->Fit( this );
// Connect Events
this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnClose ) );
m_roundToGrid->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnRoundToGridChecked ), NULL, this );
m_copyLayers->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnCopyLayersChecked ), NULL, this );
m_layerDefaultBtn->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnLayerDefaultClick ), NULL, this );
m_stdButtonsOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnOkClick ), NULL, this );
}
DIALOG_OUTSET_ITEMS_BASE::~DIALOG_OUTSET_ITEMS_BASE()
{
// Disconnect Events
this->Disconnect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnClose ) );
m_roundToGrid->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnRoundToGridChecked ), NULL, this );
m_copyLayers->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnCopyLayersChecked ), NULL, this );
m_layerDefaultBtn->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnLayerDefaultClick ), NULL, this );
m_stdButtonsOK->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_OUTSET_ITEMS_BASE::OnOkClick ), NULL, this );
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
///////////////////////////////////////////////////////////////////////////
#pragma once
#include <wx/artprov.h>
#include <wx/xrc/xmlres.h>
#include <wx/intl.h>
class PCB_LAYER_BOX_SELECTOR;
#include "dialog_shim.h"
#include <wx/string.h>
#include <wx/stattext.h>
#include <wx/gdicmn.h>
#include <wx/font.h>
#include <wx/colour.h>
#include <wx/settings.h>
#include <wx/combobox.h>
#include <wx/checkbox.h>
#include <wx/statline.h>
#include <wx/bmpcbox.h>
#include <wx/button.h>
#include <wx/bitmap.h>
#include <wx/image.h>
#include <wx/icon.h>
#include <wx/gbsizer.h>
#include <wx/sizer.h>
#include <wx/dialog.h>
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
/// Class DIALOG_OUTSET_ITEMS_BASE
///////////////////////////////////////////////////////////////////////////////
class DIALOG_OUTSET_ITEMS_BASE : public DIALOG_SHIM
{
private:
protected:
wxStaticText* m_outsetLabel;
wxComboBox* m_outsetEntry;
wxStaticText* m_outsetUnit;
wxCheckBox* m_roundToGrid;
wxCheckBox* m_roundCorners;
wxStaticText* m_gridRoundingLabel;
wxComboBox* m_gridRoundingEntry;
wxStaticText* m_gridRoundingUnit;
wxStaticLine* m_staticline2;
wxCheckBox* m_copyLayers;
wxStaticText* m_layerLabel;
PCB_LAYER_BOX_SELECTOR* m_LayerSelectionCtrl;
wxCheckBox* m_copyWidths;
wxStaticText* m_lineWidthLabel;
wxComboBox* m_lineWidthEntry;
wxStaticText* m_lineWidthUnit;
wxButton* m_layerDefaultBtn;
wxStaticLine* m_staticline21;
wxCheckBox* m_deleteSourceItems;
wxStdDialogButtonSizer* m_stdButtons;
wxButton* m_stdButtonsOK;
wxButton* m_stdButtonsCancel;
// Virtual event handlers, override them in your derived class
virtual void OnClose( wxCloseEvent& event ) { event.Skip(); }
virtual void OnRoundToGridChecked( wxCommandEvent& event ) { event.Skip(); }
virtual void OnCopyLayersChecked( wxCommandEvent& event ) { event.Skip(); }
virtual void OnLayerDefaultClick( wxCommandEvent& event ) { event.Skip(); }
virtual void OnOkClick( wxCommandEvent& event ) { event.Skip(); }
public:
DIALOG_OUTSET_ITEMS_BASE( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Outset Items"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1,-1 ), long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER );
~DIALOG_OUTSET_ITEMS_BASE();
};

View File

@ -35,6 +35,7 @@
#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>
@ -231,7 +232,7 @@ private:
CONVERT_TOOL::CONVERT_TOOL() :
TOOL_INTERACTIVE( "pcbnew.Convert" ),
PCB_TOOL_BASE( "pcbnew.Convert" ),
m_selectionTool( nullptr ),
m_menu( nullptr ),
m_frame( nullptr )
@ -278,6 +279,7 @@ bool CONVERT_TOOL::Init()
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 );
@ -289,6 +291,8 @@ bool CONVERT_TOOL::Init()
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;
@ -298,7 +302,8 @@ bool CONVERT_TOOL::Init()
|| canCreateLines
|| canCreateTracks
|| canCreateArcs
|| canCreateArray;
|| canCreateArray
|| canCreateOutset;
m_menu->AddItem( PCB_ACTIONS::convertToPoly, canCreatePoly );
@ -307,7 +312,8 @@ bool CONVERT_TOOL::Init()
m_menu->AddItem( PCB_ACTIONS::convertToKeepout, canCreatePoly );
m_menu->AddItem( PCB_ACTIONS::convertToLines, canCreateLines );
m_menu->AppendSeparator();
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
@ -316,7 +322,7 @@ bool CONVERT_TOOL::Init()
m_menu->AddItem( PCB_ACTIONS::convertToArc, canCreateArcs );
m_menu->AppendSeparator();
m_menu->AddSeparator();
m_menu->AddItem( PCB_ACTIONS::createArray, canCreateArray );
CONDITIONAL_MENU& selToolMenu = m_selectionTool->GetToolMenu().GetMenu();
@ -1287,12 +1293,139 @@ std::optional<SEG> CONVERT_TOOL::getStartEndPoints( EDA_ITEM* aItem )
}
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 )
{
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
}

View File

@ -26,7 +26,7 @@
#define CONVERT_TOOL_H_
#include <geometry/shape_poly_set.h>
#include <tool/tool_interactive.h>
#include <tools/pcb_tool_base.h>
#include <pcbnew_settings.h>
class CONDITIONAL_MENU;
@ -34,7 +34,7 @@ class PCB_SELECTION_TOOL;
class PCB_BASE_FRAME;
class CONVERT_TOOL : public TOOL_INTERACTIVE
class CONVERT_TOOL : public PCB_TOOL_BASE
{
public:
CONVERT_TOOL();
@ -61,6 +61,11 @@ public:
*/
int SegmentToArc( const TOOL_EVENT& aEvent );
/**
* Convert selected items to outset versions of themselves.
*/
int OutsetItems( const TOOL_EVENT& aEvent );
///< @copydoc TOOL_INTERACTIVE::setTransitions()
void setTransitions() override;

View File

@ -1354,16 +1354,16 @@ int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
// List of thing to select at the end of the operation
// (doing it as we go will invalidate the iterator)
std::vector<PCB_SHAPE*> items_to_select_on_success;
std::vector<BOARD_ITEM*> items_to_select_on_success;
// And same for items to deselect
std::vector<PCB_SHAPE*> items_to_deselect_on_success;
std::vector<BOARD_ITEM*> items_to_deselect_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 =
[&]( PCB_SHAPE& aItem )
[&]( BOARD_ITEM& aItem )
{
// If the item was "conjured up" it will be added later separately
if( !alg::contains( lines_to_add, &aItem ) )
@ -1375,7 +1375,7 @@ int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
bool any_items_created = !lines_to_add.empty();
auto item_creation_handler =
[&]( std::unique_ptr<PCB_SHAPE> aItem )
[&]( std::unique_ptr<BOARD_ITEM> aItem )
{
any_items_created = true;
items_to_select_on_success.push_back( aItem.get() );
@ -1384,7 +1384,7 @@ int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
bool any_items_removed = !items_to_remove.empty();
auto item_removal_handler =
[&]( PCB_SHAPE& aItem )
[&]( BOARD_ITEM& aItem )
{
aItem.SetFlags( STRUCT_DELETED );
any_items_removed = true;
@ -1462,11 +1462,11 @@ int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
} );
// Select added and modified items
for( PCB_SHAPE* item : items_to_select_on_success )
for( BOARD_ITEM* item : items_to_select_on_success )
m_selectionTool->AddItemToSel( item, true );
// Deselect removed items
for( PCB_SHAPE* item : items_to_deselect_on_success )
for( BOARD_ITEM* item : items_to_deselect_on_success )
m_selectionTool->RemoveItemFromSel( item, true );
if( any_items_removed )
@ -1677,22 +1677,22 @@ int EDIT_TOOL::BooleanPolygons( const TOOL_EVENT& aEvent )
// Handle modifications to existing items by the routine
auto item_modification_handler =
[&]( PCB_SHAPE& aItem )
[&]( BOARD_ITEM& aItem )
{
commit.Modify( &aItem );
};
std::vector<PCB_SHAPE*> items_to_select_on_success;
std::vector<BOARD_ITEM*> items_to_select_on_success;
auto item_creation_handler =
[&]( std::unique_ptr<PCB_SHAPE> aItem )
[&]( std::unique_ptr<BOARD_ITEM> aItem )
{
items_to_select_on_success.push_back( aItem.get() );
commit.Add( aItem.release() );
};
auto item_removal_handler =
[&]( PCB_SHAPE& aItem )
[&]( BOARD_ITEM& aItem )
{
commit.Remove( &aItem );
};
@ -1729,7 +1729,7 @@ int EDIT_TOOL::BooleanPolygons( const TOOL_EVENT& aEvent )
boolean_routine->ProcessShape( *shape );
// Select new items
for( PCB_SHAPE* item : items_to_select_on_success )
for( BOARD_ITEM* item : items_to_select_on_success )
m_selectionTool->AddItemToSel( item, true );
// Notify other tools of the changes
@ -3254,9 +3254,11 @@ void EDIT_TOOL::setTransitions()
Go( &EDIT_TOOL::SimplifyPolygons, PCB_ACTIONS::simplifyPolygons.MakeEvent() );
Go( &EDIT_TOOL::HealShapes, PCB_ACTIONS::healShapes.MakeEvent() );
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::extendLines.MakeEvent() );
Go( &EDIT_TOOL::BooleanPolygons, PCB_ACTIONS::mergePolygons.MakeEvent() );
Go( &EDIT_TOOL::BooleanPolygons, PCB_ACTIONS::subtractPolygons.MakeEvent() );
Go( &EDIT_TOOL::BooleanPolygons, PCB_ACTIONS::intersectPolygons.MakeEvent() );
Go( &EDIT_TOOL::JustifyText, ACTIONS::leftJustify.MakeEvent() );
Go( &EDIT_TOOL::JustifyText, ACTIONS::centerJustify.MakeEvent() );
Go( &EDIT_TOOL::JustifyText, ACTIONS::rightJustify.MakeEvent() );

View File

@ -142,6 +142,11 @@ public:
*/
int SimplifyPolygons( const TOOL_EVENT& aEvent );
/**
* Create outset items from selection
*/
int OutsetItems( const TOOL_EVENT& aEvent );
/**
* Modify selected polygons into a single polygon using boolean operations
* such as merge (union) or subtract (difference)

View File

@ -25,6 +25,14 @@
#include <geometry/geometry_utils.h>
#include <geometry/circle.h>
#include <geometry/oval.h>
#include <geometry/roundrect.h>
#include <geometry/shape_rect.h>
#include <geometry/vector_utils.h>
#include <pad.h>
#include <pcb_track.h>
#include <tools/pcb_tool_utils.h>
namespace
{
@ -604,3 +612,357 @@ bool POLYGON_INTERSECT_ROUTINE::ProcessSubsequentPolygon( const SHAPE_POLY_SET&
GetWorkingPolygon()->SetPolyShape( working_copy );
return true;
}
wxString OUTSET_ROUTINE::GetCommitDescription() const
{
return _( "Outset items." );
}
std::optional<wxString> OUTSET_ROUTINE::GetStatusMessage() const
{
if( GetSuccesses() == 0 )
{
return _( "Unable to outset the selected items." );
}
else if( GetFailures() > 0 )
{
return _( "Some of the items could not be outset." );
}
return std::nullopt;
}
static SHAPE_RECT GetRectRoundedToGridOutwards( const SHAPE_RECT& aRect, int aGridSize )
{
const VECTOR2I newPos = KIGEOM::RoundNW( aRect.GetPosition(), aGridSize );
const VECTOR2I newOpposite =
KIGEOM::RoundSE( aRect.GetPosition() + aRect.GetSize(), aGridSize );
return SHAPE_RECT( newPos, newOpposite );
}
void OUTSET_ROUTINE::ProcessItem( BOARD_ITEM& aItem )
{
/*
* This attempts to do exact outsetting, rather than punting to Clipper.
* So it can't do all shapes, but it can do the most obvious ones, which are probably
* the ones you want to outset anyway, most usually when making a courtyard for a footprint.
*/
PCB_LAYER_ID layer = m_params.useSourceLayers ? aItem.GetLayer() : m_params.layer;
// Not all items have a width, even if the parameters want to copy it
// So fall back to the given width if we can't get one.
int width = m_params.lineWidth;
if( m_params.useSourceWidths )
{
std::optional<int> item_width = GetBoardItemWidth( aItem );
if( item_width.has_value() )
{
width = *item_width;
}
}
CHANGE_HANDLER& handler = GetHandler();
const auto addPolygonalChain = [&]( const SHAPE_LINE_CHAIN& aChain )
{
SHAPE_POLY_SET new_poly( aChain );
std::unique_ptr<PCB_SHAPE> new_shape =
std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::POLY );
new_shape->SetPolyShape( new_poly );
new_shape->SetLayer( layer );
new_shape->SetWidth( width );
handler.AddNewItem( std::move( new_shape ) );
};
// Iterate the SHAPE_LINE_CHAIN in the polygon, pulling out
// segments and arcs to create new PCB_SHAPE primitives.
const auto addChain = [&]( const SHAPE_LINE_CHAIN& aChain )
{
// Prefer to add a polygonal chain if there are no arcs
// as this permits boolean ops
if( aChain.ArcCount() == 0 )
{
addPolygonalChain( aChain );
return;
}
for( size_t si = 0; si < aChain.GetSegmentCount(); ++si )
{
const SEG seg = aChain.GetSegment( si );
if( seg.Length() == 0 )
continue;
if( aChain.IsArcSegment( si ) )
continue;
std::unique_ptr<PCB_SHAPE> new_shape =
std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::SEGMENT );
new_shape->SetStart( seg.A );
new_shape->SetEnd( seg.B );
new_shape->SetLayer( layer );
new_shape->SetWidth( width );
handler.AddNewItem( std::move( new_shape ) );
}
for( size_t ai = 0; ai < aChain.ArcCount(); ++ai )
{
const SHAPE_ARC& arc = aChain.Arc( ai );
if( arc.GetRadius() == 0 || arc.GetP0() == arc.GetP1() )
continue;
std::unique_ptr<PCB_SHAPE> new_shape =
std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::ARC );
new_shape->SetArcGeometry( arc.GetP0(), arc.GetArcMid(), arc.GetP1() );
new_shape->SetLayer( layer );
new_shape->SetWidth( width );
handler.AddNewItem( std::move( new_shape ) );
}
};
const auto addPoly = [&]( const SHAPE_POLY_SET& aPoly )
{
for( int oi = 0; oi < aPoly.OutlineCount(); ++oi )
{
addChain( aPoly.Outline( oi ) );
}
};
const auto addRect = [&]( const SHAPE_RECT& aRect )
{
std::unique_ptr<PCB_SHAPE> new_shape =
std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::RECTANGLE );
if( !m_params.gridRounding.has_value() )
{
new_shape->SetPosition( aRect.GetPosition() );
new_shape->SetRectangleWidth( aRect.GetWidth() );
new_shape->SetRectangleHeight( aRect.GetHeight() );
}
else
{
const SHAPE_RECT grid_rect =
GetRectRoundedToGridOutwards( aRect, *m_params.gridRounding );
new_shape->SetPosition( grid_rect.GetPosition() );
new_shape->SetRectangleWidth( grid_rect.GetWidth() );
new_shape->SetRectangleHeight( grid_rect.GetHeight() );
}
new_shape->SetLayer( layer );
new_shape->SetWidth( width );
handler.AddNewItem( std::move( new_shape ) );
};
const auto addCircle = [&]( const CIRCLE& aCircle )
{
std::unique_ptr<PCB_SHAPE> new_shape =
std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::CIRCLE );
new_shape->SetCenter( aCircle.Center );
new_shape->SetRadius( aCircle.Radius );
new_shape->SetLayer( layer );
new_shape->SetWidth( width );
handler.AddNewItem( std::move( new_shape ) );
};
const auto addCircleOrRect = [&]( const CIRCLE& aCircle )
{
if( m_params.roundCorners )
{
addCircle( aCircle );
}
else
{
const VECTOR2I rVec{ aCircle.Radius, aCircle.Radius };
const SHAPE_RECT rect{ aCircle.Center - rVec, aCircle.Center + rVec };
addRect( rect );
}
};
switch( aItem.Type() )
{
case PCB_PAD_T:
{
const PAD& pad = static_cast<const PAD&>( aItem );
const PAD_SHAPE pad_shape = pad.GetShape();
switch( pad_shape )
{
case PAD_SHAPE::RECTANGLE:
case PAD_SHAPE::ROUNDRECT:
case PAD_SHAPE::OVAL:
{
const VECTOR2I pad_size = pad.GetSize();
BOX2I box{ pad.GetPosition() - pad_size / 2, pad_size };
box.Inflate( m_params.outsetDistance );
int radius = m_params.outsetDistance;
if( pad_shape == PAD_SHAPE::ROUNDRECT )
{
radius += pad.GetRoundRectCornerRadius();
}
else if( pad_shape == PAD_SHAPE::OVAL )
{
radius += std::min( pad_size.x, pad_size.y ) / 2;
}
radius = m_params.roundCorners ? radius : 0;
// No point doing a SHAPE_RECT as we may need to rotate it
ROUNDRECT rrect( box, radius );
SHAPE_POLY_SET poly;
rrect.TransformToPolygon( poly, 0, ERROR_LOC::ERROR_OUTSIDE );
poly.Rotate( pad.GetOrientation(), pad.GetPosition() );
addPoly( poly );
AddSuccess();
break;
}
case PAD_SHAPE::CIRCLE:
{
const int radius = pad.GetSize().x / 2 + m_params.outsetDistance;
const CIRCLE circle( pad.GetPosition(), radius );
addCircleOrRect( circle );
AddSuccess();
break;
}
case PAD_SHAPE::TRAPEZOID:
{
// Not handled yet, but could use a generic convex polygon outset method.
break;
}
default:
// Other pad shapes are not supported with exact outsets
break;
}
break;
}
case PCB_SHAPE_T:
{
const PCB_SHAPE& pcb_shape = static_cast<const PCB_SHAPE&>( aItem );
switch( pcb_shape.GetShape() )
{
case SHAPE_T::RECTANGLE:
{
BOX2I box{ pcb_shape.GetPosition(),
VECTOR2I{ pcb_shape.GetRectangleWidth(), pcb_shape.GetRectangleHeight() } };
box.Inflate( m_params.outsetDistance );
SHAPE_RECT rect( box );
if( m_params.roundCorners )
{
ROUNDRECT rrect( rect, m_params.outsetDistance );
SHAPE_POLY_SET poly;
rrect.TransformToPolygon( poly, 0, ERROR_LOC::ERROR_OUTSIDE );
addPoly( poly );
}
else
{
addRect( rect );
}
AddSuccess();
break;
}
case SHAPE_T::CIRCLE:
{
const CIRCLE circle( pcb_shape.GetCenter(),
pcb_shape.GetRadius() + m_params.outsetDistance );
addCircleOrRect( circle );
AddSuccess();
break;
}
case SHAPE_T::SEGMENT:
{
// For now just make the whole stadium shape and let the user delete the unwanted bits
const SEG seg( pcb_shape.GetStart(), pcb_shape.GetEnd() );
if( m_params.roundCorners )
{
const OVAL oval( seg, m_params.outsetDistance * 2 );
addChain( KIGEOM::ConvertToChain( oval ) );
}
else
{
SHAPE_LINE_CHAIN chain;
const VECTOR2I ext = ( seg.B - seg.A ).Resize( m_params.outsetDistance );
const VECTOR2I perp = GetRotated( ext, ANGLE_90 );
chain.Append( seg.A - ext + perp );
chain.Append( seg.A - ext - perp );
chain.Append( seg.B + ext - perp );
chain.Append( seg.B + ext + perp );
chain.SetClosed( true );
addChain( chain );
}
AddSuccess();
break;
}
case SHAPE_T::ARC:
{
// Not 100% sure what a sensible non-round outset of an arc is!
// (not sure it's that important in practice)
// Gets rather complicated if this isn't true
if( pcb_shape.GetRadius() >= m_params.outsetDistance )
{
// Again, include the endcaps and let the user delete the unwanted bits
const SHAPE_ARC arc{ pcb_shape.GetCenter(), pcb_shape.GetStart(),
pcb_shape.GetArcAngle(), 0 };
const VECTOR2I startNorm =
VECTOR2I( arc.GetP0() - arc.GetCenter() ).Resize( m_params.outsetDistance );
const SHAPE_ARC inner{ arc.GetCenter(), arc.GetP0() - startNorm,
arc.GetCentralAngle(), 0 };
const SHAPE_ARC outer{ arc.GetCenter(), arc.GetP0() + startNorm,
arc.GetCentralAngle(), 0 };
SHAPE_LINE_CHAIN chain;
chain.Append( outer );
// End cap at the P1 end
chain.Append( SHAPE_ARC{ arc.GetP1(), outer.GetP1(), ANGLE_180 } );
if( inner.GetRadius() > 0 )
{
chain.Append( inner.Reversed() );
}
// End cap at the P0 end back to the start
chain.Append( SHAPE_ARC{ arc.GetP0(), inner.GetP0(), ANGLE_180 } );
addChain( chain );
AddSuccess();
break;
}
}
default:
// Other shapes are not supported with exact outsets
// (convex) POLY shouldn't be too traumatic and it would bring trapezoids for free.
break;
}
break;
}
default:
// Other item types are not supported with exact outsets
break;
}
if( m_params.deleteSourceItems )
{
handler.DeleteItem( aItem );
}
}

View File

@ -69,21 +69,21 @@ public:
*
* @param aItem the new item
*/
virtual void AddNewItem( std::unique_ptr<PCB_SHAPE> aItem ) = 0;
virtual void AddNewItem( std::unique_ptr<BOARD_ITEM> aItem ) = 0;
/**
* @brief Report that the tool has modified an item on the board
*
* @param aItem the modified item
*/
virtual void MarkItemModified( PCB_SHAPE& aItem ) = 0;
virtual void MarkItemModified( BOARD_ITEM& aItem ) = 0;
/**
* @brief Report that the tool has deleted an item on the board
*
* @param aItem the deleted item
*/
virtual void DeleteItem( PCB_SHAPE& aItem ) = 0;
virtual void DeleteItem( BOARD_ITEM& aItem ) = 0;
};
/**
@ -96,23 +96,23 @@ public:
/**
* Handler for creating a new item on the board
*
* @param PCB_SHAPE& the shape to add
* @param BOARD_ITEM& the item to add
*/
using CREATION_HANDLER = std::function<void( std::unique_ptr<PCB_SHAPE> )>;
using CREATION_HANDLER = std::function<void( std::unique_ptr<BOARD_ITEM> )>;
/**
* Handler for modifying or deleting an existing item on the board
*
* @param PCB_SHAPE& the shape to modify
* @param BOARD_ITEM& the item to modify
*/
using MODIFICATION_HANDLER = std::function<void( PCB_SHAPE& )>;
using MODIFICATION_HANDLER = std::function<void( BOARD_ITEM& )>;
/**
* Handler for modifying or deleting an existing item on the board
*
* @param PCB_SHAPE& the shape to delete
* @param BOARD_ITEM& the item to delete
*/
using DELETION_HANDLER = std::function<void( PCB_SHAPE& )>;
using DELETION_HANDLER = std::function<void( BOARD_ITEM& )>;
CALLABLE_BASED_HANDLER( CREATION_HANDLER aCreationHandler,
MODIFICATION_HANDLER aModificationHandler,
@ -128,7 +128,7 @@ public:
*
* @param aItem the new item
*/
void AddNewItem( std::unique_ptr<PCB_SHAPE> aItem ) override
void AddNewItem( std::unique_ptr<BOARD_ITEM> aItem ) override
{
m_creationHandler( std::move( aItem ) );
}
@ -138,14 +138,14 @@ public:
*
* @param aItem the modified item
*/
void MarkItemModified( PCB_SHAPE& aItem ) override { m_modificationHandler( aItem ); }
void MarkItemModified( BOARD_ITEM& aItem ) override { m_modificationHandler( aItem ); }
/**
* @brief Report that the tool has deleted an item on the board
*
* @param aItem the deleted item
*/
void DeleteItem( PCB_SHAPE& aItem ) override { m_deletionHandler( aItem ); }
void DeleteItem( BOARD_ITEM& aItem ) override { m_deletionHandler( aItem ); }
CREATION_HANDLER m_creationHandler;
MODIFICATION_HANDLER m_modificationHandler;
@ -393,4 +393,35 @@ private:
bool ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon ) override;
};
class OUTSET_ROUTINE : public ITEM_MODIFICATION_ROUTINE
{
public:
struct PARAMETERS
{
int outsetDistance;
bool roundCorners;
bool useSourceLayers;
bool useSourceWidths;
PCB_LAYER_ID layer;
int lineWidth;
std::optional<int> gridRounding;
bool deleteSourceItems;
};
OUTSET_ROUTINE( BOARD_ITEM* aBoard, CHANGE_HANDLER& aHandler, const PARAMETERS& aParams ) :
ITEM_MODIFICATION_ROUTINE( aBoard, aHandler ), m_params( aParams )
{
}
wxString GetCommitDescription() const override;
std::optional<wxString> GetStatusMessage() const override;
void ProcessItem( BOARD_ITEM& aItem );
private:
const PARAMETERS m_params;
};
#endif /* ITEM_MODIFICATION_ROUTINE_H_ */

View File

@ -89,6 +89,12 @@ TOOL_ACTION PCB_ACTIONS::convertToTracks( TOOL_ACTION_ARGS()
.Tooltip( _( "Creates tracks from the selected graphic lines" ) )
.Icon( BITMAPS::add_tracks ) );
TOOL_ACTION PCB_ACTIONS::outsetItems( TOOL_ACTION_ARGS()
.Name( "pcbnew.Convert.outsetItems" )
.Scope( AS_GLOBAL )
.FriendlyName( _( "Create Outsets from Selection" ) )
.Tooltip( _( "Create outset lines from the selected item" ) ) );
// DRAWING_TOOL
//

View File

@ -166,6 +166,8 @@ public:
static TOOL_ACTION extendLines;
/// Simplify polygon outlines
static TOOL_ACTION simplifyPolygons;
/// Create outset items from selection
static TOOL_ACTION outsetItems;
/// Merge multiple polygons into a single polygon
static TOOL_ACTION mergePolygons;