502 lines
13 KiB
C++
502 lines
13 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, you may find one here:
|
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
* or you may search the http://www.gnu.org website for the version 2 license,
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include "item_modification_routine.h"
|
|
#include <geometry/geometry_utils.h>
|
|
|
|
namespace
|
|
{
|
|
|
|
/**
|
|
* Check if two segments share an endpoint (can be at either end of either segment)
|
|
*/
|
|
bool SegmentsShareEndpoint( const SEG& aSegA, const SEG& aSegB )
|
|
{
|
|
return ( aSegA.A == aSegB.A || aSegA.A == aSegB.B || aSegA.B == aSegB.A || aSegA.B == aSegB.B );
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
bool ITEM_MODIFICATION_ROUTINE::ModifyLineOrDeleteIfZeroLength( PCB_SHAPE& aLine, const SEG& aSeg )
|
|
{
|
|
wxASSERT_MSG( aLine.GetShape() == SHAPE_T::SEGMENT, "Can only modify segments" );
|
|
|
|
const bool removed = aSeg.Length() == 0;
|
|
if( !removed )
|
|
{
|
|
// Mark modified, then change it
|
|
GetHandler().MarkItemModified( aLine );
|
|
aLine.SetStart( aSeg.A );
|
|
aLine.SetEnd( aSeg.B );
|
|
}
|
|
else
|
|
{
|
|
// The line has become zero length - delete it
|
|
GetHandler().DeleteItem( aLine );
|
|
}
|
|
return removed;
|
|
}
|
|
|
|
|
|
wxString LINE_FILLET_ROUTINE::GetCommitDescription() const
|
|
{
|
|
return _( "Fillet Lines" );
|
|
}
|
|
|
|
|
|
std::optional<wxString> LINE_FILLET_ROUTINE::GetStatusMessage() const
|
|
{
|
|
if( GetSuccesses() == 0 )
|
|
{
|
|
return _( "Unable to fillet the selected lines." );
|
|
}
|
|
else if( GetFailures() > 0 )
|
|
{
|
|
return _( "Some of the lines could not be filleted." );
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
|
|
void LINE_FILLET_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB )
|
|
{
|
|
if( aLineA.GetLength() == 0.0 || aLineB.GetLength() == 0.0 )
|
|
return;
|
|
|
|
SEG seg_a( aLineA.GetStart(), aLineA.GetEnd() );
|
|
SEG seg_b( aLineB.GetStart(), aLineB.GetEnd() );
|
|
VECTOR2I* a_pt;
|
|
VECTOR2I* b_pt;
|
|
|
|
if( seg_a.A == seg_b.A )
|
|
{
|
|
a_pt = &seg_a.A;
|
|
b_pt = &seg_b.A;
|
|
}
|
|
else if( seg_a.A == seg_b.B )
|
|
{
|
|
a_pt = &seg_a.A;
|
|
b_pt = &seg_b.B;
|
|
}
|
|
else if( seg_a.B == seg_b.A )
|
|
{
|
|
a_pt = &seg_a.B;
|
|
b_pt = &seg_b.A;
|
|
}
|
|
else if( seg_a.B == seg_b.B )
|
|
{
|
|
a_pt = &seg_a.B;
|
|
b_pt = &seg_b.B;
|
|
}
|
|
else
|
|
// Nothing to do
|
|
return;
|
|
|
|
if( seg_a.Angle( seg_b ).IsHorizontal() )
|
|
return;
|
|
|
|
SHAPE_ARC sArc( seg_a, seg_b, m_filletRadiusIU );
|
|
VECTOR2I t1newPoint, t2newPoint;
|
|
|
|
auto setIfPointOnSeg = []( VECTOR2I& aPointToSet, SEG aSegment, VECTOR2I aVecToTest )
|
|
{
|
|
VECTOR2I segToVec = aSegment.NearestPoint( aVecToTest ) - aVecToTest;
|
|
|
|
// Find out if we are on the segment (minimum precision)
|
|
if( segToVec.EuclideanNorm() < SHAPE_ARC::MIN_PRECISION_IU )
|
|
{
|
|
aPointToSet.x = aVecToTest.x;
|
|
aPointToSet.y = aVecToTest.y;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
//Do not draw a fillet if the end points of the arc are not within the track segments
|
|
if( !setIfPointOnSeg( t1newPoint, seg_a, sArc.GetP0() )
|
|
&& !setIfPointOnSeg( t2newPoint, seg_b, sArc.GetP0() ) )
|
|
{
|
|
AddFailure();
|
|
return;
|
|
}
|
|
|
|
if( !setIfPointOnSeg( t1newPoint, seg_a, sArc.GetP1() )
|
|
&& !setIfPointOnSeg( t2newPoint, seg_b, sArc.GetP1() ) )
|
|
{
|
|
AddFailure();
|
|
return;
|
|
}
|
|
|
|
auto tArc = std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::ARC );
|
|
|
|
tArc->SetArcGeometry( sArc.GetP0(), sArc.GetArcMid(), sArc.GetP1() );
|
|
|
|
// Copy properties from one of the source lines
|
|
tArc->SetWidth( aLineA.GetWidth() );
|
|
tArc->SetLayer( aLineA.GetLayer() );
|
|
tArc->SetLocked( aLineA.IsLocked() );
|
|
|
|
CHANGE_HANDLER& handler = GetHandler();
|
|
|
|
handler.AddNewItem( std::move( tArc ) );
|
|
|
|
*a_pt = t1newPoint;
|
|
*b_pt = t2newPoint;
|
|
|
|
ModifyLineOrDeleteIfZeroLength( aLineA, seg_a );
|
|
ModifyLineOrDeleteIfZeroLength( aLineB, seg_b );
|
|
|
|
AddSuccess();
|
|
}
|
|
|
|
wxString LINE_CHAMFER_ROUTINE::GetCommitDescription() const
|
|
{
|
|
return _( "Chamfer Lines" );
|
|
}
|
|
|
|
|
|
std::optional<wxString> LINE_CHAMFER_ROUTINE::GetStatusMessage() const
|
|
{
|
|
if( GetSuccesses() == 0 )
|
|
{
|
|
return _( "Unable to chamfer the selected lines." );
|
|
}
|
|
else if( GetFailures() > 0 )
|
|
{
|
|
return _( "Some of the lines could not be chamfered." );
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
|
|
void LINE_CHAMFER_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB )
|
|
{
|
|
if( aLineA.GetLength() == 0.0 || aLineB.GetLength() == 0.0 )
|
|
return;
|
|
|
|
SEG seg_a( aLineA.GetStart(), aLineA.GetEnd() );
|
|
SEG seg_b( aLineB.GetStart(), aLineB.GetEnd() );
|
|
|
|
// If the segments share an endpoint, we won't try to chamfer them
|
|
// (we could extend to the intersection point, but this gets complicated
|
|
// and inconsistent when you select more than two lines)
|
|
if( !SegmentsShareEndpoint( seg_a, seg_b ) )
|
|
{
|
|
// not an error, lots of lines in a 2+ line selection will not intersect
|
|
return;
|
|
}
|
|
|
|
std::optional<CHAMFER_RESULT> chamfer_result =
|
|
ComputeChamferPoints( seg_a, seg_b, m_chamferParams );
|
|
|
|
if( !chamfer_result )
|
|
{
|
|
AddFailure();
|
|
return;
|
|
}
|
|
|
|
auto tSegment = std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::SEGMENT );
|
|
|
|
tSegment->SetStart( chamfer_result->m_chamfer.A );
|
|
tSegment->SetEnd( chamfer_result->m_chamfer.B );
|
|
|
|
// Copy properties from one of the source lines
|
|
tSegment->SetWidth( aLineA.GetWidth() );
|
|
tSegment->SetLayer( aLineA.GetLayer() );
|
|
tSegment->SetLocked( aLineA.IsLocked() );
|
|
|
|
CHANGE_HANDLER& handler = GetHandler();
|
|
|
|
handler.AddNewItem( std::move( tSegment ) );
|
|
|
|
ModifyLineOrDeleteIfZeroLength( aLineA, *chamfer_result->m_updated_seg_a );
|
|
ModifyLineOrDeleteIfZeroLength( aLineB, *chamfer_result->m_updated_seg_b );
|
|
|
|
AddSuccess();
|
|
}
|
|
|
|
|
|
wxString LINE_EXTENSION_ROUTINE::GetCommitDescription() const
|
|
{
|
|
return _( "Extend Lines to Meet" );
|
|
}
|
|
|
|
|
|
std::optional<wxString> LINE_EXTENSION_ROUTINE::GetStatusMessage() const
|
|
{
|
|
if( GetSuccesses() == 0 )
|
|
{
|
|
return _( "Unable to extend the selected lines to meet." );
|
|
}
|
|
else if( GetFailures() > 0 )
|
|
{
|
|
return _( "Some of the lines could not be extended to meet." );
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
|
|
void LINE_EXTENSION_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB )
|
|
{
|
|
if( aLineA.GetLength() == 0.0 || aLineB.GetLength() == 0.0 )
|
|
return;
|
|
|
|
SEG seg_a( aLineA.GetStart(), aLineA.GetEnd() );
|
|
SEG seg_b( aLineB.GetStart(), aLineB.GetEnd() );
|
|
|
|
if( seg_a.Intersects( seg_b ) )
|
|
{
|
|
// already intersecting, nothing to do
|
|
return;
|
|
}
|
|
|
|
OPT_VECTOR2I intersection = seg_a.IntersectLines( seg_b );
|
|
|
|
if( !intersection )
|
|
{
|
|
// This might be an error, but it's also possible that the lines are
|
|
// parallel and don't intersect. We'll just ignore this case.
|
|
return;
|
|
}
|
|
|
|
CHANGE_HANDLER& handler = GetHandler();
|
|
|
|
const auto line_extender = [&]( const SEG& aSeg, PCB_SHAPE& aLine )
|
|
{
|
|
// If the intersection point is not already n the line, we'll extend to it
|
|
if( !aSeg.Contains( *intersection ) )
|
|
{
|
|
const int dist_start = ( *intersection - aSeg.A ).EuclideanNorm();
|
|
const int dist_end = ( *intersection - aSeg.B ).EuclideanNorm();
|
|
|
|
const VECTOR2I& furthest_pt = ( dist_start < dist_end ) ? aSeg.B : aSeg.A;
|
|
// Note, the drawing tool has COORDS_PADDING of 20mm, but we need a larger buffer
|
|
// or we are not able to select the generated segments
|
|
unsigned int edge_padding = static_cast<unsigned>( pcbIUScale.mmToIU( 200 ) );
|
|
VECTOR2I new_end = GetClampedCoords( *intersection, edge_padding );
|
|
|
|
handler.MarkItemModified( aLine );
|
|
aLine.SetStart( furthest_pt );
|
|
aLine.SetEnd( new_end );
|
|
}
|
|
};
|
|
|
|
line_extender( seg_a, aLineA );
|
|
line_extender( seg_b, aLineB );
|
|
|
|
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;
|
|
}
|