ADDED: Polygon boolean operations in PCB editor

This allows common operations like merging a pin courtyard
into the body courtyard in the fooprint editor, taking a
"bite" out of a polygon and so on,

For now, this only supports polygons made of straight lines.

There are some wierd cases when the operations result in nothing
(e.g. wen a big polygon is substracted from a smaller one that
it contains entirely). I have tried to do something senisble in
these cases, but there may be more optimal ways to handle it.

Relates-To: https://gitlab.com/kicad/code/kicad/-/issues/13025
This commit is contained in:
John Beard 2023-07-22 01:23:59 +01:00
parent b6cd7e3b4a
commit 311f041421
21 changed files with 995 additions and 7 deletions

View File

@ -351,6 +351,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::import].emplace_back( BITMAPS::import, wxT( "import_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::info].emplace_back( BITMAPS::info, wxT( "info_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::insert_module_board].emplace_back( BITMAPS::insert_module_board, wxT( "insert_module_board_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::intersect_polygons].emplace_back( BITMAPS::intersect_polygons, wxT( "intersect_polygons_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::language].emplace_back( BITMAPS::language, wxT( "language_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::layers_manager].emplace_back( BITMAPS::layers_manager, wxT( "layers_manager_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::leave_sheet].emplace_back( BITMAPS::leave_sheet, wxT( "leave_sheet_24.png" ), 24, wxT( "light" ) );
@ -375,7 +376,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::marker_next].emplace_back( BITMAPS::marker_next, wxT( "marker_next_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::marker_previous].emplace_back( BITMAPS::marker_previous, wxT( "marker_previous_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::measurement].emplace_back( BITMAPS::measurement, wxT( "measurement_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::pcb_target].emplace_back( BITMAPS::pcb_target, wxT( "pcb_target_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::merge_polygons].emplace_back( BITMAPS::merge_polygons, wxT( "merge_polygons_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::mirror_h].emplace_back( BITMAPS::mirror_h, wxT( "mirror_h_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::mirror_v].emplace_back( BITMAPS::mirror_v, wxT( "mirror_v_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::mode_module].emplace_back( BITMAPS::mode_module, wxT( "mode_module_24.png" ), 24, wxT( "light" ) );
@ -429,6 +430,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::part_properties].emplace_back( BITMAPS::part_properties, wxT( "part_properties_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::paste].emplace_back( BITMAPS::paste, wxT( "paste_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::paste_special].emplace_back( BITMAPS::paste_special, wxT( "paste_special_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::pcb_target].emplace_back( BITMAPS::pcb_target, wxT( "pcb_target_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::pin2pin].emplace_back( BITMAPS::pin2pin, wxT( "pin2pin_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::pin_size_to].emplace_back( BITMAPS::pin_size_to, wxT( "pin_size_to_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::pin_show_etype].emplace_back( BITMAPS::pin_show_etype, wxT( "pin_show_etype_24.png" ), 24, wxT( "light" ) );
@ -520,6 +522,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::show_back_assembly_layers].emplace_back( BITMAPS::show_back_assembly_layers, wxT( "show_back_assembly_layers_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::special_tools].emplace_back( BITMAPS::special_tools, wxT( "special_tools_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::spreadsheet].emplace_back( BITMAPS::spreadsheet, wxT( "spreadsheet_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::subtract_polygons].emplace_back( BITMAPS::subtract_polygons, wxT( "subtract_polygons_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::swap].emplace_back( BITMAPS::swap, wxT( "swap_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::swap_layer].emplace_back( BITMAPS::swap_layer, wxT( "swap_layer_24.png" ), 24, wxT( "light" ) );
aBitmapInfoCache[BITMAPS::switch_corner_rounding_shape].emplace_back( BITMAPS::switch_corner_rounding_shape, wxT( "switch_corner_rounding_shape_24.png" ), 24, wxT( "light" ) );
@ -740,6 +743,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::import].emplace_back( BITMAPS::import, wxT( "import_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::info].emplace_back( BITMAPS::info, wxT( "info_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::insert_module_board].emplace_back( BITMAPS::insert_module_board, wxT( "insert_module_board_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::intersect_polygons].emplace_back( BITMAPS::intersect_polygons, wxT( "intersect_polygons_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::language].emplace_back( BITMAPS::language, wxT( "language_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::layers_manager].emplace_back( BITMAPS::layers_manager, wxT( "layers_manager_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::leave_sheet].emplace_back( BITMAPS::leave_sheet, wxT( "leave_sheet_dark_24.png" ), 24, wxT( "dark" ) );
@ -764,7 +768,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::marker_next].emplace_back( BITMAPS::marker_next, wxT( "marker_next_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::marker_previous].emplace_back( BITMAPS::marker_previous, wxT( "marker_previous_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::measurement].emplace_back( BITMAPS::measurement, wxT( "measurement_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::pcb_target].emplace_back( BITMAPS::pcb_target, wxT( "pcb_target_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::merge_polygons].emplace_back( BITMAPS::merge_polygons, wxT( "merge_polygons_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::mirror_h].emplace_back( BITMAPS::mirror_h, wxT( "mirror_h_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::mirror_v].emplace_back( BITMAPS::mirror_v, wxT( "mirror_v_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::mode_module].emplace_back( BITMAPS::mode_module, wxT( "mode_module_dark_24.png" ), 24, wxT( "dark" ) );
@ -818,6 +822,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::part_properties].emplace_back( BITMAPS::part_properties, wxT( "part_properties_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::paste].emplace_back( BITMAPS::paste, wxT( "paste_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::paste_special].emplace_back( BITMAPS::paste_special, wxT( "paste_special_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::pcb_target].emplace_back( BITMAPS::pcb_target, wxT( "pcb_target_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::pin2pin].emplace_back( BITMAPS::pin2pin, wxT( "pin2pin_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::pin_size_to].emplace_back( BITMAPS::pin_size_to, wxT( "pin_size_to_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::pin_show_etype].emplace_back( BITMAPS::pin_show_etype, wxT( "pin_show_etype_dark_24.png" ), 24, wxT( "dark" ) );
@ -907,6 +912,7 @@ void BuildBitmapInfo( std::unordered_map<BITMAPS, std::vector<BITMAP_INFO>>& aBi
aBitmapInfoCache[BITMAPS::show_back_assembly_layers].emplace_back( BITMAPS::show_back_assembly_layers, wxT( "show_back_assembly_layers_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::special_tools].emplace_back( BITMAPS::special_tools, wxT( "special_tools_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::spreadsheet].emplace_back( BITMAPS::spreadsheet, wxT( "spreadsheet_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::subtract_polygons].emplace_back( BITMAPS::subtract_polygons, wxT( "subtract_polygons_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::swap].emplace_back( BITMAPS::swap, wxT( "swap_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::swap_layer].emplace_back( BITMAPS::swap_layer, wxT( "swap_layer_dark_24.png" ), 24, wxT( "dark" ) );
aBitmapInfoCache[BITMAPS::switch_corner_rounding_shape].emplace_back( BITMAPS::switch_corner_rounding_shape, wxT( "switch_corner_rounding_shape_dark_24.png" ), 24, wxT( "dark" ) );

View File

@ -297,6 +297,7 @@ enum class BITMAPS : unsigned int
import_vector,
info,
insert_module_board,
intersect_polygons,
invisible_text,
kicad_icon_small,
label_align_left,
@ -328,6 +329,7 @@ enum class BITMAPS : unsigned int
marker_next,
marker_previous,
measurement,
merge_polygons,
microstrip,
microstrip_zodd_zeven,
minus,
@ -557,6 +559,7 @@ enum class BITMAPS : unsigned int
stroke_dashdotdot,
stroke_dot,
stroke_solid,
subtract_polygons,
swap,
swap_layer,
switch_corner_rounding_shape,

View File

@ -116,11 +116,14 @@ static std::shared_ptr<CONDITIONAL_MENU> makeShapeModificationMenu( TOOL_INTERAC
menu->SetTitle( _( "Shape Modification" ) );
static std::vector<KICAD_T> filletChamferTypes = { PCB_SHAPE_LOCATE_POLY_T,
PCB_SHAPE_LOCATE_RECT_T,
PCB_SHAPE_LOCATE_SEGMENT_T };
static const std::vector<KICAD_T> filletChamferTypes = { PCB_SHAPE_LOCATE_POLY_T,
PCB_SHAPE_LOCATE_RECT_T,
PCB_SHAPE_LOCATE_SEGMENT_T };
static std::vector<KICAD_T> lineExtendTypes = { PCB_SHAPE_LOCATE_SEGMENT_T };
static const std::vector<KICAD_T> lineExtendTypes = { PCB_SHAPE_LOCATE_SEGMENT_T };
static const std::vector<KICAD_T> polygonBooleanTypes = { PCB_SHAPE_LOCATE_RECT_T,
PCB_SHAPE_LOCATE_POLY_T };
auto hasCornerCondition =
[aTool]( const SELECTION& aSelection )
@ -145,6 +148,15 @@ static std::shared_ptr<CONDITIONAL_MENU> makeShapeModificationMenu( TOOL_INTERAC
&& SELECTION_CONDITIONS::Count( 2 ) );
menu->AddItem( PCB_ACTIONS::pointEditorMoveCorner, hasCornerCondition );
menu->AddItem( PCB_ACTIONS::pointEditorMoveMidpoint, hasMidpointCondition );
menu->AddSeparator();
menu->AddItem( PCB_ACTIONS::mergePolygons, SELECTION_CONDITIONS::OnlyTypes( polygonBooleanTypes )
&& SELECTION_CONDITIONS::MoreThan( 1 ) );
menu->AddItem( PCB_ACTIONS::subtractPolygons, SELECTION_CONDITIONS::OnlyTypes( polygonBooleanTypes )
&& SELECTION_CONDITIONS::MoreThan( 1 ) );
menu->AddItem( PCB_ACTIONS::intersectPolygons, SELECTION_CONDITIONS::OnlyTypes( polygonBooleanTypes )
&& SELECTION_CONDITIONS::MoreThan( 1 ) );
// clang-format on
return menu;
@ -1394,6 +1406,117 @@ int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
}
int EDIT_TOOL::BooleanPolygons( const TOOL_EVENT& aEvent )
{
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];
if( !item->IsType( {
PCB_SHAPE_LOCATE_POLY_T,
PCB_SHAPE_LOCATE_RECT_T,
} ) )
{
aCollector.Remove( item );
}
}
},
true /* prompt user regarding locked items */ );
const EDA_ITEM* const last_item = selection.GetLastAddedItem();
// Gather or construct polygon source shapes to merge
std::vector<PCB_SHAPE*> items_to_process;
for( EDA_ITEM* item : selection )
{
items_to_process.push_back( static_cast<PCB_SHAPE*>( item ) );
// put the last one in the selection at the front of the vector
// so it can be used as the property donor and as the basis for the
// boolean operation
if( item == last_item )
{
std::swap( items_to_process.back(), items_to_process.front() );
}
}
BOARD_COMMIT commit{ this };
// Handle modifications to existing items by the routine
const auto item_modification_handler = [&]( PCB_SHAPE& aItem )
{
commit.Modify( &aItem );
};
std::vector<PCB_SHAPE*> items_to_select_on_success;
const auto item_creation_handler = [&]( std::unique_ptr<PCB_SHAPE> aItem )
{
items_to_select_on_success.push_back( aItem.get() );
commit.Add( aItem.release() );
};
const auto item_removal_handler = [&]( PCB_SHAPE& aItem )
{
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 );
// Construct an appropriate routine
std::unique_ptr<POLYGON_BOOLEAN_ROUTINE> boolean_routine;
if( aEvent.IsAction( &PCB_ACTIONS::mergePolygons ) )
{
boolean_routine =
std::make_unique<POLYGON_MERGE_ROUTINE>( frame()->GetModel(), change_handler );
}
else if( aEvent.IsAction( &PCB_ACTIONS::subtractPolygons ) )
{
boolean_routine =
std::make_unique<POLYGON_SUBTRACT_ROUTINE>( frame()->GetModel(), change_handler );
}
else if( aEvent.IsAction( &PCB_ACTIONS::intersectPolygons ) )
{
boolean_routine =
std::make_unique<POLYGON_INTERSECT_ROUTINE>( frame()->GetModel(), change_handler );
}
else
{
wxASSERT_MSG( false, "Could not find a polygon routine for this action" );
return 0;
}
// Perform the operation on each polygon
for( PCB_SHAPE* shape : items_to_process )
{
boolean_routine->ProcessShape( *shape );
}
// Select new items
for( PCB_SHAPE* item : items_to_select_on_success )
{
m_selectionTool->AddItemToSel( item, true );
}
// Notify other tools of the changes
m_toolMgr->ProcessEvent( EVENTS::SelectedItemsModified );
commit.Push( boolean_routine->GetCommitDescription() );
if( const std::optional<wxString> msg = boolean_routine->GetStatusMessage() )
{
frame()->ShowInfoBarMsg( *msg );
}
return 0;
}
int EDIT_TOOL::Properties( const TOOL_EVENT& aEvent )
{
PCB_BASE_EDIT_FRAME* editFrame = getEditFrame<PCB_BASE_EDIT_FRAME>();
@ -2642,6 +2765,9 @@ void EDIT_TOOL::setTransitions()
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::filletLines.MakeEvent() );
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::chamferLines.MakeEvent() );
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::extendLines.MakeEvent() );
Go( &EDIT_TOOL::BooleanPolygons, PCB_ACTIONS::mergePolygons.MakeEvent() );
Go( &EDIT_TOOL::BooleanPolygons, PCB_ACTIONS::subtractPolygons.MakeEvent() );
Go( &EDIT_TOOL::BooleanPolygons, PCB_ACTIONS::intersectPolygons.MakeEvent() );
Go( &EDIT_TOOL::copyToClipboard, ACTIONS::copy.MakeEvent() );
Go( &EDIT_TOOL::copyToClipboard, PCB_ACTIONS::copyWithReference.MakeEvent() );

View File

@ -126,6 +126,12 @@ public:
*/
int ModifyLines( const TOOL_EVENT& aEvent );
/**
* Modify selected polygons into a single polygon using boolean operations
* such as merge (union) or subtract (difference)
*/
int BooleanPolygons( const TOOL_EVENT& aEvent );
/**
* Delete currently selected items.
*/

View File

@ -301,3 +301,194 @@ void LINE_EXTENSION_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLin
AddSuccess();
}
void POLYGON_BOOLEAN_ROUTINE::ProcessShape( PCB_SHAPE& aPcbShape )
{
std::unique_ptr<SHAPE_POLY_SET> poly;
switch( aPcbShape.GetShape() )
{
case SHAPE_T::POLY:
{
poly = std::make_unique<SHAPE_POLY_SET>( aPcbShape.GetPolyShape() );
break;
}
case SHAPE_T::RECTANGLE:
{
SHAPE_POLY_SET rect_poly;
const std::vector<VECTOR2I> rect_pts = aPcbShape.GetRectCorners();
rect_poly.NewOutline();
for( const VECTOR2I& pt : rect_pts )
{
rect_poly.Append( pt );
}
poly = std::make_unique<SHAPE_POLY_SET>( std::move( rect_poly ) );
break;
}
default:
{
break;
}
}
if( !poly )
{
// Not a polygon or rectangle, nothing to do
return;
}
if( !m_workingPolygon )
{
auto initial = std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::POLY );
initial->SetPolyShape( *poly );
// Copy properties
initial->SetLayer( aPcbShape.GetLayer() );
initial->SetWidth( aPcbShape.GetWidth() );
// Keep the pointer
m_workingPolygon = initial.get();
// Hand over ownership
GetHandler().AddNewItem( std::move( initial ) );
// And remove the shape
GetHandler().DeleteItem( aPcbShape );
}
else
{
if( ProcessSubsequentPolygon( *poly ) )
{
// If we could process the polygon, delete the source
GetHandler().DeleteItem( aPcbShape );
AddSuccess();
}
else
{
AddFailure();
}
}
}
wxString POLYGON_MERGE_ROUTINE::GetCommitDescription() const
{
return _( "Merge polygons." );
}
std::optional<wxString> POLYGON_MERGE_ROUTINE::GetStatusMessage() const
{
if( GetSuccesses() == 0 )
{
return _( "Unable to merge the selected polygons." );
}
else if( GetFailures() > 0 )
{
return _( "Some of the polygons could not be merged." );
}
return std::nullopt;
}
bool POLYGON_MERGE_ROUTINE::ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon )
{
const SHAPE_POLY_SET::POLYGON_MODE poly_mode = SHAPE_POLY_SET::POLYGON_MODE::PM_FAST;
SHAPE_POLY_SET working_copy = GetWorkingPolygon()->GetPolyShape();
working_copy.BooleanAdd( aPolygon, poly_mode );
// Check it's not disjoint - this doesn't work well in the UI
if( working_copy.OutlineCount() != 1 )
{
return false;
}
GetWorkingPolygon()->SetPolyShape( working_copy );
return true;
}
wxString POLYGON_SUBTRACT_ROUTINE::GetCommitDescription() const
{
return _( "Subtract polygons." );
}
std::optional<wxString> POLYGON_SUBTRACT_ROUTINE::GetStatusMessage() const
{
if( GetSuccesses() == 0 )
{
return _( "Unable to subtract the selected polygons." );
}
else if( GetFailures() > 0 )
{
return _( "Some of the polygons could not be subtracted." );
}
return std::nullopt;
}
bool POLYGON_SUBTRACT_ROUTINE::ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon )
{
const SHAPE_POLY_SET::POLYGON_MODE poly_mode = SHAPE_POLY_SET::POLYGON_MODE::PM_FAST;
SHAPE_POLY_SET working_copy = GetWorkingPolygon()->GetPolyShape();
working_copy.BooleanSubtract( aPolygon, poly_mode );
// Subtraction can create holes or delete the polygon
// In theory we can allow holes as the EDA_SHAPE will fracture for us, but that's
// probably not what the user has in mind (?)
if( working_copy.OutlineCount() != 1 || working_copy.HoleCount( 0 ) > 0
|| working_copy.VertexCount( 0 ) == 0 )
{
// If that happens, just skip the operation
return false;
}
GetWorkingPolygon()->SetPolyShape( working_copy );
return true;
}
wxString POLYGON_INTERSECT_ROUTINE::GetCommitDescription() const
{
return _( "Intersect polygons." );
}
std::optional<wxString> POLYGON_INTERSECT_ROUTINE::GetStatusMessage() const
{
if( GetSuccesses() == 0 )
{
return _( "Unable to intersect the selected polygons." );
}
else if( GetFailures() > 0 )
{
return _( "Some of the polygons could not be intersected." );
}
return std::nullopt;
}
bool POLYGON_INTERSECT_ROUTINE::ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon )
{
const SHAPE_POLY_SET::POLYGON_MODE poly_mode = SHAPE_POLY_SET::POLYGON_MODE::PM_FAST;
SHAPE_POLY_SET working_copy = GetWorkingPolygon()->GetPolyShape();
working_copy.BooleanIntersection( aPolygon, poly_mode );
// Is there anything left?
if( working_copy.OutlineCount() == 0 )
{
// There was no intersection. Rather than deleting the working polygon, we'll skip
// and report a failure.
return false;
}
GetWorkingPolygon()->SetPolyShape( working_copy );
return true;
}

View File

@ -297,4 +297,74 @@ public:
void ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB ) override;
};
/**
* A routine that modifies polygons using boolean operations
*/
class POLYGON_BOOLEAN_ROUTINE : public ITEM_MODIFICATION_ROUTINE
{
public:
POLYGON_BOOLEAN_ROUTINE( BOARD_ITEM* aBoard, CHANGE_HANDLER& aHandler ) :
ITEM_MODIFICATION_ROUTINE( aBoard, aHandler ), m_workingPolygon( nullptr )
{
}
void ProcessShape( PCB_SHAPE& aPcbShape );
protected:
PCB_SHAPE* GetWorkingPolygon() const { return m_workingPolygon; }
virtual bool ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon ) = 0;
private:
PCB_SHAPE* m_workingPolygon;
};
class POLYGON_MERGE_ROUTINE : public POLYGON_BOOLEAN_ROUTINE
{
public:
POLYGON_MERGE_ROUTINE( BOARD_ITEM* aBoard, CHANGE_HANDLER& aHandler ) :
POLYGON_BOOLEAN_ROUTINE( aBoard, aHandler )
{
}
wxString GetCommitDescription() const override;
std::optional<wxString> GetStatusMessage() const override;
private:
bool ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon ) override;
};
class POLYGON_SUBTRACT_ROUTINE : public POLYGON_BOOLEAN_ROUTINE
{
public:
POLYGON_SUBTRACT_ROUTINE( BOARD_ITEM* aBoard, CHANGE_HANDLER& aHandler ) :
POLYGON_BOOLEAN_ROUTINE( aBoard, aHandler )
{
}
wxString GetCommitDescription() const override;
std::optional<wxString> GetStatusMessage() const override;
private:
bool ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon ) override;
};
class POLYGON_INTERSECT_ROUTINE : public POLYGON_BOOLEAN_ROUTINE
{
public:
POLYGON_INTERSECT_ROUTINE( BOARD_ITEM* aBoard, CHANGE_HANDLER& aHandler ) :
POLYGON_BOOLEAN_ROUTINE( aBoard, aHandler )
{
}
wxString GetCommitDescription() const override;
std::optional<wxString> GetStatusMessage() const override;
private:
bool ProcessSubsequentPolygon( const SHAPE_POLY_SET& aPolygon ) override;
};
#endif /* ITEM_MODIFICATION_ROUTINE_H_ */

View File

@ -585,6 +585,27 @@ TOOL_ACTION PCB_ACTIONS::extendLines( TOOL_ACTION_ARGS()
.MenuText( _( "Extend Lines to Meet" ) )
.Tooltip( _( "Extend lines to meet each other" ) ) );
TOOL_ACTION PCB_ACTIONS::mergePolygons( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.mergePolygons" )
.Scope( AS_GLOBAL )
.MenuText( _( "Merge Polygons" ) )
.Tooltip( _( "Merge selected polygons into a single polygon" ) )
.Icon( BITMAPS::merge_polygons ) );
TOOL_ACTION PCB_ACTIONS::subtractPolygons( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.subtractPolygons" )
.Scope( AS_GLOBAL )
.MenuText( _( "Subtract Polygons" ) )
.Tooltip( _( "Subtract selected polygons from the last one selected" ) )
.Icon( BITMAPS::subtract_polygons ) );
TOOL_ACTION PCB_ACTIONS::intersectPolygons( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.intersectPolygons" )
.Scope( AS_GLOBAL )
.MenuText( _( "Intersect Polygons" ) )
.Tooltip( _( "Create the intersection of the selected polygons" ) )
.Icon( BITMAPS::intersect_polygons ) );
TOOL_ACTION PCB_ACTIONS::deleteFull( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.deleteFull" )
.Scope( AS_GLOBAL )

View File

@ -160,6 +160,13 @@ public:
/// Extend selected lines to meet at a point
static TOOL_ACTION extendLines;
/// Merge multiple polygons into a single polygon
static TOOL_ACTION mergePolygons;
/// Subtract polygons from other polygons
static TOOL_ACTION subtractPolygons;
/// Intersection of multiple polygons
static TOOL_ACTION intersectPolygons;
/// Activation of the edit tool
static TOOL_ACTION properties;

View File

@ -327,6 +327,7 @@ set( BMAPS_MID
import
info
insert_module_board
intersect_polygons
language
layers_manager
leave_sheet
@ -351,7 +352,7 @@ set( BMAPS_MID
marker_next
marker_previous
measurement
pcb_target
merge_polygons
mirror_h
mirror_v
mode_module
@ -405,6 +406,7 @@ set( BMAPS_MID
part_properties
paste
paste_special
pcb_target
pin2pin
pin_size_to
pin_show_etype
@ -496,6 +498,7 @@ set( BMAPS_MID
show_back_assembly_layers
special_tools
spreadsheet
subtract_polygons
swap
swap_layer
switch_corner_rounding_shape

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24"
width="24"
version="1.1"
id="svg2"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="intersect_polygons.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2554"
inkscape:window-height="1400"
id="namedview16"
showgrid="true"
inkscape:zoom="11.313708"
inkscape:cx="41.100582"
inkscape:cy="9.3249707"
inkscape:window-x="1920"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5"
spacingy="0.5" />
</sodipodi:namedview>
<path
id="rect4835"
style="fill:none;fill-opacity:0.336982;stroke:#e0e0e0;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
d="M 3,15 V 3 h 12 v 6 h 6 V 21 H 9 v -6 z"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:none;fill-opacity:1;stroke:#ded3dd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 15,15 V 9 H 9 v 6 z"
id="path6559"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24"
width="24"
version="1.1"
id="svg2"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="merge_polygons.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2554"
inkscape:window-height="1400"
id="namedview16"
showgrid="true"
inkscape:zoom="11.313708"
inkscape:cx="48.260038"
inkscape:cy="9.148194"
inkscape:window-x="1920"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5"
spacingy="0.5" />
</sodipodi:namedview>
<path
style="fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 15,15 V 9 H 9 v 6 z"
id="path6559"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
id="rect4835"
style="fill:none;fill-opacity:0.336982;stroke:#ded3dd;stroke-width:2;stroke-linejoin:round"
d="M 3,15 V 3 h 12 v 6 h 6 V 21 H 9 v -6 z"
sodipodi:nodetypes="ccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24"
width="24"
version="1.1"
id="svg2"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="subtract_polygons.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2554"
inkscape:window-height="1400"
id="namedview16"
showgrid="false"
inkscape:zoom="1"
inkscape:cx="8"
inkscape:cy="3.5"
inkscape:window-x="1920"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5"
spacingy="0.5" />
</sodipodi:namedview>
<path
style="fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 21,21 V 9 H 9 v 12 z"
id="path6559-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
id="rect4835-5"
style="fill:none;fill-opacity:0.336982;stroke:#ded3dd;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
d="M 3,15 V 3 H 15 V 9 H 9 v 6 z"
sodipodi:nodetypes="ccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24"
width="24"
version="1.1"
id="svg2"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="merge_polygons.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2554"
inkscape:window-height="1400"
id="namedview16"
showgrid="true"
inkscape:zoom="22.124583"
inkscape:cx="10.983258"
inkscape:cy="9.9662896"
inkscape:window-x="1920"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showguides="true">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5"
spacingy="0.5" />
<sodipodi:guide
position="9,34"
orientation="0,-1"
id="guide38552" />
<sodipodi:guide
position="9,34"
orientation="0,-1"
id="guide38554" />
</sodipodi:namedview>
<path
style="fill:none;fill-opacity:1;stroke:#545454;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 15,15 V 9 H 9 v 6 z"
id="path6559"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
id="rect4835"
style="fill:none;fill-opacity:0.336982;stroke:#b9b9b9;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
d="M 3,15 V 3 h 12 v 6 h 6 V 21 H 9 v -6 z"
sodipodi:nodetypes="ccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24"
width="24"
version="1.1"
id="svg2"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="merge_polygons.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2554"
inkscape:window-height="1400"
id="namedview16"
showgrid="true"
inkscape:zoom="7.8222213"
inkscape:cx="-5.113637"
inkscape:cy="18.217332"
inkscape:window-x="1920"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5"
spacingy="0.5" />
</sodipodi:namedview>
<path
style="fill:none;fill-opacity:1;stroke:#b9b9b9;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 15,15 V 9 H 9 v 6 z"
id="path6559"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
id="rect4835"
style="fill:none;fill-opacity:0.336982;stroke:#545454;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
d="M 3,15 V 3 h 12 v 6 h 6 V 21 H 9 v -6 z"
sodipodi:nodetypes="ccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24"
width="24"
version="1.1"
id="svg2"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="subtract_polygons.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2554"
inkscape:window-height="1400"
id="namedview16"
showgrid="true"
inkscape:zoom="11.062291"
inkscape:cx="14.101961"
inkscape:cy="23.819658"
inkscape:window-x="1920"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:document-rotation="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5"
spacingy="0.5" />
</sodipodi:namedview>
<path
style="fill:none;fill-opacity:1;stroke:#b9b9b9;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 21,21 V 9 H 9 v 12 z"
id="path6559"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
id="rect4835"
style="fill:none;fill-opacity:0.336982;stroke:#545454;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
d="M 3,15 V 3 H 15 V 9 H 9 v 6 z"
sodipodi:nodetypes="ccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB