From 2c5876ee8c06dd4e8a8339f43e3554f4bb1dfc81 Mon Sep 17 00:00:00 2001 From: Jeff Young Date: Tue, 16 Jul 2019 16:41:21 +0100 Subject: [PATCH] Add more control over how corners are handled when inflating polygons. --- common/geometry/shape_poly_set.cpp | 33 +++++++-------- include/geometry/shape_poly_set.h | 66 +++++++++++++++++++----------- pcbnew/eagle_plugin.cpp | 2 +- pcbnew/zone_filler.cpp | 24 ++++++++--- polygon/clipper.cpp | 4 +- polygon/include/clipper.hpp | 1 + 6 files changed, 82 insertions(+), 48 deletions(-) diff --git a/common/geometry/shape_poly_set.cpp b/common/geometry/shape_poly_set.cpp index 1832ac8992..95c1590509 100644 --- a/common/geometry/shape_poly_set.cpp +++ b/common/geometry/shape_poly_set.cpp @@ -544,7 +544,8 @@ void SHAPE_POLY_SET::BooleanIntersection( const SHAPE_POLY_SET& a, } -void SHAPE_POLY_SET::InflateWithLinkedHoles( int aFactor, int aCircleSegmentsCount, POLYGON_MODE aFastMode ) +void SHAPE_POLY_SET::InflateWithLinkedHoles( int aFactor, int aCircleSegmentsCount, + POLYGON_MODE aFastMode ) { Simplify( aFastMode ); Inflate( aFactor, aCircleSegmentsCount ); @@ -552,23 +553,23 @@ void SHAPE_POLY_SET::InflateWithLinkedHoles( int aFactor, int aCircleSegmentsCou } -void SHAPE_POLY_SET::Inflate( int aFactor, int aCircleSegmentsCount, bool aPreserveCorners ) +void SHAPE_POLY_SET::Inflate( int aAmount, int aCircleSegmentsCount, + CORNER_STRATEGY aCornerStrategy ) { // A static table to avoid repetitive calculations of the coefficient - // 1.0 - cos( M_PI/aCircleSegmentsCount) + // 1.0 - cos( M_PI / aCircleSegmentsCount ) // aCircleSegmentsCount is most of time <= 64 and usually 8, 12, 16, 32 #define SEG_CNT_MAX 64 static double arc_tolerance_factor[SEG_CNT_MAX + 1]; ClipperOffset c; - // N.B. using jtSquare here does not create square corners; they end up mitered by aFactor. - // Setting jtMiter with a sufficiently high MiterLimit will preserve corners, but things - // get ugly at very acute angles (and we don't really want to support those anyway for peeling - // concerns). Setting a MiterLimit of 1.4145 preserves corners up to 90 degrees; we set the - // limit a bit above that. - JoinType joinType = aPreserveCorners ? jtMiter : jtRound; - double miterLimit = 1.5; + // N.B. see the Clipper documentation for jtSquare/jtMiter/jtRound. They are poorly named + // and are not what you'd think they are. + // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Types/JoinType.htm + JoinType joinType = aCornerStrategy == ROUND_ALL_CORNERS ? jtRound : jtMiter; + double miterLimit = aCornerStrategy == ALLOW_ACUTE_CORNERS ? 10 : 1.5; + JoinType miterFallback = aCornerStrategy == ROUND_ACUTE_CORNERS ? jtRound : jtSquare; for( const POLYGON& poly : m_polys ) { @@ -578,10 +579,9 @@ void SHAPE_POLY_SET::Inflate( int aFactor, int aCircleSegmentsCount, bool aPrese PolyTree solution; - // Calculate the arc tolerance (arc error) from the seg count by circle. - // the seg count is nn = M_PI / acos(1.0 - c.ArcTolerance / abs(aFactor)) - // see: - // www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm + // Calculate the arc tolerance (arc error) from the seg count by circle. The seg count is + // nn = M_PI / acos(1.0 - c.ArcTolerance / abs(aAmount)) + // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm if( aCircleSegmentsCount < 6 ) // avoid incorrect aCircleSegmentsCount values aCircleSegmentsCount = 6; @@ -598,9 +598,10 @@ void SHAPE_POLY_SET::Inflate( int aFactor, int aCircleSegmentsCount, bool aPrese else coeff = arc_tolerance_factor[aCircleSegmentsCount]; - c.ArcTolerance = std::abs( aFactor ) * coeff; + c.ArcTolerance = std::abs( aAmount ) * coeff; c.MiterLimit = miterLimit; - c.Execute( solution, aFactor ); + c.MiterFallback = miterFallback; + c.Execute( solution, aAmount ); importTree( &solution ); } diff --git a/include/geometry/shape_poly_set.h b/include/geometry/shape_poly_set.h index 58e4de1bfb..cd7af373e9 100644 --- a/include/geometry/shape_poly_set.h +++ b/include/geometry/shape_poly_set.h @@ -165,11 +165,16 @@ class SHAPE_POLY_SET : public SHAPE operator bool() const { - return ( ( m_currentPolygon < m_lastPolygon ) || ( m_currentPolygon == m_poly->OutlineCount() - 1 && - ( m_currentContour < static_cast( m_poly->CPolygon( m_currentPolygon ).size() ) - 1 || - ( m_currentVertex < m_poly->CPolygon( m_currentPolygon )[m_currentContour].PointCount() ) - ) - ) ); + if( m_currentPolygon < m_lastPolygon ) + return true; + + if( m_currentPolygon != m_poly->OutlineCount() - 1 ) + return false; + + auto currentPolygon = m_poly->CPolygon( m_currentPolygon ); + + return m_currentContour < (int) currentPolygon.size() - 1 + || m_currentVertex < currentPolygon[m_currentContour].PointCount(); } /** @@ -859,29 +864,40 @@ class SHAPE_POLY_SET : public SHAPE void BooleanIntersection( const SHAPE_POLY_SET& a, const SHAPE_POLY_SET& b, POLYGON_MODE aFastMode ); - /** - * Performs outline inflation/deflation, using (optionally) round corners. - * Polygons can have holes, but not linked holes with main outlines, - * if aFactor < 0. - * When aFactor is < 0 a bad shape can result from these extra-segments used to - * link holes to main outlines - * Use InflateWithLinkedHoles for these polygons, especially if aFactor < 0 - * - * @param aFactor - number of units to offset edges - * @param aCircleSegmentsCount - number of segments per 360° to use in curve approx - * @param aPreserveCorners - If true, use square joints to keep angles preserved - */ - void Inflate( int aFactor, int aCircleSegmentsCount, bool aPreserveCorners = false ); - - void Deflate( int aFactor, int aCircleSegmentsCount, bool aPreserveCorners = false ) + enum CORNER_STRATEGY { - Inflate( -aFactor, aPreserveCorners, aPreserveCorners ); + ALLOW_ACUTE_CORNERS, + CHOP_ACUTE_CORNERS, + ROUND_ACUTE_CORNERS, + ROUND_ALL_CORNERS + }; + + /** + * Performs outline inflation/deflation. Polygons can have holes, but not linked holes + * with main outlines, if aFactor < 0. For those use InflateWithLinkedHoles() to avoid + * odd corners where the link segments meet the outline. + * + * @param aAmount - number of units to offset edges + * @param aCircleSegmentsCount - number of segments per 360° to use in curve approx + * @param aCornerStrategy - ALLOW_ACUTE_CORNERS to preserve all angles, + * CHOP_ACUTE_CORNERS to chop angles less than 90°, + * ROUND_ACUTE_CORNERS to round off angles less than 90°, + * ROUND_ALL_CORNERS to round regardless of angles + */ + void Inflate( int aAmount, int aCircleSegmentsCount, + CORNER_STRATEGY aCornerStrategy = CHOP_ACUTE_CORNERS ); + + void Deflate( int aAmount, int aCircleSegmentsCount, + CORNER_STRATEGY aCornerStrategy = CHOP_ACUTE_CORNERS ) + { + Inflate( -aAmount, aCircleSegmentsCount, aCornerStrategy ); } - ///> Performs outline inflation/deflation, using round corners. - ///> Polygons can have holes, and/or linked holes with main outlines. - ///> The resulting polygons are laso polygons with linked holes to main outlines - ///> For aFastMode meaning, see function booleanOp + /** + * Performs outline inflation/deflation, using round corners. Polygons can have holes, + * and/or linked holes with main outlines. The resulting polygons are laso polygons with + * linked holes to main outlines. For aFastMode meaning, see function booleanOp . + */ void InflateWithLinkedHoles( int aFactor, int aCircleSegmentsCount, POLYGON_MODE aFastMode ); ///> Converts a set of polygons with holes to a singe outline with "slits"/"fractures" diff --git a/pcbnew/eagle_plugin.cpp b/pcbnew/eagle_plugin.cpp index 72539bc2ab..db44fdaafd 100644 --- a/pcbnew/eagle_plugin.cpp +++ b/pcbnew/eagle_plugin.cpp @@ -1186,7 +1186,7 @@ ZONE_CONTAINER* EAGLE_PLUGIN::loadPolygon( wxXmlNode* aPolyNode ) // We trace the zone such that the copper is completely inside. if( p.width.ToPcbUnits() > 0 ) { - polygon.Inflate( p.width.ToPcbUnits() / 2, 32, true ); + polygon.Inflate( p.width.ToPcbUnits() / 2, 32, SHAPE_POLY_SET::ALLOW_ACUTE_CORNERS ); polygon.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); } diff --git a/pcbnew/zone_filler.cpp b/pcbnew/zone_filler.cpp index 65ba2ef136..acd4af0986 100644 --- a/pcbnew/zone_filler.cpp +++ b/pcbnew/zone_filler.cpp @@ -693,6 +693,11 @@ void ZONE_FILLER::computeRawFilledArea( const ZONE_CONTAINER* aZone, int epsilon = Millimeter2iu( 0.001 ); int numSegs = std::max( GetArcToSegmentCount( half_min_width, m_high_def, 360.0 ), 6 ); + SHAPE_POLY_SET::CORNER_STRATEGY cornerStrategy = SHAPE_POLY_SET::CHOP_ACUTE_CORNERS; + + if( aZone->GetCornerSmoothingType() == ZONE_SETTINGS::SMOOTHING_FILLET ) + cornerStrategy = SHAPE_POLY_SET::ROUND_ACUTE_CORNERS; + std::deque thermalSpokes; SHAPE_POLY_SET clearanceHoles; @@ -723,8 +728,11 @@ void ZONE_FILLER::computeRawFilledArea( const ZONE_CONTAINER* aZone, testAreas.BooleanSubtract( clearanceHoles, SHAPE_POLY_SET::PM_FAST ); // Prune features that don't meet minimum-width criteria - testAreas.Deflate( half_min_width - epsilon, numSegs, true ); - testAreas.Inflate( half_min_width - epsilon, numSegs, true ); + if( half_min_width - epsilon > epsilon ) + { + testAreas.Deflate( half_min_width - epsilon, numSegs, cornerStrategy ); + testAreas.Inflate( half_min_width - epsilon, numSegs, cornerStrategy ); + } // Spoke-end-testing is hugely expensive so we generate cached bounding-boxes to speed // things up a bit. @@ -759,7 +767,8 @@ void ZONE_FILLER::computeRawFilledArea( const ZONE_CONTAINER* aZone, aRawPolys.BooleanSubtract( clearanceHoles, SHAPE_POLY_SET::PM_FAST ); // Prune features that don't meet minimum-width criteria - aRawPolys.Deflate( half_min_width - epsilon, numSegs, true ); + if( half_min_width - epsilon > epsilon ) + aRawPolys.Deflate( half_min_width - epsilon, numSegs, cornerStrategy ); if( s_DumpZonesWhenFilling ) dumper->Write( &aRawPolys, "solid-areas-before-hatching" ); @@ -777,10 +786,15 @@ void ZONE_FILLER::computeRawFilledArea( const ZONE_CONTAINER* aZone, // If we're stroking the zone with a min_width stroke then this will naturally // inflate the zone by half_min_width } - else if( half_min_width - epsilon > epsilon ) // avoid very small outline thickness + else if( half_min_width - epsilon > epsilon ) { aRawPolys.Simplify( SHAPE_POLY_SET::PM_FAST ); - aRawPolys.Inflate( half_min_width - epsilon, numSegs, true ); + aRawPolys.Inflate( half_min_width - epsilon, numSegs, cornerStrategy ); + + // If we've deflated/inflated by something near our corner radius then we will have + // ended up with too-sharp corners. Apply outline smoothing again. + if( aZone->GetMinThickness() > aZone->GetCornerRadius() ) + aRawPolys.BooleanIntersection( aSmoothedOutline, SHAPE_POLY_SET::PM_FAST ); } aRawPolys.Fracture( SHAPE_POLY_SET::PM_FAST ); diff --git a/polygon/clipper.cpp b/polygon/clipper.cpp index 04e3a84cf9..76fa5cfbed 100644 --- a/polygon/clipper.cpp +++ b/polygon/clipper.cpp @@ -5369,8 +5369,10 @@ void ClipperOffset::OffsetPoint( int j, int& k, JoinType jointype ) if( r >= m_miterLim ) DoMiter( j, k, r ); - else + else if( MiterFallback == jtRound ) DoRound( j, k ); + else + DoSquare( j, k ); break; } diff --git a/polygon/include/clipper.hpp b/polygon/include/clipper.hpp index f385adeef6..d25066b9ad 100644 --- a/polygon/include/clipper.hpp +++ b/polygon/include/clipper.hpp @@ -418,6 +418,7 @@ public: void Clear(); double MiterLimit; + JoinType MiterFallback; double ArcTolerance; private: