Add a method to offset line chains.

This commit is contained in:
Alex Shvartzkop 2023-10-06 15:09:28 +03:00
parent 8a22a8fa69
commit de0c1218c3
4 changed files with 257 additions and 0 deletions

View File

@ -32,6 +32,7 @@
#include <geometry/seg.h>
#include <geometry/shape.h>
#include <geometry/shape_arc.h>
#include <geometry/corner_strategy.h>
#include <math/vector2d.h>
/**
@ -786,6 +787,20 @@ public:
*/
double Area( bool aAbsolute = true ) const;
/**
* Creates line chains \a aLeft and \a aRight offset to this line chain.
*
* @param aAmount is the amount to offset.
* @param aCornerStrategy is the corner rounding strategy.
* @param aMaxError is the max error used for rounding.
* @param aLeft left line chain output.
* @param aRight right line chain output.
* @param aSimplify set to run Simplify on the inflated polygon.
*/
bool OffsetLine( int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError,
SHAPE_LINE_CHAIN& aLeft, SHAPE_LINE_CHAIN& aRight,
bool aSimplify = false ) const;
size_t ArcCount() const
{
return m_arcs.size();

View File

@ -1032,6 +1032,22 @@ public:
Inflate( -aAmount, aCornerStrategy, aMaxError );
}
/**
* Perform offsetting of a line chain. Replaces this polygon set with the result.
*
* @param aLine is the line to perform offsetting on.
* @param aAmount is the number of units to offset the line chain.
* @param aCornerStrategy #ALLOW_ACUTE_CORNERS to preserve all angles,
* #CHAMFER_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
* @param aMaxError is the allowable deviation when rounding corners with an approximated
* polygon
* @param aSimplify set to simplify the output polygon.
*/
void OffsetLineChain( const SHAPE_LINE_CHAIN& aLine, int aAmount,
CORNER_STRATEGY aCornerStrategy, int aMaxError, bool aSimplify );
/**
* Perform outline inflation/deflation, using round corners.
*
@ -1435,6 +1451,9 @@ private:
void inflate1( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy );
void inflate2( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy, bool aSimplify = false );
void inflateLine2( const SHAPE_LINE_CHAIN& aLine, int aAmount, int aCircleSegCount,
CORNER_STRATEGY aCornerStrategy, bool aSimplify = false );
/**
* This is the engine to execute all polygon boolean transforms (AND, OR, ... and polygon
* simplification (merging overlapping polygons).

View File

@ -2131,6 +2131,123 @@ double SHAPE_LINE_CHAIN::Area( bool aAbsolute ) const
}
bool SHAPE_LINE_CHAIN::OffsetLine( int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError,
SHAPE_LINE_CHAIN& aLeft, SHAPE_LINE_CHAIN& aRight,
bool aSimplify ) const
{
if( PointCount() < 2 )
return false;
SHAPE_POLY_SET poly;
poly.OffsetLineChain( *this, aAmount * 2, aCornerStrategy, aMaxError, aSimplify );
if( poly.OutlineCount() != 1 )
return false;
if( poly.COutline( 0 ).PointCount() < 3 )
return false;
if( poly.HasHoles() )
return false;
SHAPE_LINE_CHAIN outline = poly.COutline( 0 );
wxASSERT( outline.IsClosed() );
const VECTOR2I& start = CPoint( 0 );
const VECTOR2I& end = CPoint( -1 );
outline.Split( start, true );
outline.Split( end, true );
const int idA = outline.Find( start );
const int idB = outline.Find( end );
if( idA == -1 || idB == -1 )
return false;
aLeft.Clear();
aRight.Clear();
for( int i = idA;; )
{
aLeft.Append( outline.CPoint( i ) );
i = ( i + 1 ) % outline.PointCount();
if( i == idB )
{
aLeft.Append( outline.CPoint( i ) );
break;
}
if( i == idA )
return false;
}
if( aLeft.PointCount() < 2 )
return false;
for( int i = idB;; )
{
aRight.Append( outline.CPoint( i ) );
i = ( i + 1 ) % outline.PointCount();
if( i == idA )
{
aRight.Append( outline.CPoint( i ) );
break;
}
if( i == idB )
return false;
}
if( aRight.PointCount() < 2 )
return false;
if( aLeft.CPoint( 0 ) != start )
{
aLeft = aLeft.Reverse();
wxASSERT( aLeft.CPoint( 0 ) == start );
}
if( aRight.CPoint( 0 ) != start )
{
aRight = aRight.Reverse();
wxASSERT( aRight.CPoint( 0 ) == start );
}
SEG base( CPoint( 0 ), CPoint( 1 ) );
int sideLeft = base.Side( aLeft.CPoint( 1 ) );
int sideRight = base.Side( aRight.CPoint( 1 ) );
if( sideLeft == 0 || sideRight == 0 )
return false;
if( sideLeft == sideRight )
return false;
if( sideLeft > 0 && sideRight < 0 )
std::swap( aLeft, aRight );
if( aLeft.PointCount() < 4 )
return false;
if( aRight.PointCount() < 4 )
return false;
aLeft.Remove( 0 );
aLeft.Remove( aLeft.PointCount() - 1 );
aRight.Remove( 0 );
aRight.Remove( aRight.PointCount() - 1 );
return true;
}
void SHAPE_LINE_CHAIN::TransformToPolygon( SHAPE_POLY_SET& aBuffer, int aError,
ERROR_LOC aErrorLoc ) const
{

View File

@ -1235,6 +1235,103 @@ void SHAPE_POLY_SET::inflate2( int aAmount, int aCircleSegCount, CORNER_STRATEGY
}
void SHAPE_POLY_SET::inflateLine2( const SHAPE_LINE_CHAIN& aLine, int aAmount, int aCircleSegCount,
CORNER_STRATEGY aCornerStrategy, bool aSimplify )
{
using namespace Clipper2Lib;
// A static table to avoid repetitive calculations of the coefficient
// 1.0 - cos( M_PI / aCircleSegCount )
// aCircleSegCount 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. 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 = JoinType::Round; // The way corners are offsetted
double miterLimit = 2.0; // Smaller value when using jtMiter for joinType
switch( aCornerStrategy )
{
case CORNER_STRATEGY::ALLOW_ACUTE_CORNERS:
joinType = JoinType::Miter;
miterLimit = 10; // Allows large spikes
break;
case CORNER_STRATEGY::CHAMFER_ACUTE_CORNERS: // Acute angles are chamfered
joinType = JoinType::Miter;
break;
case CORNER_STRATEGY::ROUND_ACUTE_CORNERS: // Acute angles are rounded
joinType = JoinType::Miter;
break;
case CORNER_STRATEGY::CHAMFER_ALL_CORNERS: // All angles are chamfered.
joinType = JoinType::Square;
break;
case CORNER_STRATEGY::ROUND_ALL_CORNERS: // All angles are rounded.
joinType = JoinType::Round;
break;
}
std::vector<CLIPPER_Z_VALUE> zValues;
std::vector<SHAPE_ARC> arcBuffer;
Path64 path = aLine.convertToClipper2( true, zValues, arcBuffer );
c.AddPath( path, joinType, EndType::Butt );
// 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( aCircleSegCount < 6 ) // avoid incorrect aCircleSegCount values
aCircleSegCount = 6;
double coeff;
if( aCircleSegCount > SEG_CNT_MAX || arc_tolerance_factor[aCircleSegCount] == 0 )
{
coeff = 1.0 - cos( M_PI / aCircleSegCount );
if( aCircleSegCount <= SEG_CNT_MAX )
arc_tolerance_factor[aCircleSegCount] = coeff;
}
else
{
coeff = arc_tolerance_factor[aCircleSegCount];
}
c.ArcTolerance( std::abs( aAmount ) * coeff );
c.MiterLimit( miterLimit );
PolyTree64 tree;
if( aSimplify )
{
Paths64 paths2;
c.Execute( aAmount, paths2 );
Clipper2Lib::SimplifyPaths( paths2, std::abs( aAmount ) * coeff, false );
Clipper64 c2;
c2.PreserveCollinear = false;
c2.ReverseSolution = false;
c2.AddSubject( paths2 );
c2.Execute( ClipType::Union, FillRule::Positive, tree );
}
else
{
c.Execute( aAmount, tree );
}
importTree( tree, zValues, arcBuffer );
tree.Clear();
}
void SHAPE_POLY_SET::Inflate( int aAmount, CORNER_STRATEGY aCornerStrategy, int aMaxError,
bool aSimplify )
{
@ -1247,6 +1344,15 @@ void SHAPE_POLY_SET::Inflate( int aAmount, CORNER_STRATEGY aCornerStrategy, int
}
void SHAPE_POLY_SET::OffsetLineChain( const SHAPE_LINE_CHAIN& aLine, int aAmount,
CORNER_STRATEGY aCornerStrategy, int aMaxError, bool aSimplify )
{
int segCount = GetArcToSegmentCount( std::abs( aAmount ), aMaxError, FULL_CIRCLE );
inflateLine2( aLine, aAmount, segCount, aCornerStrategy, aSimplify );
}
void SHAPE_POLY_SET::importTree( ClipperLib::PolyTree* tree,
const std::vector<CLIPPER_Z_VALUE>& aZValueBuffer,
const std::vector<SHAPE_ARC>& aArcBuffer )