/* * 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 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 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( 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 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 = ComputeChamferPoints( seg_a, seg_b, m_chamferParams ); if( !chamfer_result ) { AddFailure(); return; } auto tSegment = std::make_unique( 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 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( 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 poly; switch( aPcbShape.GetShape() ) { case SHAPE_T::POLY: { poly = std::make_unique( aPcbShape.GetPolyShape() ); break; } case SHAPE_T::RECTANGLE: { SHAPE_POLY_SET rect_poly; const std::vector rect_pts = aPcbShape.GetRectCorners(); rect_poly.NewOutline(); for( const VECTOR2I& pt : rect_pts ) { rect_poly.Append( pt ); } poly = std::make_unique( 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( 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 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 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 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; }