From 27add591ecdba3de7f5a29d14b0c17279729638b Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Wed, 19 Oct 2022 16:25:45 -0700 Subject: [PATCH] Add Clipper2 Currently this lives behind the advanced config flag `UseClipper2`. Enabling this flag will route all Clipper-based calls through the Clipper2 library instead of the older Clipper. The changes should be mostly transparent. Of note, Clipper2 does not utilize the `STRICTLY_SIMPLE` flag because clipper1 did not actually guarantee a strictly simple polygon. Currently we ignore this flag but we may decide to run strictly-simple operations through a second NULL union to simplify the results as much as possible. Additionally, the inflation options are slightly different. We cannot choose the fallback miter. The fallback miter is always square. This only affects the CHAMFER_ACUTE_CORNERS option in inflate, which does not appear to be used. Lastly, we currently utilize the 64-bit integer coordinates for calculations. This appears to still be faster than 32-bit calculations in Clipper1 on a modern x86 system. This may not be the case for older systems, particularly 32-bit systems. --- common/advanced_config.cpp | 7 + common/settings/common_settings.cpp | 5 +- include/advanced_config.h | 6 + libs/kimath/CMakeLists.txt | 1 + .../include/geometry/shape_line_chain.h | 12 + libs/kimath/include/geometry/shape_poly_set.h | 15 + libs/kimath/src/geometry/shape_line_chain.cpp | 81 + libs/kimath/src/geometry/shape_poly_set.cpp | 299 +- qa/unittests/libs/kimath/CMakeLists.txt | 4 + thirdparty/CMakeLists.txt | 1 + thirdparty/clipper2/CMakeLists.txt | 33 + .../include/clipper2/clipper.core.h | 632 +++ .../include/clipper2/clipper.engine.h | 539 +++ .../Clipper2Lib/include/clipper2/clipper.h | 672 ++++ .../include/clipper2/clipper.minkowski.h | 118 + .../include/clipper2/clipper.offset.h | 107 + .../include/clipper2/clipper.rectclip.h | 46 + .../Clipper2Lib/src/clipper.engine.cpp | 3496 +++++++++++++++++ .../Clipper2Lib/src/clipper.offset.cpp | 485 +++ .../Clipper2Lib/src/clipper.rectclip.cpp | 480 +++ thirdparty/clipper2/LICENSE | 23 + 21 files changed, 7053 insertions(+), 9 deletions(-) create mode 100644 thirdparty/clipper2/CMakeLists.txt create mode 100644 thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.core.h create mode 100644 thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h create mode 100644 thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.h create mode 100644 thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h create mode 100644 thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h create mode 100644 thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h create mode 100644 thirdparty/clipper2/Clipper2Lib/src/clipper.engine.cpp create mode 100644 thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp create mode 100644 thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp create mode 100644 thirdparty/clipper2/LICENSE diff --git a/common/advanced_config.cpp b/common/advanced_config.cpp index 4fcd6a59de..466081360b 100644 --- a/common/advanced_config.cpp +++ b/common/advanced_config.cpp @@ -196,6 +196,8 @@ static const wxChar ShowPropertiesPanel[] = wxT( "ShowPropertiesPanel" ); static const wxChar V3DRT_BevelHeight_um[] = wxT( "V3DRT_BevelHeight_um" ); static const wxChar V3DRT_BevelExtentFactor[] = wxT( "V3DRT_BevelExtentFactor" ); + +static const wxChar UseClipper2[] = wxT( "UseClipper2" ); } // namespace KEYS @@ -323,6 +325,8 @@ ADVANCED_CFG::ADVANCED_CFG() m_3DRT_BevelHeight_um = 30; m_3DRT_BevelExtentFactor = 1.0 / 16.0; + m_UseClipper2 = false; + loadFromConfigFile(); } @@ -462,6 +466,9 @@ void ADVANCED_CFG::loadSettings( wxConfigBase& aCfg ) 0.0, 100.0, AC_GROUPS::V3D_RayTracing ) ); + configParams.push_back( new PARAM_CFG_BOOL( true, AC_KEYS::UseClipper2, + &m_UseClipper2, m_UseClipper2 ) ); + // Special case for trace mask setting...we just grab them and set them immediately diff --git a/common/settings/common_settings.cpp b/common/settings/common_settings.cpp index b9361aadb4..3ea61e960c 100644 --- a/common/settings/common_settings.cpp +++ b/common/settings/common_settings.cpp @@ -289,6 +289,9 @@ COMMON_SETTINGS::COMMON_SETTINGS() : m_params.emplace_back( new PARAM( "graphics.cairo_antialiasing_mode", &m_Graphics.cairo_aa_mode, 0, 0, 2 ) ); + m_params.emplace_back( new PARAM( "graphics.use_clipper2", + &m_Graphics.cairo_aa_mode, 0, 0, 1 ) ); + m_params.emplace_back( new PARAM( "system.autosave_interval", &m_System.autosave_interval, 600 ) ); @@ -768,4 +771,4 @@ bool COMMON_SETTINGS::getLegacy3DHollerith( const std::string& aString, size_t& aIndex = i2 + 1; return true; -} \ No newline at end of file +} diff --git a/include/advanced_config.h b/include/advanced_config.h index de30b9a83b..4c679e7c7b 100644 --- a/include/advanced_config.h +++ b/include/advanced_config.h @@ -238,6 +238,12 @@ public: */ double m_3DRT_BevelExtentFactor; + /** + * User Clipper2 instead of Clipper1 + */ + bool m_UseClipper2; + + private: ADVANCED_CFG(); diff --git a/libs/kimath/CMakeLists.txt b/libs/kimath/CMakeLists.txt index ea4e12d043..5e99e1725a 100644 --- a/libs/kimath/CMakeLists.txt +++ b/libs/kimath/CMakeLists.txt @@ -32,6 +32,7 @@ add_library( kimath STATIC target_link_libraries( kimath clipper + clipper2 othermath rtree ${wxWidgets_LIBRARIES} # wxLogDebug, wxASSERT diff --git a/libs/kimath/include/geometry/shape_line_chain.h b/libs/kimath/include/geometry/shape_line_chain.h index eb159ed4f8..c316cf2383 100644 --- a/libs/kimath/include/geometry/shape_line_chain.h +++ b/libs/kimath/include/geometry/shape_line_chain.h @@ -28,6 +28,7 @@ #include +#include #include #include #include @@ -195,6 +196,10 @@ public: const std::vector& aZValueBuffer, const std::vector& aArcBuffer ); + SHAPE_LINE_CHAIN( const Clipper2Lib::Path64& aPath, + const std::vector& aZValueBuffer, + const std::vector& aArcBuffer ); + virtual ~SHAPE_LINE_CHAIN() {} @@ -913,6 +918,13 @@ protected: std::vector& aZValueBuffer, std::vector& aArcBuffer ) const; + /** + * Create a new Clipper2 path from the SHAPE_LINE_CHAIN in a given orientation + */ + Clipper2Lib::Path64 convertToClipper2( bool aRequiredOrientation, + std::vector &aZValueBuffer, + std::vector &aArcBuffer ) const; + /** * Fix indices of this chain to ensure arcs are not split between the end and start indices */ diff --git a/libs/kimath/include/geometry/shape_poly_set.h b/libs/kimath/include/geometry/shape_poly_set.h index 7e2c695d47..10ae9554bb 100644 --- a/libs/kimath/include/geometry/shape_poly_set.h +++ b/libs/kimath/include/geometry/shape_poly_set.h @@ -39,6 +39,7 @@ #include #include // for ClipType, PolyTree (ptr only) +#include #include // for SEG #include #include @@ -1387,6 +1388,15 @@ private: void importTree( ClipperLib::PolyTree* tree, const std::vector& aZValueBuffer, const std::vector& aArcBuffe ); + void importTree( Clipper2Lib::PolyTree64& tree, + const std::vector& aZValueBuffer, + const std::vector& aArcBuffe ); + void importTree( Clipper2Lib::Paths64& paths, + const std::vector& aZValueBuffer, + const std::vector& aArcBuffe ); + + void inflate1( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy ); + void inflate2( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy ); /** * This is the engine to execute all polygon boolean transforms (AND, OR, ... and polygon @@ -1406,6 +1416,11 @@ private: void booleanOp( ClipperLib::ClipType aType, const SHAPE_POLY_SET& aShape, const SHAPE_POLY_SET& aOtherShape, POLYGON_MODE aFastMode ); + void booleanOp( Clipper2Lib::ClipType aType, const SHAPE_POLY_SET& aOtherShape ); + + void booleanOp( Clipper2Lib::ClipType aType, const SHAPE_POLY_SET& aShape, + const SHAPE_POLY_SET& aOtherShape ); + /** * Check whether the point \a aP is inside the \a aSubpolyIndex-th polygon of the polyset. If * the points lies on an edge, the polygon is considered to contain it. diff --git a/libs/kimath/src/geometry/shape_line_chain.cpp b/libs/kimath/src/geometry/shape_line_chain.cpp index 5422bb7f58..41e1d32ed7 100644 --- a/libs/kimath/src/geometry/shape_line_chain.cpp +++ b/libs/kimath/src/geometry/shape_line_chain.cpp @@ -30,6 +30,7 @@ #include // for basic_string #include +#include #include // for alg::run_on_pair #include // for SEG, OPT_VECTOR2I #include @@ -52,6 +53,7 @@ SHAPE_LINE_CHAIN::SHAPE_LINE_CHAIN( const std::vector& aV) } } + SHAPE_LINE_CHAIN::SHAPE_LINE_CHAIN( const ClipperLib::Path& aPath, const std::vector& aZValueBuffer, const std::vector& aArcBuffer ) : @@ -95,6 +97,51 @@ SHAPE_LINE_CHAIN::SHAPE_LINE_CHAIN( const ClipperLib::Path& aPath, fixIndicesRotation(); } + +SHAPE_LINE_CHAIN::SHAPE_LINE_CHAIN( const Clipper2Lib::Path64& aPath, + const std::vector& aZValueBuffer, + const std::vector& aArcBuffer ) : + SHAPE_LINE_CHAIN_BASE( SH_LINE_CHAIN ), + m_closed( true ), m_width( 0 ) +{ + std::map loadedArcs; + m_points.reserve( aPath.size() ); + m_shapes.reserve( aPath.size() ); + + auto loadArc = + [&]( ssize_t aArcIndex ) -> ssize_t + { + if( aArcIndex == SHAPE_IS_PT ) + { + return SHAPE_IS_PT; + } + else if( loadedArcs.count( aArcIndex ) == 0 ) + { + loadedArcs.insert( { aArcIndex, m_arcs.size() } ); + m_arcs.push_back( aArcBuffer.at( aArcIndex ) ); + } + + return loadedArcs.at( aArcIndex ); + }; + + for( size_t ii = 0; ii < aPath.size(); ++ii ) + { + Append( aPath[ii].x, aPath[ii].y ); + + m_shapes[ii].first = loadArc( aZValueBuffer[aPath[ii].z].m_FirstArcIdx ); + m_shapes[ii].second = loadArc( aZValueBuffer[aPath[ii].z].m_SecondArcIdx ); + } + + // Clipper shouldn't return duplicate contiguous points. if it did, these would be + // removed during Append() and we would have different number of shapes to points + wxASSERT( m_shapes.size() == m_points.size() ); + + // Clipper might mess up the rotation of the indices such that an arc can be split between + // the end point and wrap around to the start point. Lets fix the indices up now + fixIndicesRotation(); +} + + ClipperLib::Path SHAPE_LINE_CHAIN::convertToClipper( bool aRequiredOrientation, std::vector& aZValueBuffer, std::vector& aArcBuffer ) const @@ -129,6 +176,40 @@ ClipperLib::Path SHAPE_LINE_CHAIN::convertToClipper( bool aRequiredOrientation, } +Clipper2Lib::Path64 SHAPE_LINE_CHAIN::convertToClipper2( bool aRequiredOrientation, + std::vector& aZValueBuffer, + std::vector& aArcBuffer ) const +{ + Clipper2Lib::Path64 c_path; + SHAPE_LINE_CHAIN input; + bool orientation = Area( false ) >= 0; + ssize_t shape_offset = aArcBuffer.size(); + + if( orientation != aRequiredOrientation ) + input = Reverse(); + else + input = *this; + + int pointCount = input.PointCount(); + c_path.reserve( pointCount ); + + for( int i = 0; i < pointCount; i++ ) + { + const VECTOR2I& vertex = input.CPoint( i ); + + CLIPPER_Z_VALUE z_value( input.m_shapes[i], shape_offset ); + size_t z_value_ptr = aZValueBuffer.size(); + aZValueBuffer.push_back( z_value ); + + c_path.emplace_back( vertex.x, vertex.y, z_value_ptr ); + } + + aArcBuffer.insert( aArcBuffer.end(), input.m_arcs.begin(), input.m_arcs.end() ); + + return c_path; +} + + void SHAPE_LINE_CHAIN::fixIndicesRotation() { wxCHECK( m_shapes.size() == m_points.size(), /*void*/ ); diff --git a/libs/kimath/src/geometry/shape_poly_set.cpp b/libs/kimath/src/geometry/shape_poly_set.cpp index 2b8cb35569..bf4e62c4f2 100644 --- a/libs/kimath/src/geometry/shape_poly_set.cpp +++ b/libs/kimath/src/geometry/shape_poly_set.cpp @@ -43,6 +43,7 @@ #include #include // for Clipper, PolyNode, Clipp... +#include #include #include #include // for SEG, OPT_VECTOR2I @@ -57,6 +58,9 @@ #include #include +// Do not keep this for release. Only for testing clipper +#include + #include @@ -692,42 +696,173 @@ void SHAPE_POLY_SET::booleanOp( ClipperLib::ClipType aType, const SHAPE_POLY_SET } +void SHAPE_POLY_SET::booleanOp( Clipper2Lib::ClipType aType, const SHAPE_POLY_SET& aOtherShape ) +{ + booleanOp( aType, *this, aOtherShape ); +} + + +void SHAPE_POLY_SET::booleanOp( Clipper2Lib::ClipType aType, const SHAPE_POLY_SET& aShape, + const SHAPE_POLY_SET& aOtherShape ) +{ + if( ( aShape.OutlineCount() > 1 || aOtherShape.OutlineCount() > 0 ) + && ( aShape.ArcCount() > 0 || aOtherShape.ArcCount() > 0 ) ) + { + wxFAIL_MSG( wxT( "Boolean ops on curved polygons are not supported. You should call " + "ClearArcs() before carrying out the boolean operation." ) ); + } + + Clipper2Lib::Clipper64 c; + + std::vector zValues; + std::vector arcBuffer; + std::map newIntersectPoints; + + Clipper2Lib::Paths64 paths; + Clipper2Lib::Paths64 clips; + + for( const POLYGON& poly : aShape.m_polys ) + { + for( size_t i = 0; i < poly.size(); i++ ) + { + paths.push_back( poly[i].convertToClipper2( i == 0, zValues, arcBuffer ) ); + } + } + + for( const POLYGON& poly : aOtherShape.m_polys ) + { + for( size_t i = 0; i < poly.size(); i++ ) + { + clips.push_back( poly[i].convertToClipper2( i == 0, zValues, arcBuffer ) ); + } + } + + c.AddSubject( paths ); + c.AddClip( clips ); + + Clipper2Lib::PolyTree64 solution; + + Clipper2Lib::ZCallback64 callback = + [&]( const Clipper2Lib::Point64 & e1bot, const Clipper2Lib::Point64 & e1top, + const Clipper2Lib::Point64 & e2bot, const Clipper2Lib::Point64 & e2top, + Clipper2Lib::Point64 & pt ) + { + auto arcIndex = + [&]( const ssize_t& aZvalue, const ssize_t& aCompareVal = -1 ) -> ssize_t + { + ssize_t retval; + + retval = zValues.at( aZvalue ).m_SecondArcIdx; + + if( retval == -1 || ( aCompareVal > 0 && retval != aCompareVal ) ) + retval = zValues.at( aZvalue ).m_FirstArcIdx; + + return retval; + }; + + auto arcSegment = + [&]( const ssize_t& aBottomZ, const ssize_t aTopZ ) -> ssize_t + { + ssize_t retval = arcIndex( aBottomZ ); + + if( retval != -1 ) + { + if( retval != arcIndex( aTopZ, retval ) ) + retval = -1; // Not an arc segment as the two indices do not match + } + + return retval; + }; + + ssize_t e1ArcSegmentIndex = arcSegment( e1bot.z, e1top.z ); + ssize_t e2ArcSegmentIndex = arcSegment( e2bot.z, e2top.z ); + + CLIPPER_Z_VALUE newZval; + + if( e1ArcSegmentIndex != -1 ) + { + newZval.m_FirstArcIdx = e1ArcSegmentIndex; + newZval.m_SecondArcIdx = e2ArcSegmentIndex; + } + else + { + newZval.m_FirstArcIdx = e2ArcSegmentIndex; + newZval.m_SecondArcIdx = -1; + } + + size_t z_value_ptr = zValues.size(); + zValues.push_back( newZval ); + + // Only worry about arc segments for later processing + if( newZval.m_FirstArcIdx != -1 ) + newIntersectPoints.insert( { VECTOR2I( pt.x, pt.y ), newZval } ); + + pt.z = z_value_ptr; + //@todo amend X,Y values to true intersection between arcs or arc and segment + }; + + c.SetZCallback( callback ); // register callback + + c.Execute( aType, Clipper2Lib::FillRule::NonZero, solution ); + + importTree( solution, zValues, arcBuffer ); +} + + void SHAPE_POLY_SET::BooleanAdd( const SHAPE_POLY_SET& b, POLYGON_MODE aFastMode ) { - booleanOp( ClipperLib::ctUnion, b, aFastMode ); + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + booleanOp( Clipper2Lib::ClipType::Union, b ); + else + booleanOp( ClipperLib::ctUnion, b, aFastMode ); } void SHAPE_POLY_SET::BooleanSubtract( const SHAPE_POLY_SET& b, POLYGON_MODE aFastMode ) { - booleanOp( ClipperLib::ctDifference, b, aFastMode ); + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + booleanOp( Clipper2Lib::ClipType::Difference, b ); + else + booleanOp( ClipperLib::ctDifference, b, aFastMode ); } void SHAPE_POLY_SET::BooleanIntersection( const SHAPE_POLY_SET& b, POLYGON_MODE aFastMode ) { - booleanOp( ClipperLib::ctIntersection, b, aFastMode ); + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + booleanOp( Clipper2Lib::ClipType::Intersection, b ); + else + booleanOp( ClipperLib::ctIntersection, b, aFastMode ); } void SHAPE_POLY_SET::BooleanAdd( const SHAPE_POLY_SET& a, const SHAPE_POLY_SET& b, POLYGON_MODE aFastMode ) { - booleanOp( ClipperLib::ctUnion, a, b, aFastMode ); + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + booleanOp( Clipper2Lib::ClipType::Union, a, b ); + else + booleanOp( ClipperLib::ctUnion, a, b, aFastMode ); } void SHAPE_POLY_SET::BooleanSubtract( const SHAPE_POLY_SET& a, const SHAPE_POLY_SET& b, POLYGON_MODE aFastMode ) { - booleanOp( ClipperLib::ctDifference, a, b, aFastMode ); + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + booleanOp( Clipper2Lib::ClipType::Difference, a, b ); + else + booleanOp( ClipperLib::ctDifference, a, b, aFastMode ); } void SHAPE_POLY_SET::BooleanIntersection( const SHAPE_POLY_SET& a, const SHAPE_POLY_SET& b, POLYGON_MODE aFastMode ) { - booleanOp( ClipperLib::ctIntersection, a, b, aFastMode ); + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + booleanOp( Clipper2Lib::ClipType::Intersection, a, b ); + else + booleanOp( ClipperLib::ctIntersection, a, b, aFastMode ); } @@ -740,7 +875,7 @@ void SHAPE_POLY_SET::InflateWithLinkedHoles( int aFactor, int aCircleSegmentsCou } -void SHAPE_POLY_SET::Inflate( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy ) +void SHAPE_POLY_SET::inflate1( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy ) { using namespace ClipperLib; // A static table to avoid repetitive calculations of the coefficient @@ -831,6 +966,98 @@ void SHAPE_POLY_SET::Inflate( int aAmount, int aCircleSegCount, CORNER_STRATEGY } +void SHAPE_POLY_SET::inflate2( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy ) +{ + 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 + JoinType miterFallback = JoinType::Square; + + switch( aCornerStrategy ) + { + case ALLOW_ACUTE_CORNERS: + joinType = JoinType::Miter; + miterLimit = 10; // Allows large spikes + break; + + case CHAMFER_ACUTE_CORNERS: // Acute angles are chamfered + joinType = JoinType::Miter; + break; + + case ROUND_ACUTE_CORNERS: // Acute angles are rounded + joinType = JoinType::Miter; + break; + + case CHAMFER_ALL_CORNERS: // All angles are chamfered. + joinType = JoinType::Square; + break; + + case ROUND_ALL_CORNERS: // All angles are rounded. + joinType = JoinType::Round; + break; + } + + std::vector zValues; + std::vector arcBuffer; + + for( const POLYGON& poly : m_polys ) + { + for( size_t i = 0; i < poly.size(); i++ ) + { + c.AddPath( poly[i].convertToClipper2( i == 0, zValues, arcBuffer ), + joinType, EndType::Polygon ); + } + } + + // 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 ); + Paths64 solution = c.Execute( aAmount ); + + importTree( solution, zValues, arcBuffer ); +} + + +void SHAPE_POLY_SET::Inflate( int aAmount, int aCircleSegCount, CORNER_STRATEGY aCornerStrategy ) +{ + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + inflate2( aAmount, aCircleSegCount, aCornerStrategy ); + else + inflate1( aAmount, aCircleSegCount, aCornerStrategy ); +} + + void SHAPE_POLY_SET::importTree( ClipperLib::PolyTree* tree, const std::vector& aZValueBuffer, const std::vector& aArcBuffer ) @@ -855,6 +1082,59 @@ void SHAPE_POLY_SET::importTree( ClipperLib::PolyTree* tree, } +void SHAPE_POLY_SET::importTree( Clipper2Lib::PolyTree64& tree, + const std::vector& aZValueBuffer, + const std::vector& aArcBuffer ) +{ + m_polys.clear(); + + for( Clipper2Lib::PolyPath64* n : tree ) + { + if( !n->IsHole() ) + { + POLYGON paths; + paths.reserve( n->Count() + 1 ); + + paths.emplace_back( n->Polygon(), aZValueBuffer, aArcBuffer ); + + for( Clipper2Lib::PolyPath64* child : *n) + paths.emplace_back( child->Polygon(), aZValueBuffer, aArcBuffer ); + + m_polys.push_back( paths ); + } + } +} + + +void SHAPE_POLY_SET::importTree( Clipper2Lib::Paths64& tree, + const std::vector& aZValueBuffer, + const std::vector& aArcBuffer ) +{ + m_polys.clear(); + POLYGON path; + + for( const Clipper2Lib::Path64& n : tree ) + { + if( Clipper2Lib::Area( n ) > 0 ) + { + if( !path.empty() ) + m_polys.emplace_back( path ); + + path.clear(); + } + else + { + wxCHECK2_MSG( !path.empty(), continue, wxT( "Cannot add a hole before an outline" ) ); + } + + path.emplace_back( n, aZValueBuffer, aArcBuffer ); + } + + if( !path.empty() ) + m_polys.emplace_back( path ); +} + + struct FractureEdge { FractureEdge( int y = 0 ) : @@ -1281,7 +1561,10 @@ void SHAPE_POLY_SET::Simplify( POLYGON_MODE aFastMode ) { SHAPE_POLY_SET empty; - booleanOp( ClipperLib::ctUnion, empty, aFastMode ); + if( ADVANCED_CFG::GetCfg().m_UseClipper2 ) + booleanOp( Clipper2Lib::ClipType::Union, empty ); + else + booleanOp( ClipperLib::ctUnion, empty, aFastMode ); } diff --git a/qa/unittests/libs/kimath/CMakeLists.txt b/qa/unittests/libs/kimath/CMakeLists.txt index f17f270782..43e030b6cf 100644 --- a/qa/unittests/libs/kimath/CMakeLists.txt +++ b/qa/unittests/libs/kimath/CMakeLists.txt @@ -59,6 +59,9 @@ endif() add_executable( qa_kimath ${QA_KIMATH_SRCS} ${QA_KIMATH_RESOURCES} + + # Mock Pgm needed for advanced_config + ${CMAKE_SOURCE_DIR}/qa/mocks/kicad/common_mocks.cpp ) target_link_libraries( qa_kimath @@ -69,6 +72,7 @@ target_link_libraries( qa_kimath target_include_directories( qa_kimath PRIVATE ${CMAKE_SOURCE_DIR}/include # Needed for core/optional.h + ${CMAKE_SOURCE_DIR}/qa/mocks/include ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 3a74883cf7..bf255fdb70 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory( argparse ) add_subdirectory( clipper ) +add_subdirectory( clipper2 ) add_subdirectory( compoundfilereader ) add_subdirectory( delaunator ) add_subdirectory( dxflib_qcad ) diff --git a/thirdparty/clipper2/CMakeLists.txt b/thirdparty/clipper2/CMakeLists.txt new file mode 100644 index 0000000000..304b9adb66 --- /dev/null +++ b/thirdparty/clipper2/CMakeLists.txt @@ -0,0 +1,33 @@ +include(GNUInstallDirs) + +set(CLIPPER2_INC + Clipper2Lib/include/clipper2/clipper.h + Clipper2Lib/include/clipper2/clipper.core.h + Clipper2Lib/include/clipper2/clipper.engine.h + Clipper2Lib/include/clipper2/clipper.minkowski.h + Clipper2Lib/include/clipper2/clipper.offset.h + Clipper2Lib/include/clipper2/clipper.rectclip.h +) + +set(CLIPPER2_SRC + Clipper2Lib/src/clipper.engine.cpp + Clipper2Lib/src/clipper.offset.cpp + Clipper2Lib/src/clipper.rectclip.cpp +) + + +add_library(clipper2 STATIC ${CLIPPER2_INC} ${CLIPPER2_SRC}) + +target_include_directories(clipper2 + PUBLIC Clipper2Lib/include +) + +target_compile_definitions(clipper2 PUBLIC USINGZ) + +if (WIN32) + target_compile_options(clipper2 PRIVATE /W4 /WX) +else() + target_compile_options(clipper2 PRIVATE -Wall -Wextra -Wpedantic -Werror) + target_link_libraries(clipper2 PUBLIC -lm) +endif() + diff --git a/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.core.h b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.core.h new file mode 100644 index 0000000000..0f41529622 --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.core.h @@ -0,0 +1,632 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : Core Clipper Library structures and functions * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef CLIPPER_CORE_H +#define CLIPPER_CORE_H + +#include +#include +#include +#include +#include +#include + +namespace Clipper2Lib +{ + + static double const PI = 3.141592653589793238; + + //By far the most widely used filling rules for polygons are EvenOdd + //and NonZero, sometimes called Alternate and Winding respectively. + //https://en.wikipedia.org/wiki/Nonzero-rule + enum class FillRule { EvenOdd, NonZero, Positive, Negative }; + +// Point ------------------------------------------------------------------------ + +template +struct Point { + T x; + T y; +#ifdef USINGZ + int64_t z; + + template + inline void Init(const T2 x_ = 0, const T2 y_ = 0, const int64_t z_ = 0) + { + if constexpr (std::numeric_limits::is_integer && + !std::numeric_limits::is_integer) + { + x = static_cast(std::round(x_)); + y = static_cast(std::round(y_)); + z = z_; + } + else + { + x = static_cast(x_); + y = static_cast(y_); + z = z_; + } + } + + explicit Point() : x(0), y(0), z(0) {}; + + template + Point(const T2 x_, const T2 y_, const int64_t z_ = 0) + { + Init(x_, y_); + z = z_; + } + + template + explicit Point(const Point& p) + { + Init(p.x, p.y, p.z); + } + + Point operator * (const double scale) const + { + return Point(x * scale, y * scale, z); + } + + + friend std::ostream& operator<<(std::ostream& os, const Point& point) + { + os << point.x << "," << point.y << "," << point.z; + return os; + } + +#else + + template + inline void Init(const T2 x_ = 0, const T2 y_ = 0) + { + if constexpr (std::numeric_limits::is_integer && + !std::numeric_limits::is_integer) + { + x = static_cast(std::round(x_)); + y = static_cast(std::round(y_)); + } + else + { + x = static_cast(x_); + y = static_cast(y_); + } + } + + explicit Point() : x(0), y(0) {}; + + template + Point(const T2 x_, const T2 y_) { Init(x_, y_); } + + template + explicit Point(const Point& p) { Init(p.x, p.y); } + + Point operator * (const double scale) const + { + return Point(x * scale, y * scale); + } + + friend std::ostream& operator<<(std::ostream& os, const Point& point) + { + os << point.x << "," << point.y; + return os; + } +#endif + + friend bool operator==(const Point &a, const Point &b) + { + return a.x == b.x && a.y == b.y; + } + + friend bool operator!=(const Point& a, const Point& b) + { + return !(a == b); + } + + inline Point operator-() const + { + return Point(-x,-y); + } + + inline Point operator+(const Point &b) const + { + return Point(x+b.x, y+b.y); + } + + inline Point operator-(const Point &b) const + { + return Point(x-b.x, y-b.y); + } + + inline void Negate() { x = -x; y = -y; } + +}; + +//nb: using 'using' here (instead of typedef) as they can be used in templates +using Point64 = Point; +using PointD = Point; + +template +using Path = std::vector>; +template +using Paths = std::vector>; + +using Path64 = Path; +using PathD = Path; +using Paths64 = std::vector< Path64>; +using PathsD = std::vector< PathD>; + +template +std::ostream& operator << (std::ostream& outstream, const Path& path) +{ + if (!path.empty()) + { + auto pt = path.cbegin(), last = path.cend() - 1; + while (pt != last) + outstream << *pt++ << ", "; + outstream << *last << std::endl; + } + return outstream; +} + +template +std::ostream& operator << (std::ostream& outstream, const Paths& paths) +{ + for (auto p : paths) + outstream << p; + return outstream; +} + +template +inline Path ScalePath(const Path& path, double scale) +{ + Path result; + result.reserve(path.size()); +#ifdef USINGZ + for (const Point& pt : path) + result.push_back(Point(pt.x * scale, pt.y * scale, pt.z)); +#else + for (const Point& pt : path) + result.push_back(Point(pt.x * scale, pt.y * scale)); +#endif + return result; +} + +template +inline Paths ScalePaths(const Paths& paths, double scale) +{ + Paths result; + result.reserve(paths.size()); + for (const Path& path : paths) + result.push_back(ScalePath(path, scale)); + return result; +} + +template +inline Path TransformPath(const Path& path) +{ + Path result; + result.reserve(path.size()); + std::transform(path.cbegin(), path.cend(), std::back_inserter(result), + [](const Point& pt) {return Point(pt); }); + return result; +} + +template +inline Paths TransformPaths(const Paths& paths) +{ + Paths result; + std::transform(paths.cbegin(), paths.cend(), std::back_inserter(result), + [](const Path& path) {return TransformPath(path); }); + return result; +} + +inline PathD Path64ToPathD(const Path64& path) +{ + return TransformPath(path); +} + +inline PathsD Paths64ToPathsD(const Paths64& paths) +{ + return TransformPaths(paths); +} + +inline Path64 PathDToPath64(const PathD& path) +{ + return TransformPath(path); +} + +inline Paths64 PathsDToPaths64(const PathsD& paths) +{ + return TransformPaths(paths); +} + +template +inline double Sqr(T val) +{ + return static_cast(val) * static_cast(val); +} + +template +inline bool NearEqual(const Point& p1, + const Point& p2, double max_dist_sqrd) +{ + return Sqr(p1.x - p2.x) + Sqr(p1.y - p2.y) < max_dist_sqrd; +} + +template +inline Path StripNearEqual(const Path& path, + double max_dist_sqrd, bool is_closed_path) +{ + if (path.size() == 0) return Path(); + Path result; + result.reserve(path.size()); + typename Path::const_iterator path_iter = path.cbegin(); + Point first_pt = *path_iter++, last_pt = first_pt; + result.push_back(first_pt); + for (; path_iter != path.cend(); ++path_iter) + { + if (!NearEqual(*path_iter, last_pt, max_dist_sqrd)) + { + last_pt = *path_iter; + result.push_back(last_pt); + } + } + if (!is_closed_path) return result; + while (result.size() > 1 && + NearEqual(result.back(), first_pt, max_dist_sqrd)) result.pop_back(); + return result; +} + +template +inline Paths StripNearEqual(const Paths& paths, + double max_dist_sqrd, bool is_closed_path) +{ + Paths result; + result.reserve(paths.size()); + for (typename Paths::const_iterator paths_citer = paths.cbegin(); + paths_citer != paths.cend(); ++paths_citer) + { + result.push_back(StripNearEqual(*paths_citer, max_dist_sqrd, is_closed_path)); + } + return result; +} + +template +inline Path StripDuplicates(const Path& path, bool is_closed_path) +{ + if (path.size() == 0) return Path(); + Path result; + result.reserve(path.size()); + typename Path::const_iterator path_iter = path.cbegin(); + Point first_pt = *path_iter++, last_pt = first_pt; + result.push_back(first_pt); + for (; path_iter != path.cend(); ++path_iter) + { + if (*path_iter != last_pt) + { + last_pt = *path_iter; + result.push_back(last_pt); + } + } + if (!is_closed_path) return result; + while (result.size() > 1 && result.back() == first_pt) result.pop_back(); + return result; +} + +template +inline Paths StripDuplicates(const Paths& paths, bool is_closed_path) +{ + Paths result; + result.reserve(paths.size()); + for (typename Paths::const_iterator paths_citer = paths.cbegin(); + paths_citer != paths.cend(); ++paths_citer) + { + result.push_back(StripDuplicates(*paths_citer, is_closed_path)); + } + return result; +} + +// Rect ------------------------------------------------------------------------ + +template +struct Rect; + +using Rect64 = Rect; +using RectD = Rect; + +template +struct Rect { + T left; + T top; + T right; + T bottom; + + Rect() : + left(0), + top(0), + right(0), + bottom(0) {} + + Rect(T l, T t, T r, T b) : + left(l), + top(t), + right(r), + bottom(b) {} + + + T Width() const { return right - left; } + T Height() const { return bottom - top; } + void Width(T width) { right = left + width; } + void Height(T height) { bottom = top + height; } + + Point MidPoint() const + { + return Point((left + right) / 2, (top + bottom) / 2); + } + + Path AsPath() const + { + Path result; + result.reserve(4); + result.push_back(Point(left, top)); + result.push_back(Point(right, top)); + result.push_back(Point(right, bottom)); + result.push_back(Point(left, bottom)); + return result; + } + + bool Contains(const Point& pt) const + { + return pt.x > left && pt.x < right&& pt.y > top && pt.y < bottom; + } + + bool Contains(const Rect& rec) const + { + return rec.left >= left && rec.right <= right && + rec.top >= top && rec.bottom <= bottom; + } + + void Scale(double scale) { + left *= scale; + top *= scale; + right *= scale; + bottom *= scale; + } + + bool IsEmpty() const { return bottom <= top || right <= left; }; + + bool Intersects(const Rect& rec) const + { + return (std::max(left, rec.left) < std::min(right, rec.right)) && + (std::max(top, rec.top) < std::min(bottom, rec.bottom)); + }; + + friend std::ostream &operator<<(std::ostream &os, const Rect &rect) { + os << "(" + << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom + << ")"; + return os; + } +}; + +template +inline Rect ScaleRect(const Rect& rect, double scale) +{ + Rect result; + + if constexpr (std::numeric_limits::is_integer && + !std::numeric_limits::is_integer) + { + result.left = static_cast(std::round(rect.left * scale)); + result.top = static_cast(std::round(rect.top * scale)); + result.right = static_cast(std::round(rect.right * scale)); + result.bottom = static_cast(std::round(rect.bottom * scale)); + } + else + { + result.left = rect.left * scale; + result.top = rect.top * scale; + result.right = rect.right * scale; + result.bottom = rect.bottom * scale; + } + return result; +} + +// clipper2Exception --------------------------------------------------------- + +class Clipper2Exception : public std::exception { +public: + explicit Clipper2Exception(const char *description) : + m_descr(description) {} + virtual const char *what() const throw() override { return m_descr.c_str(); } + +private: + std::string m_descr; +}; + +// Miscellaneous ------------------------------------------------------------ + +template +inline double CrossProduct(const Point& pt1, const Point& pt2, const Point& pt3) { + return (static_cast(pt2.x - pt1.x) * static_cast(pt3.y - + pt2.y) - static_cast(pt2.y - pt1.y) * static_cast(pt3.x - pt2.x)); +} + +template +inline double CrossProduct(const Point& vec1, const Point& vec2) +{ + return static_cast(vec1.y * vec2.x) - static_cast(vec2.y * vec1.x); +} + +template +inline double DotProduct(const Point& pt1, const Point& pt2, const Point& pt3) { + return (static_cast(pt2.x - pt1.x) * static_cast(pt3.x - pt2.x) + + static_cast(pt2.y - pt1.y) * static_cast(pt3.y - pt2.y)); +} + +template +inline double DotProduct(const Point& vec1, const Point& vec2) +{ + return static_cast(vec1.x * vec2.x) + static_cast(vec1.y * vec2.y); +} + +template +inline double DistanceSqr(const Point pt1, const Point pt2) +{ + return Sqr(pt1.x - pt2.x) + Sqr(pt1.y - pt2.y); +} + +template +inline double DistanceFromLineSqrd(const Point& pt, const Point& ln1, const Point& ln2) +{ + //perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) + //see http://en.wikipedia.org/wiki/Perpendicular_distance + double A = static_cast(ln1.y - ln2.y); + double B = static_cast(ln2.x - ln1.x); + double C = A * ln1.x + B * ln1.y; + C = A * pt.x + B * pt.y - C; + return (C * C) / (A * A + B * B); +} + +template +inline double Area(const Path& path) +{ + size_t cnt = path.size(); + if (cnt < 3) return 0.0; + double a = 0.0; + typename Path::const_iterator it1, it2 = path.cend() - 1, stop = it2; + if (!(cnt & 1)) ++stop; + for (it1 = path.cbegin(); it1 != stop;) + { + a += static_cast(it2->y + it1->y) * (it2->x - it1->x); + it2 = it1 + 1; + a += static_cast(it1->y + it2->y) * (it1->x - it2->x); + it1 += 2; + } + if (cnt & 1) + a += static_cast(it2->y + it1->y) * (it2->x - it1->x); + return a * 0.5; +} + +template +inline double Area(const Paths& paths) +{ + double a = 0.0; + for (typename Paths::const_iterator paths_iter = paths.cbegin(); + paths_iter != paths.cend(); ++paths_iter) + { + a += Area(*paths_iter); + } + return a; +} + +template +inline bool IsPositive(const Path& poly) +{ + // A curve has positive orientation [and area] if a region 'R' + // is on the left when traveling around the outside of 'R'. + //https://mathworld.wolfram.com/CurveOrientation.html + //nb: This statement is premised on using Cartesian coordinates + return Area(poly) >= 0; +} + +inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, + const Point64& seg2a, const Point64& seg2b, bool inclusive = false) +{ + if (inclusive) + { + double res1 = CrossProduct(seg1a, seg2a, seg2b); + double res2 = CrossProduct(seg1b, seg2a, seg2b); + if (res1 * res2 > 0) return false; + double res3 = CrossProduct(seg2a, seg1a, seg1b); + double res4 = CrossProduct(seg2b, seg1a, seg1b); + if (res3 * res4 > 0) return false; + return (res1 || res2 || res3 || res4); // ensures not collinear + } + else { + double dx1 = static_cast(seg1a.x - seg1b.x); + double dy1 = static_cast(seg1a.y - seg1b.y); + double dx2 = static_cast(seg2a.x - seg2b.x); + double dy2 = static_cast(seg2a.y - seg2b.y); + return (((dy1 * (seg2a.x - seg1a.x) - dx1 * (seg2a.y - seg1a.y)) * + (dy1 * (seg2b.x - seg1a.x) - dx1 * (seg2b.y - seg1a.y)) < 0) && + ((dy2 * (seg1a.x - seg2a.x) - dx2 * (seg1a.y - seg2a.y)) * + (dy2 * (seg1b.x - seg2a.x) - dx2 * (seg1b.y - seg2a.y)) < 0)); + } +} + + +enum class PointInPolygonResult { IsOn, IsInside, IsOutside }; + +template +inline PointInPolygonResult PointInPolygon(const Point& pt, const Path& polygon) +{ + if (polygon.size() < 3) + return PointInPolygonResult::IsOutside; + + int val = 0; + typename Path::const_iterator start = polygon.cbegin(), cit = start; + typename Path::const_iterator cend = polygon.cend(), pit = cend - 1; + + while (pit->y == pt.y) + { + if (pit == start) return PointInPolygonResult::IsOutside; + --pit; + } + bool is_above = pit->y < pt.y; + + while (cit != cend) + { + if (is_above) + { + while (cit != cend && cit->y < pt.y) ++cit; + if (cit == cend) break; + } + else + { + while (cit != cend && cit->y > pt.y) ++cit; + if (cit == cend) break; + } + + if (cit == start) pit = cend - 1; + else pit = cit - 1; + + if (cit->y == pt.y) + { + if (cit->x == pt.x || (cit->y == pit->y && + ((pt.x < pit->x) != (pt.x < cit->x)))) + return PointInPolygonResult::IsOn; + ++cit; + continue; + } + + if (pt.x < cit->x && pt.x < pit->x) + { + // we're only interested in edges crossing on the left + } + else if (pt.x > pit->x && pt.x > cit->x) + val = 1 - val; // toggle val + else + { + double d = CrossProduct(*pit, *cit, pt); + if (d == 0) return PointInPolygonResult::IsOn; + if ((d < 0) == is_above) val = 1 - val; + } + is_above = !is_above; + ++cit; + } + return (val == 0) ? + PointInPolygonResult::IsOutside : + PointInPolygonResult::IsInside; +} + +} // namespace + +#endif // CLIPPER_CORE_H diff --git a/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h new file mode 100644 index 0000000000..ca4c51aa40 --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.engine.h @@ -0,0 +1,539 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : This is the main polygon clipping module * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef clipper_engine_h +#define clipper_engine_h + +#define CLIPPER2_VERSION "1.0.5" + +#include +#include +#include +#include +#include +#include "clipper.core.h" + +namespace Clipper2Lib { + + struct Scanline; + struct IntersectNode; + struct Active; + struct Vertex; + struct LocalMinima; + struct OutRec; + struct Joiner; + + //Note: all clipping operations except for Difference are commutative. + enum class ClipType { None, Intersection, Union, Difference, Xor }; + + enum class PathType { Subject, Clip }; + + enum class VertexFlags : uint32_t { + None = 0, OpenStart = 1, OpenEnd = 2, LocalMax = 4, LocalMin = 8 + }; + + constexpr enum VertexFlags operator &(enum VertexFlags a, enum VertexFlags b) + { + return (enum VertexFlags)(uint32_t(a) & uint32_t(b)); + } + + constexpr enum VertexFlags operator |(enum VertexFlags a, enum VertexFlags b) + { + return (enum VertexFlags)(uint32_t(a) | uint32_t(b)); + } + + struct Vertex { + Point64 pt; + Vertex* next = nullptr; + Vertex* prev = nullptr; + VertexFlags flags = VertexFlags::None; + }; + + struct OutPt { + Point64 pt; + OutPt* next = nullptr; + OutPt* prev = nullptr; + OutRec* outrec; + Joiner* joiner = nullptr; + + OutPt(const Point64& pt_, OutRec* outrec_): pt(pt_), outrec(outrec_) { + next = this; + prev = this; + } + }; + + template + class PolyPath; + + using PolyPath64 = PolyPath; + using PolyPathD = PolyPath; + + template + using PolyTree = PolyPath; + using PolyTree64 = PolyTree; + using PolyTreeD = PolyTree; + + struct OutRec; + typedef std::vector OutRecList; + + //OutRec: contains a path in the clipping solution. Edges in the AEL will + //have OutRec pointers assigned when they form part of the clipping solution. + struct OutRec { + size_t idx = 0; + OutRec* owner = nullptr; + OutRecList* splits = nullptr; + Active* front_edge = nullptr; + Active* back_edge = nullptr; + OutPt* pts = nullptr; + PolyPath64* polypath = nullptr; + Rect64 bounds = {}; + Path64 path; + bool is_open = false; + ~OutRec() { if (splits) delete splits; }; + }; + + /////////////////////////////////////////////////////////////////// + //Important: UP and DOWN here are premised on Y-axis positive down + //displays, which is the orientation used in Clipper's development. + /////////////////////////////////////////////////////////////////// + + struct Active { + Point64 bot; + Point64 top; + int64_t curr_x = 0; //current (updated at every new scanline) + double dx = 0.0; + int wind_dx = 1; //1 or -1 depending on winding direction + int wind_cnt = 0; + int wind_cnt2 = 0; //winding count of the opposite polytype + OutRec* outrec = nullptr; + //AEL: 'active edge list' (Vatti's AET - active edge table) + // a linked list of all edges (from left to right) that are present + // (or 'active') within the current scanbeam (a horizontal 'beam' that + // sweeps from bottom to top over the paths in the clipping operation). + Active* prev_in_ael = nullptr; + Active* next_in_ael = nullptr; + //SEL: 'sorted edge list' (Vatti's ST - sorted table) + // linked list used when sorting edges into their new positions at the + // top of scanbeams, but also (re)used to process horizontals. + Active* prev_in_sel = nullptr; + Active* next_in_sel = nullptr; + Active* jump = nullptr; + Vertex* vertex_top = nullptr; + LocalMinima* local_min = nullptr; // the bottom of an edge 'bound' (also Vatti) + bool is_left_bound = false; + }; + + struct LocalMinima { + Vertex* vertex; + PathType polytype; + bool is_open; + LocalMinima(Vertex* v, PathType pt, bool open) : + vertex(v), polytype(pt), is_open(open){} + }; + + struct IntersectNode { + Point64 pt; + Active* edge1; + Active* edge2; + IntersectNode() : pt(Point64(0, 0)), edge1(NULL), edge2(NULL) {} + IntersectNode(Active* e1, Active* e2, Point64& pt_) : + pt(pt_), edge1(e1), edge2(e2) + { + } + }; + +#ifdef USINGZ + typedef std::function ZCallback64; + + typedef std::function ZCallbackD; +#endif + + // ClipperBase ------------------------------------------------------------- + + class ClipperBase { + private: + ClipType cliptype_ = ClipType::None; + FillRule fillrule_ = FillRule::EvenOdd; + FillRule fillpos = FillRule::Positive; + int64_t bot_y_ = 0; + bool has_open_paths_ = false; + bool minima_list_sorted_ = false; + bool using_polytree_ = false; + bool succeeded_ = true; + Active* actives_ = nullptr; + Active *sel_ = nullptr; + Joiner *horz_joiners_ = nullptr; + std::vector minima_list_; //pointers in case of memory reallocs + std::vector::iterator current_locmin_iter_; + std::vector vertex_lists_; + std::priority_queue scanline_list_; + std::vector intersect_nodes_; + std::vector outrec_list_; //pointers in case of memory reallocs + std::vector joiner_list_; //pointers in case of memory reallocs + void Reset(); + void InsertScanline(int64_t y); + bool PopScanline(int64_t &y); + bool PopLocalMinima(int64_t y, LocalMinima *&local_minima); + void DisposeAllOutRecs(); + void DisposeVerticesAndLocalMinima(); + void DeleteEdges(Active*& e); + void AddLocMin(Vertex &vert, PathType polytype, bool is_open); + bool IsContributingClosed(const Active &e) const; + inline bool IsContributingOpen(const Active &e) const; + void SetWindCountForClosedPathEdge(Active &edge); + void SetWindCountForOpenPathEdge(Active &e); + void InsertLocalMinimaIntoAEL(int64_t bot_y); + void InsertLeftEdge(Active &e); + inline void PushHorz(Active &e); + inline bool PopHorz(Active *&e); + inline OutPt* StartOpenPath(Active &e, const Point64& pt); + inline void UpdateEdgeIntoAEL(Active *e); + OutPt* IntersectEdges(Active &e1, Active &e2, const Point64& pt); + inline void DeleteFromAEL(Active &e); + inline void AdjustCurrXAndCopyToSEL(const int64_t top_y); + void DoIntersections(const int64_t top_y); + void AddNewIntersectNode(Active &e1, Active &e2, const int64_t top_y); + bool BuildIntersectList(const int64_t top_y); + void ProcessIntersectList(); + void SwapPositionsInAEL(Active& edge1, Active& edge2); + OutPt* AddOutPt(const Active &e, const Point64& pt); + OutPt* AddLocalMinPoly(Active &e1, Active &e2, + const Point64& pt, bool is_new = false); + OutPt* AddLocalMaxPoly(Active &e1, Active &e2, const Point64& pt); + void DoHorizontal(Active &horz); + bool ResetHorzDirection(const Active &horz, const Active *max_pair, + int64_t &horz_left, int64_t &horz_right); + void DoTopOfScanbeam(const int64_t top_y); + Active *DoMaxima(Active &e); + void JoinOutrecPaths(Active &e1, Active &e2); + void CompleteSplit(OutPt* op1, OutPt* op2, OutRec& outrec); + bool ValidateClosedPathEx(OutPt*& outrec); + void CleanCollinear(OutRec* outrec); + void FixSelfIntersects(OutRec* outrec); + OutPt* DoSplitOp(OutPt* outRecOp, OutPt* splitOp); + Joiner* GetHorzTrialParent(const OutPt* op); + bool OutPtInTrialHorzList(OutPt* op); + void SafeDisposeOutPts(OutPt*& op); + void SafeDeleteOutPtJoiners(OutPt* op); + void AddTrialHorzJoin(OutPt* op); + void DeleteTrialHorzJoin(OutPt* op); + void ConvertHorzTrialsToJoins(); + void AddJoin(OutPt* op1, OutPt* op2); + void DeleteJoin(Joiner* joiner); + void ProcessJoinerList(); + OutRec* ProcessJoin(Joiner* joiner); + bool ExecuteInternal(ClipType ct, FillRule ft, bool use_polytrees); + bool DeepCheckOwner(OutRec* outrec, OutRec* owner); + void BuildPaths(Paths64& solutionClosed, Paths64* solutionOpen); + void BuildTree(PolyPath64& polytree, Paths64& open_paths); + protected: +#ifdef USINGZ + ZCallback64 zCallback_ = nullptr; + void SetZ(const Active& e1, const Active& e2, Point64& pt); +#endif + void CleanUp(); // unlike Clear, CleanUp preserves added paths + void AddPath(const Path64& path, PathType polytype, bool is_open); + void AddPaths(const Paths64& paths, PathType polytype, bool is_open); + + bool Execute(ClipType clip_type, + FillRule fill_rule, Paths64& solution_closed); + bool Execute(ClipType clip_type, + FillRule fill_rule, Paths64& solution_closed, Paths64& solution_open); + bool Execute(ClipType clip_type, FillRule fill_rule, PolyTree64& polytree); + bool Execute(ClipType clip_type, + FillRule fill_rule, PolyTree64& polytree, Paths64& open_paths); + public: + virtual ~ClipperBase(); + bool PreserveCollinear = true; + bool ReverseSolution = false; + void Clear(); + }; + + // PolyPath / PolyTree -------------------------------------------------------- + + //PolyTree: is intended as a READ-ONLY data structure for CLOSED paths returned + //by clipping operations. While this structure is more complex than the + //alternative Paths structure, it does preserve path 'ownership' - ie those + //paths that contain (or own) other paths. This will be useful to some users. + + template + class PolyPath final { + private: + double scale_; + Path polygon_; + std::vector childs_; + protected: + const PolyPath* parent_; + PolyPath(const PolyPath* parent, + const Path& path) : + scale_(parent->scale_), polygon_(path), parent_(parent){} + public: + + explicit PolyPath(int precision = 0) // NB only for root node + { + scale_ = std::pow(10, precision); + parent_ = nullptr; + } + + ~PolyPath() { Clear(); }; + + //https://en.cppreference.com/w/cpp/language/rule_of_three + PolyPath(const PolyPath&) = delete; + PolyPath& operator=(const PolyPath&) = delete; + + PolyPath* operator [] (size_t index) { return childs_[index]; } + + typename std::vector::const_iterator begin() const { return childs_.cbegin(); } + typename std::vector::const_iterator end() const { return childs_.cend(); } + + void Clear() { + for (PolyPath* child : childs_) delete child; + childs_.resize(0); + } + + unsigned Level() const + { + unsigned result = 0; + const PolyPath* p = parent_; + while (p) { ++result; p = p->parent_; } + return result; + } + + void reserve(size_t size) + { + if (size > childs_.size()) childs_.reserve(size); + } + + PolyPath* AddChild(const Path& path) + { + childs_.push_back(new PolyPath(this, path)); + return childs_.back(); + } + + size_t Count() const { return childs_.size(); } + + const PolyPath* parent() const { return parent_; } + + bool IsHole() const + { + const PolyPath* pp = parent_; + bool is_hole = pp; + while (pp) { + is_hole = !is_hole; + pp = pp->parent_; + } + return is_hole; + } + + const Path& Polygon() const { return polygon_; } + + double Area() const + { + double result = Clipper2Lib::Area(polygon_); + for (const PolyPath* child : childs_) + result += child->Area(); + return result; + } + + friend std::ostream& operator << (std::ostream& outstream, const PolyPath& polypath) + { + const unsigned level_indent = 4; + const unsigned coords_per_line = 4; + + unsigned level = polypath.Level(); + if (level > 0) + { + std::string level_padding; + level_padding.insert(0, (level -1) * level_indent, ' '); + std::string caption = polypath.IsHole() ? "Hole " : "Outer Polygon "; + std::string childs = polypath.Count() == 1 ? " child" : " children"; + outstream << level_padding.c_str() << caption << "with " << polypath.Count() << childs << std::endl; + int last_on_line = coords_per_line - 1; + outstream << level_padding; + int i = 0, highI = polypath.Polygon().size() - 1; + for (; i < highI; ++i) + { + outstream << polypath.Polygon()[i] << ' '; + if ((i % coords_per_line) == last_on_line) + outstream << std::endl << level_padding; + } + if (highI >= 0) + outstream << polypath.Polygon()[i]; + outstream << std::endl; + } + for (auto child : polypath) + outstream << *child; + return outstream; + } + + }; + + void Polytree64ToPolytreeD(const PolyPath64& polytree, PolyPathD& result); + + class Clipper64 : public ClipperBase + { + public: +#ifdef USINGZ + void SetZCallback(ZCallback64 cb) { zCallback_ = cb; } +#endif + + void AddSubject(const Paths64& subjects) + { + AddPaths(subjects, PathType::Subject, false); + } + void AddOpenSubject(const Paths64& open_subjects) + { + AddPaths(open_subjects, PathType::Subject, true); + } + void AddClip(const Paths64& clips) + { + AddPaths(clips, PathType::Clip, false); + } + + bool Execute(ClipType clip_type, + FillRule fill_rule, Paths64& closed_paths) + { + return ClipperBase::Execute(clip_type, fill_rule, closed_paths); + } + + bool Execute(ClipType clip_type, + FillRule fill_rule, Paths64& closed_paths, Paths64& open_paths) + { + return ClipperBase::Execute(clip_type, fill_rule, closed_paths, open_paths); + } + + bool Execute(ClipType clip_type, FillRule fill_rule, PolyTree64& polytree) + { + return ClipperBase::Execute(clip_type, fill_rule, polytree); + } + bool Execute(ClipType clip_type, + FillRule fill_rule, PolyTree64& polytree, Paths64& open_paths) + { + return ClipperBase::Execute(clip_type, fill_rule, polytree, open_paths); + } + }; + + class ClipperD : public ClipperBase { + private: + double scale_ = 1.0, invScale_ = 1.0; +#ifdef USINGZ + ZCallbackD zCallback_ = nullptr; +#endif + public: + explicit ClipperD(int precision = 2) : ClipperBase() + { + scale_ = std::pow(10, precision); + invScale_ = 1 / scale_; + } + +#ifdef USINGZ + void SetZCallback(ZCallbackD cb) { zCallback_ = cb; }; + + void ZCB(const Point64& e1bot, const Point64& e1top, + const Point64& e2bot, const Point64& e2top, Point64& pt) + { + // de-scale (x & y) + // temporarily convert integers to their initial float values + // this will slow clipping marginally but will make it much easier + // to understand the coordinates passed to the callback function + PointD tmp = PointD(pt) * invScale_; + PointD e1b = PointD(e1bot) * invScale_; + PointD e1t = PointD(e1top) * invScale_; + PointD e2b = PointD(e2bot) * invScale_; + PointD e2t = PointD(e2top) * invScale_; + zCallback_(e1b,e1t, e2b, e2t, tmp); + pt.z = tmp.z; // only update 'z' + }; + + void CheckCallback() + { + if(zCallback_) + // if the user defined float point callback has been assigned + // then assign the proxy callback function + ClipperBase::zCallback_ = + std::bind(&ClipperD::ZCB, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4, std::placeholders::_5); + else + ClipperBase::zCallback_ = nullptr; + } + +#endif + + void AddSubject(const PathsD& subjects) + { + AddPaths(ScalePaths(subjects, scale_), PathType::Subject, false); + } + + void AddOpenSubject(const PathsD& open_subjects) + { + AddPaths(ScalePaths(open_subjects, scale_), PathType::Subject, true); + } + + void AddClip(const PathsD& clips) + { + AddPaths(ScalePaths(clips, scale_), PathType::Clip, false); + } + + bool Execute(ClipType clip_type, FillRule fill_rule, PathsD& closed_paths) + { +#ifdef USINGZ + CheckCallback(); +#endif + Paths64 closed_paths64; + if (!ClipperBase::Execute(clip_type, fill_rule, closed_paths64)) return false; + closed_paths = ScalePaths(closed_paths64, invScale_); + return true; + } + + bool Execute(ClipType clip_type, + FillRule fill_rule, PathsD& closed_paths, PathsD& open_paths) + { +#ifdef USINGZ + CheckCallback(); +#endif + Paths64 closed_paths64; + Paths64 open_paths64; + if (!ClipperBase::Execute(clip_type, + fill_rule, closed_paths64, open_paths64)) return false; + closed_paths = ScalePaths(closed_paths64, invScale_); + open_paths = ScalePaths(open_paths64, invScale_); + return true; + } + + bool Execute(ClipType clip_type, FillRule fill_rule, PolyTreeD& polytree) + { +#ifdef USINGZ + CheckCallback(); +#endif + PolyTree64 tree_result; + if (!ClipperBase::Execute(clip_type, fill_rule, tree_result)) return false;; + Polytree64ToPolytreeD(tree_result, polytree); + return true; + } + + bool Execute(ClipType clip_type, + FillRule fill_rule, PolyTreeD& polytree, Paths64& open_paths) + { +#ifdef USINGZ + CheckCallback(); +#endif + PolyTree64 tree_result; + if (!ClipperBase::Execute(clip_type, fill_rule, tree_result, open_paths)) return false;; + Polytree64ToPolytreeD(tree_result, polytree); + return true; + } + + }; + +} // namespace + +#endif // clipper_engine_h diff --git a/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.h b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.h new file mode 100644 index 0000000000..33b7bfccaf --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.h @@ -0,0 +1,672 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : This module provides a simple interface to the Clipper Library * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef CLIPPER_H +#define CLIPPER_H + +#include +#include + +#include "clipper.core.h" +#include "clipper.engine.h" +#include "clipper.offset.h" +#include "clipper.minkowski.h" +#include "clipper.rectclip.h" + +namespace Clipper2Lib { + + static const char* precision_error = + "Precision exceeds the permitted range"; + + static const Rect64 MaxInvalidRect64 = Rect64( + (std::numeric_limits::max)(), + (std::numeric_limits::max)(), + (std::numeric_limits::lowest)(), + (std::numeric_limits::lowest)()); + + static const RectD MaxInvalidRectD = RectD( + (std::numeric_limits::max)(), + (std::numeric_limits::max)(), + (std::numeric_limits::lowest)(), + (std::numeric_limits::lowest)()); + + inline Paths64 BooleanOp(ClipType cliptype, FillRule fillrule, + const Paths64& subjects, const Paths64& clips) + { + Paths64 result; + Clipper64 clipper; + clipper.AddSubject(subjects); + clipper.AddClip(clips); + clipper.Execute(cliptype, fillrule, result); + return result; + } + + inline void BooleanOp(ClipType cliptype, FillRule fillrule, + const Paths64& subjects, const Paths64& clips, PolyTree64& solution) + { + Paths64 sol_open; + Clipper64 clipper; + clipper.AddSubject(subjects); + clipper.AddClip(clips); + clipper.Execute(cliptype, fillrule, solution, sol_open); + } + + inline PathsD BooleanOp(ClipType cliptype, FillRule fillrule, + const PathsD& subjects, const PathsD& clips, int decimal_prec = 2) + { + if (decimal_prec > 8 || decimal_prec < -8) + throw Clipper2Exception(precision_error); + PathsD result; + ClipperD clipper(decimal_prec); + clipper.AddSubject(subjects); + clipper.AddClip(clips); + clipper.Execute(cliptype, fillrule, result); + return result; + } + + inline Paths64 Intersect(const Paths64& subjects, const Paths64& clips, FillRule fillrule) + { + return BooleanOp(ClipType::Intersection, fillrule, subjects, clips); + } + + inline PathsD Intersect(const PathsD& subjects, const PathsD& clips, FillRule fillrule, int decimal_prec = 2) + { + return BooleanOp(ClipType::Intersection, fillrule, subjects, clips, decimal_prec); + } + + inline Paths64 Union(const Paths64& subjects, const Paths64& clips, FillRule fillrule) + { + return BooleanOp(ClipType::Union, fillrule, subjects, clips); + } + + inline PathsD Union(const PathsD& subjects, const PathsD& clips, FillRule fillrule, int decimal_prec = 2) + { + return BooleanOp(ClipType::Union, fillrule, subjects, clips, decimal_prec); + } + + inline Paths64 Union(const Paths64& subjects, FillRule fillrule) + { + Paths64 result; + Clipper64 clipper; + clipper.AddSubject(subjects); + clipper.Execute(ClipType::Union, fillrule, result); + return result; + } + + inline PathsD Union(const PathsD& subjects, FillRule fillrule, int decimal_prec = 2) + { + if (decimal_prec > 8 || decimal_prec < -8) + throw Clipper2Exception(precision_error); + PathsD result; + ClipperD clipper(decimal_prec); + clipper.AddSubject(subjects); + clipper.Execute(ClipType::Union, fillrule, result); + return result; + } + + inline Paths64 Difference(const Paths64& subjects, const Paths64& clips, FillRule fillrule) + { + return BooleanOp(ClipType::Difference, fillrule, subjects, clips); + } + + inline PathsD Difference(const PathsD& subjects, const PathsD& clips, FillRule fillrule, int decimal_prec = 2) + { + return BooleanOp(ClipType::Difference, fillrule, subjects, clips, decimal_prec); + } + + inline Paths64 Xor(const Paths64& subjects, const Paths64& clips, FillRule fillrule) + { + return BooleanOp(ClipType::Xor, fillrule, subjects, clips); + } + + inline PathsD Xor(const PathsD& subjects, const PathsD& clips, FillRule fillrule, int decimal_prec = 2) + { + return BooleanOp(ClipType::Xor, fillrule, subjects, clips, decimal_prec); + } + + inline bool IsFullOpenEndType(EndType et) + { + return (et != EndType::Polygon) && (et != EndType::Joined); + } + + inline Paths64 InflatePaths(const Paths64& paths, double delta, + JoinType jt, EndType et, double miter_limit = 2.0) + { + ClipperOffset clip_offset(miter_limit); + clip_offset.AddPaths(paths, jt, et); + return clip_offset.Execute(delta); + } + + inline PathsD InflatePaths(const PathsD& paths, double delta, + JoinType jt, EndType et, double miter_limit = 2.0, double precision = 2) + { + if (precision < -8 || precision > 8) + throw new Clipper2Exception(precision_error); + const double scale = std::pow(10, precision); + ClipperOffset clip_offset(miter_limit); + clip_offset.AddPaths(ScalePaths(paths, scale), jt, et); + Paths64 tmp = clip_offset.Execute(delta * scale); + return ScalePaths(tmp, 1 / scale); + } + + inline Path64 TranslatePath(const Path64& path, int64_t dx, int64_t dy) + { + Path64 result; + result.reserve(path.size()); + for (const Point64& pt : path) + result.push_back(Point64(pt.x + dx, pt.y + dy)); + return result; + } + + inline PathD TranslatePath(const PathD& path, double dx, double dy) + { + PathD result; + result.reserve(path.size()); + for (const PointD& pt : path) + result.push_back(PointD(pt.x + dx, pt.y + dy)); + return result; + } + + inline Paths64 TranslatePaths(const Paths64& paths, int64_t dx, int64_t dy) + { + Paths64 result; + result.reserve(paths.size()); + for (const Path64& path : paths) + result.push_back(TranslatePath(path, dx, dy)); + return result; + } + + inline PathsD TranslatePaths(const PathsD& paths, double dx, double dy) + { + PathsD result; + result.reserve(paths.size()); + for (const PathD& path : paths) + result.push_back(TranslatePath(path, dx, dy)); + return result; + } + + inline Rect64 Bounds(const Path64& path) + { + Rect64 rec = MaxInvalidRect64; + for (const Point64& pt : path) + { + if (pt.x < rec.left) rec.left = pt.x; + if (pt.x > rec.right) rec.right = pt.x; + if (pt.y < rec.top) rec.top = pt.y; + if (pt.y > rec.bottom) rec.bottom = pt.y; + } + if (rec.IsEmpty()) return Rect64(); + return rec; + } + + inline Rect64 Bounds(const Paths64& paths) + { + Rect64 rec = MaxInvalidRect64; + for (const Path64& path : paths) + for (const Point64& pt : path) + { + if (pt.x < rec.left) rec.left = pt.x; + if (pt.x > rec.right) rec.right = pt.x; + if (pt.y < rec.top) rec.top = pt.y; + if (pt.y > rec.bottom) rec.bottom = pt.y; + } + if (rec.IsEmpty()) return Rect64(); + return rec; + } + + inline RectD Bounds(const PathD& path) + { + RectD rec = MaxInvalidRectD; + for (const PointD& pt : path) + { + if (pt.x < rec.left) rec.left = pt.x; + if (pt.x > rec.right) rec.right = pt.x; + if (pt.y < rec.top) rec.top = pt.y; + if (pt.y > rec.bottom) rec.bottom = pt.y; + } + if (rec.IsEmpty()) return RectD(); + return rec; + } + + inline RectD Bounds(const PathsD& paths) + { + RectD rec = MaxInvalidRectD; + for (const PathD& path : paths) + for (const PointD& pt : path) + { + if (pt.x < rec.left) rec.left = pt.x; + if (pt.x > rec.right) rec.right = pt.x; + if (pt.y < rec.top) rec.top = pt.y; + if (pt.y > rec.bottom) rec.bottom = pt.y; + } + if (rec.IsEmpty()) return RectD(); + return rec; + } + + inline Path64 RectClip(const Rect64& rect, const Path64& path) + { + if (rect.IsEmpty() || path.empty()) return Path64(); + Rect64 pathRec = Bounds(path); + if (!rect.Intersects(pathRec)) return Path64(); + if (rect.Contains(pathRec)) return path; + RectClip64 rc(rect); + return rc.Execute(path); + } + + inline Paths64 RectClip(const Rect64& rect, const Paths64& paths) + { + if (rect.IsEmpty() || paths.empty()) return Paths64(); + RectClip64 rc(rect); + Paths64 result; + result.reserve(paths.size()); + + for (const Path64& p : paths) + { + Rect64 pathRec = Bounds(p); + if (!rect.Intersects(pathRec)) + continue; + else if (rect.Contains(pathRec)) + result.push_back(p); + else + { + Path64 p2 = rc.Execute(p); + if (!p2.empty()) result.push_back(std::move(p2)); + } + } + return result; + } + + inline PathD RectClip(const RectD& rect, const PathD& path, int precision = 2) + { + if (rect.IsEmpty() || path.empty() || + !rect.Contains(Bounds(path))) return PathD(); + if (precision < -8 || precision > 8) + throw new Clipper2Exception(precision_error); + const double scale = std::pow(10, precision); + Rect64 r = ScaleRect(rect, scale); + RectClip64 rc(r); + Path64 p = ScalePath(path, scale); + return ScalePath(rc.Execute(p), 1 / scale); + } + + inline PathsD RectClip(const RectD& rect, const PathsD& paths, int precision = 2) + { + if (rect.IsEmpty() || paths.empty()) return PathsD(); + if (precision < -8 || precision > 8) + throw new Clipper2Exception(precision_error); + const double scale = std::pow(10, precision); + Rect64 r = ScaleRect(rect, scale); + RectClip64 rc(r); + PathsD result; + result.reserve(paths.size()); + for (const PathD& path : paths) + { + RectD pathRec = Bounds(path); + if (!rect.Intersects(pathRec)) + continue; + else if (rect.Contains(pathRec)) + result.push_back(path); + else + { + Path64 p = ScalePath(path, scale); + p = rc.Execute(p); + if (!p.empty()) + result.push_back(ScalePath(p, 1 / scale)); + } + } + return result; + } + + namespace details + { + + template + inline void InternalPolyNodeToPaths(const PolyPath& polypath, Paths& paths) + { + paths.push_back(polypath.Polygon()); + for (auto child : polypath) + InternalPolyNodeToPaths(*child, paths); + } + + inline bool InternalPolyPathContainsChildren(const PolyPath64& pp) + { + for (auto child : pp) + { + for (const Point64& pt : child->Polygon()) + if (PointInPolygon(pt, pp.Polygon()) == PointInPolygonResult::IsOutside) + return false; + if (child->Count() > 0 && !InternalPolyPathContainsChildren(*child)) + return false; + } + return true; + } + + inline bool GetInt(std::string::const_iterator& iter, const + std::string::const_iterator& end_iter, int64_t& val) + { + val = 0; + bool is_neg = *iter == '-'; + if (is_neg) ++iter; + std::string::const_iterator start_iter = iter; + while (iter != end_iter && + ((*iter >= '0') && (*iter <= '9'))) + { + val = val * 10 + (static_cast(*iter++) - '0'); + } + if (is_neg) val = -val; + return (iter != start_iter); + } + + inline bool GetFloat(std::string::const_iterator& iter, const + std::string::const_iterator& end_iter, double& val) + { + val = 0; + bool is_neg = *iter == '-'; + if (is_neg) ++iter; + int dec_pos = 1; + const std::string::const_iterator start_iter = iter; + while (iter != end_iter && (*iter == '.' || + ((*iter >= '0') && (*iter <= '9')))) + { + if (*iter == '.') + { + if (dec_pos != 1) break; + dec_pos = 0; + ++iter; + continue; + } + if (dec_pos != 1) --dec_pos; + val = val * 10 + ((int64_t)(*iter++) - '0'); + } + if (iter == start_iter || dec_pos == 0) return false; + if (dec_pos < 0) + val *= std::pow(10, dec_pos); + if (is_neg) + val *= -1; + return true; + } + + inline void SkipWhiteSpace(std::string::const_iterator& iter, + const std::string::const_iterator& end_iter) + { + while (iter != end_iter && *iter <= ' ') ++iter; + } + + inline void SkipSpacesWithOptionalComma(std::string::const_iterator& iter, + const std::string::const_iterator& end_iter) + { + bool comma_seen = false; + while (iter != end_iter) + { + if (*iter == ' ') ++iter; + else if (*iter == ',') + { + if (comma_seen) return; // don't skip 2 commas! + comma_seen = true; + ++iter; + } + else return; + } + } + + inline bool has_one_match(const char c, char* chrs) + { + while (*chrs > 0 && c != *chrs) ++chrs; + if (!*chrs) return false; + *chrs = ' '; // only match once per char + return true; + } + + + inline void SkipUserDefinedChars(std::string::const_iterator& iter, + const std::string::const_iterator& end_iter, const std::string& skip_chars) + { + const size_t MAX_CHARS = 16; + char buff[MAX_CHARS] = {0}; + std::copy(skip_chars.cbegin(), skip_chars.cend(), &buff[0]); + while (iter != end_iter && + (*iter <= ' ' || has_one_match(*iter, buff))) ++iter; + return; + } + + } // end details namespace + + template + inline Paths PolyTreeToPaths(const PolyTree& polytree) + { + Paths result; + for (auto child : polytree) + details::InternalPolyNodeToPaths(*child, result); + return result; + } + + inline bool CheckPolytreeFullyContainsChildren(const PolyTree64& polytree) + { + for (auto child : polytree) + if (child->Count() > 0 && !details::InternalPolyPathContainsChildren(*child)) + return false; + return true; + } + + inline Path64 MakePath(const std::string& s) + { + const std::string skip_chars = " ,(){}[]"; + Path64 result; + std::string::const_iterator s_iter = s.cbegin(); + details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); + while (s_iter != s.cend()) + { + int64_t y = 0, x = 0; + if (!details::GetInt(s_iter, s.cend(), x)) break; + details::SkipSpacesWithOptionalComma(s_iter, s.cend()); + if (!details::GetInt(s_iter, s.cend(), y)) break; + result.push_back(Point64(x, y)); + details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); + } + return result; + } + + inline PathD MakePathD(const std::string& s) + { + const std::string skip_chars = " ,(){}[]"; + PathD result; + std::string::const_iterator s_iter = s.cbegin(); + details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); + while (s_iter != s.cend()) + { + double y = 0, x = 0; + if (!details::GetFloat(s_iter, s.cend(), x)) break; + details::SkipSpacesWithOptionalComma(s_iter, s.cend()); + if (!details::GetFloat(s_iter, s.cend(), y)) break; + result.push_back(PointD(x, y)); + details::SkipUserDefinedChars(s_iter, s.cend(), skip_chars); + } + return result; + } + + inline Path64 TrimCollinear(const Path64& p, bool is_open_path = false) + { + size_t len = p.size(); + if (len < 3) + { + if (!is_open_path || len < 2 || p[0] == p[1]) return Path64(); + else return p; + } + + Path64 dst; + dst.reserve(len); + Path64::const_iterator srcIt = p.cbegin(), prevIt, stop = p.cend() - 1; + + if (!is_open_path) + { + while (srcIt != stop && !CrossProduct(*stop, *srcIt, *(srcIt + 1))) + ++srcIt; + while (srcIt != stop && !CrossProduct(*(stop - 1), *stop, *srcIt)) + --stop; + if (srcIt == stop) return Path64(); + } + + prevIt = srcIt++; + dst.push_back(*prevIt); + for (; srcIt != stop; ++srcIt) + { + if (CrossProduct(*prevIt, *srcIt, *(srcIt + 1))) + { + prevIt = srcIt; + dst.push_back(*prevIt); + } + } + + if (is_open_path) + dst.push_back(*srcIt); + else if (CrossProduct(*prevIt, *stop, dst[0])) + dst.push_back(*stop); + else + { + while (dst.size() > 2 && + !CrossProduct(dst[dst.size() - 1], dst[dst.size() - 2], dst[0])) + dst.pop_back(); + if (dst.size() < 3) return Path64(); + } + return dst; + } + + inline PathD TrimCollinear(const PathD& path, int precision, bool is_open_path = false) + { + if (precision > 8 || precision < -8) + throw new Clipper2Exception(precision_error); + const double scale = std::pow(10, precision); + Path64 p = ScalePath(path, scale); + p = TrimCollinear(p, is_open_path); + return ScalePath(p, 1/scale); + } + + template + inline double Distance(const Point pt1, const Point pt2) + { + return std::sqrt(DistanceSqr(pt1, pt2)); + } + + template + inline double Length(const Path& path, bool is_closed_path = false) + { + double result = 0.0; + if (path.size() < 2) return result; + auto it = path.cbegin(), stop = path.end() - 1; + for (; it != stop; ++it) + result += Distance(*it, *(it + 1)); + if (is_closed_path) + result += Distance(*stop, *path.cbegin()); + return result; + } + + + template + inline bool NearCollinear(const Point& pt1, const Point& pt2, const Point& pt3, double sin_sqrd_min_angle_rads) + { + double cp = std::abs(CrossProduct(pt1, pt2, pt3)); + return (cp * cp) / (DistanceSqr(pt1, pt2) * DistanceSqr(pt2, pt3)) < sin_sqrd_min_angle_rads; + } + + template + inline Path Ellipse(const Rect& rect, int steps = 0) + { + return Ellipse(rect.MidPoint(), + static_cast(rect.Width()) *0.5, + static_cast(rect.Height()) * 0.5, steps); + } + + template + inline Path Ellipse(const Point& center, + double radiusX, double radiusY = 0, int steps = 0) + { + if (radiusX <= 0) return Path(); + if (radiusY <= 0) radiusY = radiusX; + if (steps <= 2) + steps = static_cast(PI * sqrt((radiusX + radiusY) / 2)); + + double si = std::sin(2 * PI / steps); + double co = std::cos(2 * PI / steps); + double dx = co, dy = si; + Path result; + result.reserve(steps); + result.push_back(Point(center.x + radiusX, static_cast(center.y))); + for (int i = 1; i < steps; ++i) + { + result.push_back(Point(center.x + radiusX * dx, center.y + radiusY * dy)); + double x = dx * co - dy * si; + dy = dy * co + dx * si; + dx = x; + } + return result; + } + + template + inline double PerpendicDistFromLineSqrd(const Point& pt, + const Point& line1, const Point& line2) + { + double a = static_cast(pt.x - line1.x); + double b = static_cast(pt.y - line1.y); + double c = static_cast(line2.x - line1.x); + double d = static_cast(line2.y - line1.y); + if (c == 0 && d == 0) return 0; + return Sqr(a * d - c * b) / (c * c + d * d); + } + + template + inline void RDP(const Path path, std::size_t begin, + std::size_t end, double epsSqrd, std::vector& flags) + { + typename Path::size_type idx = 0; + double max_d = 0; + while (end > begin && path[begin] == path[end]) flags[end--] = false; + for (typename Path::size_type i = begin + 1; i < end; ++i) + { + // PerpendicDistFromLineSqrd - avoids expensive Sqrt() + double d = PerpendicDistFromLineSqrd(path[i], path[begin], path[end]); + if (d <= max_d) continue; + max_d = d; + idx = i; + } + if (max_d <= epsSqrd) return; + flags[idx] = true; + if (idx > begin + 1) RDP(path, begin, idx, epsSqrd, flags); + if (idx < end - 1) RDP(path, idx, end, epsSqrd, flags); + } + + template + inline Path RamerDouglasPeucker(const Path& path, double epsilon) + { + const typename Path::size_type len = path.size(); + if (len < 5) return Path(path); + std::vector flags(len); + flags[0] = true; + flags[len - 1] = true; + RDP(path, 0, len - 1, Sqr(epsilon), flags); + Path result; + result.reserve(len); + for (typename Path::size_type i = 0; i < len; ++i) + if (flags[i]) + result.push_back(path[i]); + return result; + } + + template + inline Paths RamerDouglasPeucker(const Paths& paths, double epsilon) + { + Paths result; + result.reserve(paths.size()); + for (const Path& path : paths) + result.push_back(RamerDouglasPeucker(path, epsilon)); + return result; + } + +} // end Clipper2Lib namespace + +#endif // CLIPPER_H diff --git a/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h new file mode 100644 index 0000000000..ca0ab6be81 --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.minkowski.h @@ -0,0 +1,118 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : Minkowski Sum and Difference * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef CLIPPER_MINKOWSKI_H +#define CLIPPER_MINKOWSKI_H + +#include +#include +#include +#include "clipper.core.h" + +namespace Clipper2Lib +{ + + namespace detail + { + inline Paths64 Minkowski(const Path64& pattern, const Path64& path, bool isSum, bool isClosed) + { + size_t delta = isClosed ? 0 : 1; + size_t patLen = pattern.size(), pathLen = path.size(); + if (patLen == 0 || pathLen == 0) return Paths64(); + Paths64 tmp; + tmp.reserve(pathLen); + + if (isSum) + { + for (const Point64& p : path) + { + Path64 path2(pattern.size()); + std::transform(pattern.cbegin(), pattern.cend(), + path2.begin(), [p](const Point64& pt2) {return p + pt2; }); + tmp.push_back(path2); + } + } + else + { + for (const Point64& p : path) + { + Path64 path2(pattern.size()); + std::transform(pattern.cbegin(), pattern.cend(), + path2.begin(), [p](const Point64& pt2) {return p - pt2; }); + tmp.push_back(path2); + } + } + + Paths64 result; + result.reserve((pathLen - delta) * patLen); + size_t g = isClosed ? pathLen - 1 : 0; + for (size_t h = patLen - 1, i = delta; i < pathLen; ++i) + { + for (size_t j = 0; j < patLen; j++) + { + Path64 quad; + quad.reserve(4); + { + quad.push_back(tmp[g][h]); + quad.push_back(tmp[i][h]); + quad.push_back(tmp[i][j]); + quad.push_back(tmp[g][j]); + }; + if (!IsPositive(quad)) + std::reverse(quad.begin(), quad.end()); + result.push_back(quad); + h = j; + } + g = i; + } + return result; + } + + inline Paths64 Union(const Paths64& subjects, FillRule fillrule) + { + Paths64 result; + Clipper64 clipper; + clipper.AddSubject(subjects); + clipper.Execute(ClipType::Union, fillrule, result); + return result; + } + + } // namespace internal + + inline Paths64 MinkowskiSum(const Path64& pattern, const Path64& path, bool isClosed) + { + return detail::Union(detail::Minkowski(pattern, path, true, isClosed), FillRule::NonZero); + } + + inline PathsD MinkowskiSum(const PathD& pattern, const PathD& path, bool isClosed, int decimalPlaces = 2) + { + double scale = pow(10, decimalPlaces); + Path64 pat64 = ScalePath(pattern, scale); + Path64 path64 = ScalePath(path, scale); + Paths64 tmp = detail::Union(detail::Minkowski(pat64, path64, true, isClosed), FillRule::NonZero); + return ScalePaths(tmp, 1 / scale); + } + + inline Paths64 MinkowskiDiff(const Path64& pattern, const Path64& path, bool isClosed) + { + return detail::Union(detail::Minkowski(pattern, path, false, isClosed), FillRule::NonZero); + } + + inline PathsD MinkowskiDiff(const PathD& pattern, const PathD& path, bool isClosed, int decimalPlaces = 2) + { + double scale = pow(10, decimalPlaces); + Path64 pat64 = ScalePath(pattern, scale); + Path64 path64 = ScalePath(path, scale); + Paths64 tmp = detail::Union(detail::Minkowski(pat64, path64, false, isClosed), FillRule::NonZero); + return ScalePaths(tmp, 1 / scale); + } + +} // Clipper2Lib namespace + +#endif // CLIPPER_MINKOWSKI_H diff --git a/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h new file mode 100644 index 0000000000..4fd130bf4d --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.offset.h @@ -0,0 +1,107 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : Path Offset (Inflate/Shrink) * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef CLIPPER_OFFSET_H_ +#define CLIPPER_OFFSET_H_ + +#include "clipper.core.h" + +namespace Clipper2Lib { + +enum class JoinType { Square, Round, Miter }; + +enum class EndType {Polygon, Joined, Butt, Square, Round}; +//Butt : offsets both sides of a path, with square blunt ends +//Square : offsets both sides of a path, with square extended ends +//Round : offsets both sides of a path, with round extended ends +//Joined : offsets both sides of a path, with joined ends +//Polygon: offsets only one side of a closed path + +class ClipperOffset { +private: + + class Group { + public: + Paths64 paths_in_; + Paths64 paths_out_; + Path64 path_; + bool is_reversed_ = false; + JoinType join_type_; + EndType end_type_; + Group(const Paths64& paths, JoinType join_type, EndType end_type) : + paths_in_(paths), join_type_(join_type), end_type_(end_type) {} + }; + + double group_delta_ = 0.0; + double abs_group_delta_ = 0.0; + double temp_lim_ = 0.0; + double steps_per_rad_ = 0.0; + PathD norms; + Paths64 solution; + std::vector groups_; + JoinType join_type_ = JoinType::Square; + + double miter_limit_ = 0.0; + double arc_tolerance_ = 0.0; + bool merge_groups_ = true; + bool preserve_collinear_ = false; + bool reverse_solution_ = false; + + void DoSquare(Group& group, const Path64& path, size_t j, size_t k); + void DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a); + void DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle); + void BuildNormals(const Path64& path); + void OffsetPolygon(Group& group, Path64& path); + void OffsetOpenJoined(Group& group, Path64& path); + void OffsetOpenPath(Group& group, Path64& path, EndType endType); + void OffsetPoint(Group& group, Path64& path, size_t j, size_t& k); + void DoGroupOffset(Group &group, double delta); +public: + ClipperOffset(double miter_limit = 2.0, + double arc_tolerance = 0.0, + bool preserve_collinear = false, + bool reverse_solution = false) : + miter_limit_(miter_limit), arc_tolerance_(arc_tolerance), + preserve_collinear_(preserve_collinear), + reverse_solution_(reverse_solution) { }; + + ~ClipperOffset() { Clear(); }; + + void AddPath(const Path64& path, JoinType jt_, EndType et_); + void AddPaths(const Paths64& paths, JoinType jt_, EndType et_); + void AddPath(const PathD &p, JoinType jt_, EndType et_); + void AddPaths(const PathsD &p, JoinType jt_, EndType et_); + void Clear() { groups_.clear(); norms.clear(); }; + + Paths64 Execute(double delta); + + double MiterLimit() const { return miter_limit_; } + void MiterLimit(double miter_limit) { miter_limit_ = miter_limit; } + + //ArcTolerance: needed for rounded offsets (See offset_triginometry2.svg) + double ArcTolerance() const { return arc_tolerance_; } + void ArcTolerance(double arc_tolerance) { arc_tolerance_ = arc_tolerance; } + + //MergeGroups: A path group is one or more paths added via the AddPath or + //AddPaths methods. By default these path groups will be offset + //independently of other groups and this may cause overlaps (intersections). + //However, when MergeGroups is enabled, any overlapping offsets will be + //merged (via a clipping union operation) to remove overlaps. + bool MergeGroups() const { return merge_groups_; } + void MergeGroups(bool merge_groups) { merge_groups_ = merge_groups; } + + bool PreserveCollinear() const { return preserve_collinear_; } + void PreserveCollinear(bool preserve_collinear){preserve_collinear_ = preserve_collinear;} + + bool ReverseSolution() const { return reverse_solution_; } + void ReverseSolution(bool reverse_solution) {reverse_solution_ = reverse_solution;} +}; + +} +#endif /* CLIPPER_OFFSET_H_ */ diff --git a/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h new file mode 100644 index 0000000000..a610b7bbbe --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/include/clipper2/clipper.rectclip.h @@ -0,0 +1,46 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : FAST rectangular clipping * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#ifndef CLIPPER_RECTCLIP_H +#define CLIPPER_RECTCLIP_H + +#include +#include +#include "clipper.h" +#include "clipper.core.h" + +namespace Clipper2Lib +{ + + enum class Location { Left, Top, Right, Bottom, Inside }; + + class RectClip64 { + private: + const Rect64 rect_; + const Point64 mp_; + const Path64 rectPath_; + Path64 result_; + std::vector start_locs_; + + void Reset(); + void GetNextLocation(const Path64& path, + Location& loc, int& i, int highI); + void AddCorner(Location prev, Location curr); + void AddCorner(Location& loc, bool isClockwise); + + public: + RectClip64(const Rect64& rect) : + rect_(rect), + mp_(rect.MidPoint()), + rectPath_(rect.AsPath()) {} + Path64 Execute(const Path64& path); + }; + +} // Clipper2Lib namespace +#endif // CLIPPER_RECTCLIP_H diff --git a/thirdparty/clipper2/Clipper2Lib/src/clipper.engine.cpp b/thirdparty/clipper2/Clipper2Lib/src/clipper.engine.cpp new file mode 100644 index 0000000000..28bc806b73 --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/src/clipper.engine.cpp @@ -0,0 +1,3496 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : This is the main polygon clipping module * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include "clipper2/clipper.engine.h" + +namespace Clipper2Lib { + + static const double FloatingPointTolerance = 1.0e-12; + static const Rect64 invalid_rect = Rect64( + std::numeric_limits::max(), + std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max() + ); + + //Every closed path (or polygon) is made up of a series of vertices forming + //edges that alternate between going up (relative to the Y-axis) and going + //down. Edges consecutively going up or consecutively going down are called + //'bounds' (or sides if they're simple polygons). 'Local Minima' refer to + //vertices where descending bounds become ascending ones. + + struct Scanline { + int64_t y = 0; + Scanline* next = nullptr; + + explicit Scanline(int64_t y_) : y(y_) {} + }; + + struct Joiner { + int idx; + OutPt* op1; + OutPt* op2; + Joiner* next1; + Joiner* next2; + Joiner* nextH; + + explicit Joiner(OutPt* op1_, OutPt* op2_, Joiner* nexth) : + op1(op1_), op2(op2_), nextH(nexth) + { + idx = -1; + next1 = op1->joiner; + op1->joiner = this; + + if (op2) + { + next2 = op2->joiner; + op2->joiner = this; + } + else + next2 = nullptr; + } + + }; + + struct LocMinSorter { + inline bool operator()(const LocalMinima* locMin1, const LocalMinima* locMin2) + { + if (locMin2->vertex->pt.y != locMin1->vertex->pt.y) + return locMin2->vertex->pt.y < locMin1->vertex->pt.y; + else + return locMin2->vertex->pt.x < locMin1->vertex->pt.x; + } + }; + + inline bool IsOdd(int val) + { + return (val & 1) ? true : false; + } + + + inline bool IsHotEdge(const Active& e) + { + return (e.outrec); + } + + + inline bool IsOpen(const Active& e) + { + return (e.local_min->is_open); + } + + + inline bool IsOpenEnd(const Vertex& v) + { + return (v.flags & (VertexFlags::OpenStart | VertexFlags::OpenEnd)) != + VertexFlags::None; + } + + + inline bool IsOpenEnd(const Active& ae) + { + return IsOpenEnd(*ae.vertex_top); + } + + + inline Active* GetPrevHotEdge(const Active& e) + { + Active* prev = e.prev_in_ael; + while (prev && (IsOpen(*prev) || !IsHotEdge(*prev))) + prev = prev->prev_in_ael; + return prev; + } + + inline bool IsFront(const Active& e) + { + return (&e == e.outrec->front_edge); + } + + inline bool IsInvalidPath(OutPt* op) + { + return (!op || op->next == op); + } + + /******************************************************************************* + * Dx: 0(90deg) * + * | * + * +inf (180deg) <--- o ---> -inf (0deg) * + *******************************************************************************/ + + inline double GetDx(const Point64& pt1, const Point64& pt2) + { + double dy = double(pt2.y - pt1.y); + if (dy != 0) + return double(pt2.x - pt1.x) / dy; + else if (pt2.x > pt1.x) + return -std::numeric_limits::max(); + else + return std::numeric_limits::max(); + } + + + inline int64_t TopX(const Active& ae, const int64_t currentY) + { + if ((currentY == ae.top.y) || (ae.top.x == ae.bot.x)) return ae.top.x; + else if (currentY == ae.bot.y) return ae.bot.x; + else return ae.bot.x + static_cast(std::round(ae.dx * (currentY - ae.bot.y))); + //nb: std::round above substantially *improves* performance + } + + + inline bool IsHorizontal(const Active& e) + { + return (e.top.y == e.bot.y); + } + + + inline bool IsHeadingRightHorz(const Active& e) + { + return e.dx == -std::numeric_limits::max(); + } + + + inline bool IsHeadingLeftHorz(const Active& e) + { + return e.dx == std::numeric_limits::max(); + } + + + inline void SwapActives(Active*& e1, Active*& e2) + { + Active* e = e1; + e1 = e2; + e2 = e; + } + + + inline PathType GetPolyType(const Active& e) + { + return e.local_min->polytype; + } + + + inline bool IsSamePolyType(const Active& e1, const Active& e2) + { + return e1.local_min->polytype == e2.local_min->polytype; + } + + + Point64 GetIntersectPoint(const Active& e1, const Active& e2) + { + double b1, b2; + if (e1.dx == e2.dx) return e1.top; + + if (e1.dx == 0) + { + if (IsHorizontal(e2)) return Point64(e1.bot.x, e2.bot.y); + b2 = e2.bot.y - (e2.bot.x / e2.dx); + return Point64(e1.bot.x, + static_cast(std::round(e1.bot.x / e2.dx + b2))); + } + else if (e2.dx == 0) + { + if (IsHorizontal(e1)) return Point64(e2.bot.x, e1.bot.y); + b1 = e1.bot.y - (e1.bot.x / e1.dx); + return Point64(e2.bot.x, + static_cast(std::round(e2.bot.x / e1.dx + b1))); + } + else + { + b1 = e1.bot.x - e1.bot.y * e1.dx; + b2 = e2.bot.x - e2.bot.y * e2.dx; + double q = (b2 - b1) / (e1.dx - e2.dx); + return (abs(e1.dx) < abs(e2.dx)) ? + Point64(static_cast((e1.dx * q + b1)), + static_cast((q))) : + Point64(static_cast((e2.dx * q + b2)), + static_cast((q))); + } + } + + + bool GetIntersectPoint(const Point64& ln1a, const Point64& ln1b, + const Point64& ln2a, const Point64& ln2b, PointD& ip) + { + ip = PointD(0, 0); + double m1, b1, m2, b2; + if (ln1b.x == ln1a.x) + { + if (ln2b.x == ln2a.x) return false; + m2 = static_cast(ln2b.y - ln2a.y) / + static_cast(ln2b.x - ln2a.x); + b2 = ln2a.y - m2 * ln2a.x; + ip.x = static_cast(ln1a.x); + ip.y = m2 * ln1a.x + b2; + } + else if (ln2b.x == ln2a.x) + { + m1 = static_cast(ln1b.y - ln1a.y) / + static_cast(ln1b.x - ln1a.x); + b1 = ln1a.y - m1 * ln1a.x; + ip.x = static_cast(ln2a.x); + ip.y = m1 * ln2a.x + b1; + } + else + { + m1 = static_cast(ln1b.y - ln1a.y) / + static_cast(ln1b.x - ln1a.x); + b1 = ln1a.y - m1 * ln1a.x; + m2 = static_cast(ln2b.y - ln2a.y) / + static_cast(ln2b.x - ln2a.x); + b2 = ln2a.y - m2 * ln2a.x; + if (std::abs(m1 - m2) > FloatingPointTolerance) + { + ip.x = (b2 - b1) / (m1 - m2); + ip.y = m1 * ip.x + b1; + } + else + { + ip.x = static_cast(ln1a.x + ln1b.x) / 2; + ip.y = static_cast(ln1a.y + ln1b.y) / 2; + } + } + return true; + } + + + inline void SetDx(Active& e) + { + e.dx = GetDx(e.bot, e.top); + } + + + inline Vertex* NextVertex(const Active& e) + { + if (e.wind_dx > 0) + return e.vertex_top->next; + else + return e.vertex_top->prev; + } + + + //PrevPrevVertex: useful to get the (inverted Y-axis) top of the + //alternate edge (ie left or right bound) during edge insertion. + inline Vertex* PrevPrevVertex(const Active& ae) + { + if (ae.wind_dx > 0) + return ae.vertex_top->prev->prev; + else + return ae.vertex_top->next->next; + } + + + inline Active* ExtractFromSEL(Active* ae) + { + Active* res = ae->next_in_sel; + if (res) + res->prev_in_sel = ae->prev_in_sel; + ae->prev_in_sel->next_in_sel = res; + return res; + } + + + inline void Insert1Before2InSEL(Active* ae1, Active* ae2) + { + ae1->prev_in_sel = ae2->prev_in_sel; + if (ae1->prev_in_sel) + ae1->prev_in_sel->next_in_sel = ae1; + ae1->next_in_sel = ae2; + ae2->prev_in_sel = ae1; + } + + inline bool IsMaxima(const Vertex& v) + { + return ((v.flags & VertexFlags::LocalMax) != VertexFlags::None); + } + + + inline bool IsMaxima(const Active& e) + { + return IsMaxima(*e.vertex_top); + } + + Vertex* GetCurrYMaximaVertex(const Active& e) + { + Vertex* result = e.vertex_top; + if (e.wind_dx > 0) + while (result->next->pt.y == result->pt.y) result = result->next; + else + while (result->prev->pt.y == result->pt.y) result = result->prev; + if (!IsMaxima(*result)) result = nullptr; // not a maxima + return result; + } + + Active* GetMaximaPair(const Active& e) + { + Active* e2; + e2 = e.next_in_ael; + while (e2) + { + if (e2->vertex_top == e.vertex_top) return e2; // Found! + e2 = e2->next_in_ael; + } + return nullptr; + } + + Active* GetHorzMaximaPair(const Active& horz, const Vertex* vert_max) + { + //we can't be sure whether the MaximaPair is on the left or right, so ... + Active* result = horz.prev_in_ael; + while (result && result->curr_x >= vert_max->pt.x) + { + if (result->vertex_top == vert_max) return result; // Found! + result = result->prev_in_ael; + } + result = horz.next_in_ael; + while (result && TopX(*result, horz.top.y) <= vert_max->pt.x) + { + if (result->vertex_top == vert_max) return result; // Found! + result = result->next_in_ael; + } + return nullptr; + } + + inline int PointCount(OutPt* op) + { + OutPt* op2 = op; + int cnt = 0; + do + { + op2 = op2->next; + ++cnt; + } while (op2 != op); + return cnt; + } + + + inline OutPt* InsertOp(const Point64& pt, OutPt* insertAfter) + { + OutPt* result = new OutPt(pt, insertAfter->outrec); + result->next = insertAfter->next; + insertAfter->next->prev = result; + insertAfter->next = result; + result->prev = insertAfter; + return result; + } + + + inline OutPt* DisposeOutPt(OutPt* op) + { + OutPt* result = op->next; + op->prev->next = op->next; + op->next->prev = op->prev; + delete op; + return result; + } + + + inline void DisposeOutPts(OutRec& outrec) + { + if (!outrec.pts) return; + OutPt* op2 = outrec.pts->next; + while (op2 != outrec.pts) + { + OutPt* tmp = op2->next; + delete op2; + op2 = tmp; + } + delete outrec.pts; + outrec.pts = nullptr; + } + + + bool IntersectListSort(const IntersectNode& a, const IntersectNode& b) + { + //note different inequality tests ... + return (a.pt.y == b.pt.y) ? (a.pt.x < b.pt.x) : (a.pt.y > b.pt.y); + } + + + inline void SetSides(OutRec& outrec, Active& start_edge, Active& end_edge) + { + outrec.front_edge = &start_edge; + outrec.back_edge = &end_edge; + } + + + void SwapOutrecs(Active& e1, Active& e2) + { + OutRec* or1 = e1.outrec; + OutRec* or2 = e2.outrec; + if (or1 == or2) + { + Active* e = or1->front_edge; + or1->front_edge = or1->back_edge; + or1->back_edge = e; + return; + } + if (or1) + { + if (&e1 == or1->front_edge) + or1->front_edge = &e2; + else + or1->back_edge = &e2; + } + if (or2) + { + if (&e2 == or2->front_edge) + or2->front_edge = &e1; + else + or2->back_edge = &e1; + } + e1.outrec = or2; + e2.outrec = or1; + } + + + double Area(OutPt* op) + { + //https://en.wikipedia.org/wiki/Shoelace_formula + double result = 0.0; + OutPt* op2 = op; + do + { + result += static_cast(op2->prev->pt.y + op2->pt.y) * + static_cast(op2->prev->pt.x - op2->pt.x); + op2 = op2->next; + } while (op2 != op); + return result * 0.5; + } + + inline double AreaTriangle(const Point64& pt1, + const Point64& pt2, const Point64& pt3) + { + return (static_cast(pt3.y + pt1.y) * static_cast(pt3.x - pt1.x) + + static_cast(pt1.y + pt2.y) * static_cast(pt1.x - pt2.x) + + static_cast(pt2.y + pt3.y) * static_cast(pt2.x - pt3.x)); + } + + void ReverseOutPts(OutPt* op) + { + if (!op) return; + + OutPt* op1 = op; + OutPt* op2; + + do + { + op2 = op1->next; + op1->next = op1->prev; + op1->prev = op2; + op1 = op2; + } while (op1 != op); + } + + + inline void SwapSides(OutRec& outrec) + { + Active* e2 = outrec.front_edge; + outrec.front_edge = outrec.back_edge; + outrec.back_edge = e2; + outrec.pts = outrec.pts->next; + } + + + inline OutRec* GetRealOutRec(OutRec* outrec) + { + while (outrec && !outrec->pts) outrec = outrec->owner; + return outrec; + } + + + inline void UncoupleOutRec(Active ae) + { + OutRec* outrec = ae.outrec; + if (!outrec) return; + outrec->front_edge->outrec = nullptr; + outrec->back_edge->outrec = nullptr; + outrec->front_edge = nullptr; + outrec->back_edge = nullptr; + } + + + inline bool AreReallyClose(const Point64& pt1, const Point64& pt2) + { + return (std::llabs(pt1.x - pt2.x) < 2) && (std::llabs(pt1.y - pt2.y) < 2); + } + + + inline bool IsValidClosedPath(const OutPt* op) + { + return (op && op->next != op && op->next != op->prev && + //also treat inconsequential polygons as invalid + !(op->next->next == op->prev && + (AreReallyClose(op->pt, op->next->pt) || + AreReallyClose(op->pt, op->prev->pt)))); + } + + inline bool OutrecIsAscending(const Active* hotEdge) + { + return (hotEdge == hotEdge->outrec->front_edge); + } + + inline void SwapFrontBackSides(OutRec& outrec) + { + Active* tmp = outrec.front_edge; + outrec.front_edge = outrec.back_edge; + outrec.back_edge = tmp; + outrec.pts = outrec.pts->next; + } + + inline bool EdgesAdjacentInAEL(const IntersectNode& inode) + { + return (inode.edge1->next_in_ael == inode.edge2) || (inode.edge1->prev_in_ael == inode.edge2); + } + + inline bool TestJoinWithPrev1(const Active& e) + { + //this is marginally quicker than TestJoinWithPrev2 + //but can only be used when e.PrevInAEL.currX is accurate + return IsHotEdge(e) && !IsOpen(e) && + e.prev_in_ael && e.prev_in_ael->curr_x == e.curr_x && + IsHotEdge(*e.prev_in_ael) && !IsOpen(*e.prev_in_ael) && + (CrossProduct(e.prev_in_ael->top, e.bot, e.top) == 0); + } + + inline bool TestJoinWithPrev2(const Active& e, const Point64& curr_pt) + { + return IsHotEdge(e) && !IsOpen(e) && + e.prev_in_ael && !IsOpen(*e.prev_in_ael) && + IsHotEdge(*e.prev_in_ael) && (e.prev_in_ael->top.y < e.bot.y) && + (std::llabs(TopX(*e.prev_in_ael, curr_pt.y) - curr_pt.x) < 2) && + (CrossProduct(e.prev_in_ael->top, curr_pt, e.top) == 0); + } + + inline bool TestJoinWithNext1(const Active& e) + { + //this is marginally quicker than TestJoinWithNext2 + //but can only be used when e.NextInAEL.currX is accurate + return IsHotEdge(e) && !IsOpen(e) && + e.next_in_ael && (e.next_in_ael->curr_x == e.curr_x) && + IsHotEdge(*e.next_in_ael) && !IsOpen(*e.next_in_ael) && + (CrossProduct(e.next_in_ael->top, e.bot, e.top) == 0); + } + + inline bool TestJoinWithNext2(const Active& e, const Point64& curr_pt) + { + return IsHotEdge(e) && !IsOpen(e) && + e.next_in_ael && !IsOpen(*e.next_in_ael) && + IsHotEdge(*e.next_in_ael) && (e.next_in_ael->top.y < e.bot.y) && + (std::llabs(TopX(*e.next_in_ael, curr_pt.y) - curr_pt.x) < 2) && + (CrossProduct(e.next_in_ael->top, curr_pt, e.top) == 0); + } + + //------------------------------------------------------------------------------ + // ClipperBase methods ... + //------------------------------------------------------------------------------ + + ClipperBase::~ClipperBase() + { + Clear(); + } + + void ClipperBase::DeleteEdges(Active*& e) + { + while (e) + { + Active* e2 = e; + e = e->next_in_ael; + delete e2; + } + } + + void ClipperBase::CleanUp() + { + DeleteEdges(actives_); + scanline_list_ = std::priority_queue(); + intersect_nodes_.clear(); + DisposeAllOutRecs(); + } + + + void ClipperBase::Clear() + { + CleanUp(); + DisposeVerticesAndLocalMinima(); + current_locmin_iter_ = minima_list_.begin(); + minima_list_sorted_ = false; + has_open_paths_ = false; + } + + + void ClipperBase::Reset() + { + if (!minima_list_sorted_) + { + std::sort(minima_list_.begin(), minima_list_.end(), LocMinSorter()); + minima_list_sorted_ = true; + } + std::vector::const_reverse_iterator i; + for (i = minima_list_.rbegin(); i != minima_list_.rend(); ++i) + InsertScanline((*i)->vertex->pt.y); + + current_locmin_iter_ = minima_list_.begin(); + actives_ = nullptr; + sel_ = nullptr; + succeeded_ = true; + } + + +#ifdef USINGZ + void ClipperBase::SetZ(const Active& e1, const Active& e2, Point64& ip) + { + if (!zCallback_) return; + // prioritize subject over clip vertices by passing + // subject vertices before clip vertices in the callback + if (GetPolyType(e1) == PathType::Subject) + { + if (ip == e1.bot) ip.z = e1.bot.z; + else if (ip == e1.top) ip.z = e1.top.z; + else if (ip == e2.bot) ip.z = e2.bot.z; + else if (ip == e2.top) ip.z = e2.top.z; + zCallback_(e1.bot, e1.top, e2.bot, e2.top, ip); + } + else + { + if (ip == e2.bot) ip.z = e2.bot.z; + else if (ip == e2.top) ip.z = e2.top.z; + else if (ip == e1.bot) ip.z = e1.bot.z; + else if (ip == e1.top) ip.z = e1.top.z; + zCallback_(e2.bot, e2.top, e1.bot, e1.top, ip); + } + } +#endif + + void ClipperBase::AddPath(const Path64& path, PathType polytype, bool is_open) + { + Paths64 tmp; + tmp.push_back(path); + AddPaths(tmp, polytype, is_open); + } + + + void ClipperBase::AddPaths(const Paths64& paths, PathType polytype, bool is_open) + { + if (is_open) has_open_paths_ = true; + minima_list_sorted_ = false; + + Path64::size_type total_vertex_count = 0; + for (const Path64& path : paths) total_vertex_count += path.size(); + if (total_vertex_count == 0) return; + Vertex* vertices = new Vertex[total_vertex_count], *v = vertices; + for (const Path64& path : paths) + { + //for each path create a circular double linked list of vertices + Vertex *v0 = v, *curr_v = v, *prev_v = nullptr; + + v->prev = nullptr; + int cnt = 0; + for (const Point64& pt : path) + { + if (prev_v) + { + if (prev_v->pt == pt) continue; // ie skips duplicates + prev_v->next = curr_v; + } + curr_v->prev = prev_v; + curr_v->pt = pt; + curr_v->flags = VertexFlags::None; + prev_v = curr_v++; + cnt++; + } + if (!prev_v || !prev_v->prev) continue; + if (!is_open && prev_v->pt == v0->pt) + prev_v = prev_v->prev; + prev_v->next = v0; + v0->prev = prev_v; + v = curr_v; // ie get ready for next path + if (cnt < 2 || (cnt == 2 && !is_open)) continue; + + //now find and assign local minima + bool going_up, going_up0; + if (is_open) + { + curr_v = v0->next; + while (curr_v != v0 && curr_v->pt.y == v0->pt.y) + curr_v = curr_v->next; + going_up = curr_v->pt.y <= v0->pt.y; + if (going_up) + { + v0->flags = VertexFlags::OpenStart; + AddLocMin(*v0, polytype, true); + } + else + v0->flags = VertexFlags::OpenStart | VertexFlags::LocalMax; + } + else // closed path + { + prev_v = v0->prev; + while (prev_v != v0 && prev_v->pt.y == v0->pt.y) + prev_v = prev_v->prev; + if (prev_v == v0) + continue; // only open paths can be completely flat + going_up = prev_v->pt.y > v0->pt.y; + } + + going_up0 = going_up; + prev_v = v0; + curr_v = v0->next; + while (curr_v != v0) + { + if (curr_v->pt.y > prev_v->pt.y && going_up) + { + prev_v->flags = (prev_v->flags | VertexFlags::LocalMax); + going_up = false; + } + else if (curr_v->pt.y < prev_v->pt.y && !going_up) + { + going_up = true; + AddLocMin(*prev_v, polytype, is_open); + } + prev_v = curr_v; + curr_v = curr_v->next; + } + + if (is_open) + { + prev_v->flags = prev_v->flags | VertexFlags::OpenEnd; + if (going_up) + prev_v->flags = prev_v->flags | VertexFlags::LocalMax; + else + AddLocMin(*prev_v, polytype, is_open); + } + else if (going_up != going_up0) + { + if (going_up0) AddLocMin(*prev_v, polytype, false); + else prev_v->flags = prev_v->flags | VertexFlags::LocalMax; + } + } // end processing current path + + vertex_lists_.emplace_back(vertices); + } // end AddPaths + + + inline void ClipperBase::InsertScanline(int64_t y) + { + scanline_list_.push(y); + } + + + bool ClipperBase::PopScanline(int64_t& y) + { + if (scanline_list_.empty()) return false; + y = scanline_list_.top(); + scanline_list_.pop(); + while (!scanline_list_.empty() && y == scanline_list_.top()) + scanline_list_.pop(); // Pop duplicates. + return true; + } + + + bool ClipperBase::PopLocalMinima(int64_t y, LocalMinima*& local_minima) + { + if (current_locmin_iter_ == minima_list_.end() || (*current_locmin_iter_)->vertex->pt.y != y) return false; + local_minima = (*current_locmin_iter_++); + return true; + } + + + void ClipperBase::DisposeAllOutRecs() + { + for (auto outrec : outrec_list_) + { + if (outrec->pts) DisposeOutPts(*outrec); + delete outrec; + } + outrec_list_.resize(0); + } + + + void ClipperBase::DisposeVerticesAndLocalMinima() + { + for (auto lm : minima_list_) delete lm; + minima_list_.clear(); + for (auto v : vertex_lists_) delete[] v; + vertex_lists_.clear(); + } + + + void ClipperBase::AddLocMin(Vertex& vert, PathType polytype, bool is_open) + { + //make sure the vertex is added only once ... + if ((VertexFlags::LocalMin & vert.flags) != VertexFlags::None) return; + + vert.flags = (vert.flags | VertexFlags::LocalMin); + minima_list_.push_back(new LocalMinima(&vert, polytype, is_open)); + } + + bool ClipperBase::IsContributingClosed(const Active & e) const + { + switch (fillrule_) + { + case FillRule::EvenOdd: + break; + case FillRule::NonZero: + if (abs(e.wind_cnt) != 1) return false; + break; + case FillRule::Positive: + if (e.wind_cnt != 1) return false; + break; + case FillRule::Negative: + if (e.wind_cnt != -1) return false; + break; + } + + switch (cliptype_) + { + case ClipType::None: + return false; + case ClipType::Intersection: + switch (fillrule_) + { + case FillRule::Positive: + return (e.wind_cnt2 > 0); + case FillRule::Negative: + return (e.wind_cnt2 < 0); + default: + return (e.wind_cnt2 != 0); + } + break; + + case ClipType::Union: + switch (fillrule_) + { + case FillRule::Positive: + return (e.wind_cnt2 <= 0); + case FillRule::Negative: + return (e.wind_cnt2 >= 0); + default: + return (e.wind_cnt2 == 0); + } + break; + + case ClipType::Difference: + bool result; + switch (fillrule_) + { + case FillRule::Positive: + result = (e.wind_cnt2 <= 0); + break; + case FillRule::Negative: + result = (e.wind_cnt2 >= 0); + break; + default: + result = (e.wind_cnt2 == 0); + } + if (GetPolyType(e) == PathType::Subject) + return result; + else + return !result; + break; + + case ClipType::Xor: return true; break; + } + return false; // we should never get here + } + + + inline bool ClipperBase::IsContributingOpen(const Active& e) const + { + bool is_in_clip, is_in_subj; + switch (fillrule_) + { + case FillRule::Positive: + is_in_clip = e.wind_cnt2 > 0; + is_in_subj = e.wind_cnt > 0; + break; + case FillRule::Negative: + is_in_clip = e.wind_cnt2 < 0; + is_in_subj = e.wind_cnt < 0; + break; + default: + is_in_clip = e.wind_cnt2 != 0; + is_in_subj = e.wind_cnt != 0; + } + + switch (cliptype_) + { + case ClipType::Intersection: return is_in_clip; + case ClipType::Union: return (!is_in_subj && !is_in_clip); + default: return !is_in_clip; + } + } + + + void ClipperBase::SetWindCountForClosedPathEdge(Active& e) + { + //Wind counts refer to polygon regions not edges, so here an edge's WindCnt + //indicates the higher of the wind counts for the two regions touching the + //edge. (NB Adjacent regions can only ever have their wind counts differ by + //one. Also, open paths have no meaningful wind directions or counts.) + + Active* e2 = e.prev_in_ael; + //find the nearest closed path edge of the same PolyType in AEL (heading left) + PathType pt = GetPolyType(e); + while (e2 && (GetPolyType(*e2) != pt || IsOpen(*e2))) e2 = e2->prev_in_ael; + + if (!e2) + { + e.wind_cnt = e.wind_dx; + e2 = actives_; + } + else if (fillrule_ == FillRule::EvenOdd) + { + e.wind_cnt = e.wind_dx; + e.wind_cnt2 = e2->wind_cnt2; + e2 = e2->next_in_ael; + } + else + { + //NonZero, positive, or negative filling here ... + //if e's WindCnt is in the SAME direction as its WindDx, then polygon + //filling will be on the right of 'e'. + //NB neither e2.WindCnt nor e2.WindDx should ever be 0. + if (e2->wind_cnt * e2->wind_dx < 0) + { + //opposite directions so 'e' is outside 'e2' ... + if (abs(e2->wind_cnt) > 1) + { + //outside prev poly but still inside another. + if (e2->wind_dx * e.wind_dx < 0) + //reversing direction so use the same WC + e.wind_cnt = e2->wind_cnt; + else + //otherwise keep 'reducing' the WC by 1 (ie towards 0) ... + e.wind_cnt = e2->wind_cnt + e.wind_dx; + } + else + //now outside all polys of same polytype so set own WC ... + e.wind_cnt = (IsOpen(e) ? 1 : e.wind_dx); + } + else + { + //'e' must be inside 'e2' + if (e2->wind_dx * e.wind_dx < 0) + //reversing direction so use the same WC + e.wind_cnt = e2->wind_cnt; + else + //otherwise keep 'increasing' the WC by 1 (ie away from 0) ... + e.wind_cnt = e2->wind_cnt + e.wind_dx; + } + e.wind_cnt2 = e2->wind_cnt2; + e2 = e2->next_in_ael; // ie get ready to calc WindCnt2 + } + + //update wind_cnt2 ... + if (fillrule_ == FillRule::EvenOdd) + while (e2 != &e) + { + if (GetPolyType(*e2) != pt && !IsOpen(*e2)) + e.wind_cnt2 = (e.wind_cnt2 == 0 ? 1 : 0); + e2 = e2->next_in_ael; + } + else + while (e2 != &e) + { + if (GetPolyType(*e2) != pt && !IsOpen(*e2)) + e.wind_cnt2 += e2->wind_dx; + e2 = e2->next_in_ael; + } + } + + + void ClipperBase::SetWindCountForOpenPathEdge(Active& e) + { + Active* e2 = actives_; + if (fillrule_ == FillRule::EvenOdd) + { + int cnt1 = 0, cnt2 = 0; + while (e2 != &e) + { + if (GetPolyType(*e2) == PathType::Clip) + cnt2++; + else if (!IsOpen(*e2)) + cnt1++; + e2 = e2->next_in_ael; + } + e.wind_cnt = (IsOdd(cnt1) ? 1 : 0); + e.wind_cnt2 = (IsOdd(cnt2) ? 1 : 0); + } + else + { + while (e2 != &e) + { + if (GetPolyType(*e2) == PathType::Clip) + e.wind_cnt2 += e2->wind_dx; + else if (!IsOpen(*e2)) + e.wind_cnt += e2->wind_dx; + e2 = e2->next_in_ael; + } + } + } + + + bool IsValidAelOrder(const Active& resident, const Active& newcomer) + { + if (newcomer.curr_x != resident.curr_x) + return newcomer.curr_x > resident.curr_x; + + //get the turning direction a1.top, a2.bot, a2.top + double d = CrossProduct(resident.top, newcomer.bot, newcomer.top); + if (d != 0) return d < 0; + + //edges must be collinear to get here + //for starting open paths, place them according to + //the direction they're about to turn + if (!IsMaxima(resident) && (resident.top.y > newcomer.top.y)) + { + return CrossProduct(newcomer.bot, + resident.top, NextVertex(resident)->pt) <= 0; + } + else if (!IsMaxima(newcomer) && (newcomer.top.y > resident.top.y)) + { + return CrossProduct(newcomer.bot, + newcomer.top, NextVertex(newcomer)->pt) >= 0; + } + + int64_t y = newcomer.bot.y; + bool newcomerIsLeft = newcomer.is_left_bound; + + if (resident.bot.y != y || resident.local_min->vertex->pt.y != y) + return newcomer.is_left_bound; + //resident must also have just been inserted + else if (resident.is_left_bound != newcomerIsLeft) + return newcomerIsLeft; + else if (CrossProduct(PrevPrevVertex(resident)->pt, + resident.bot, resident.top) == 0) return true; + else + //compare turning direction of the alternate bound + return (CrossProduct(PrevPrevVertex(resident)->pt, + newcomer.bot, PrevPrevVertex(newcomer)->pt) > 0) == newcomerIsLeft; + } + + + void ClipperBase::InsertLeftEdge(Active& e) + { + Active* e2; + if (!actives_) + { + e.prev_in_ael = nullptr; + e.next_in_ael = nullptr; + actives_ = &e; + } + else if (!IsValidAelOrder(*actives_, e)) + { + e.prev_in_ael = nullptr; + e.next_in_ael = actives_; + actives_->prev_in_ael = &e; + actives_ = &e; + } + else + { + e2 = actives_; + while (e2->next_in_ael && IsValidAelOrder(*e2->next_in_ael, e)) + e2 = e2->next_in_ael; + e.next_in_ael = e2->next_in_ael; + if (e2->next_in_ael) e2->next_in_ael->prev_in_ael = &e; + e.prev_in_ael = e2; + e2->next_in_ael = &e; + } + } + + + void InsertRightEdge(Active& e, Active& e2) + { + e2.next_in_ael = e.next_in_ael; + if (e.next_in_ael) e.next_in_ael->prev_in_ael = &e2; + e2.prev_in_ael = &e; + e.next_in_ael = &e2; + } + + + void ClipperBase::InsertLocalMinimaIntoAEL(int64_t bot_y) + { + LocalMinima* local_minima; + Active* left_bound, * right_bound; + //Add any local minima (if any) at BotY ... + //nb: horizontal local minima edges should contain locMin.vertex.prev + + while (PopLocalMinima(bot_y, local_minima)) + { + if ((local_minima->vertex->flags & VertexFlags::OpenStart) != VertexFlags::None) + { + left_bound = nullptr; + } + else + { + left_bound = new Active(); + left_bound->bot = local_minima->vertex->pt; + left_bound->curr_x = left_bound->bot.x; + left_bound->wind_cnt = 0, + left_bound->wind_cnt2 = 0, + left_bound->wind_dx = -1, + left_bound->vertex_top = local_minima->vertex->prev; // ie descending + left_bound->top = left_bound->vertex_top->pt; + left_bound->outrec = nullptr; + left_bound->local_min = local_minima; + SetDx(*left_bound); + } + + if ((local_minima->vertex->flags & VertexFlags::OpenEnd) != VertexFlags::None) + { + right_bound = nullptr; + } + else + { + right_bound = new Active(); + right_bound->bot = local_minima->vertex->pt; + right_bound->curr_x = right_bound->bot.x; + right_bound->wind_cnt = 0, + right_bound->wind_cnt2 = 0, + right_bound->wind_dx = 1, + right_bound->vertex_top = local_minima->vertex->next; // ie ascending + right_bound->top = right_bound->vertex_top->pt; + right_bound->outrec = nullptr; + right_bound->local_min = local_minima; + SetDx(*right_bound); + } + + //Currently LeftB is just the descending bound and RightB is the ascending. + //Now if the LeftB isn't on the left of RightB then we need swap them. + if (left_bound && right_bound) + { + if (IsHorizontal(*left_bound)) + { + if (IsHeadingRightHorz(*left_bound)) SwapActives(left_bound, right_bound); + } + else if (IsHorizontal(*right_bound)) + { + if (IsHeadingLeftHorz(*right_bound)) SwapActives(left_bound, right_bound); + } + else if (left_bound->dx < right_bound->dx) + SwapActives(left_bound, right_bound); + } + else if (!left_bound) + { + left_bound = right_bound; + right_bound = nullptr; + } + + bool contributing; + left_bound->is_left_bound = true; + InsertLeftEdge(*left_bound); + + if (IsOpen(*left_bound)) + { + SetWindCountForOpenPathEdge(*left_bound); + contributing = IsContributingOpen(*left_bound); + } + else + { + SetWindCountForClosedPathEdge(*left_bound); + contributing = IsContributingClosed(*left_bound); + } + + if (right_bound) + { + right_bound->is_left_bound = false; + right_bound->wind_cnt = left_bound->wind_cnt; + right_bound->wind_cnt2 = left_bound->wind_cnt2; + InsertRightEdge(*left_bound, *right_bound); /////// + if (contributing) + { + AddLocalMinPoly(*left_bound, *right_bound, left_bound->bot, true); + if (!IsHorizontal(*left_bound) && TestJoinWithPrev1(*left_bound)) + { + OutPt* op = AddOutPt(*left_bound->prev_in_ael, left_bound->bot); + AddJoin(op, left_bound->outrec->pts); + } + } + + while (right_bound->next_in_ael && + IsValidAelOrder(*right_bound->next_in_ael, *right_bound)) + { + IntersectEdges(*right_bound, *right_bound->next_in_ael, right_bound->bot); + SwapPositionsInAEL(*right_bound, *right_bound->next_in_ael); + } + + if (!IsHorizontal(*right_bound) && + TestJoinWithNext1(*right_bound)) + { + OutPt* op = AddOutPt(*right_bound->next_in_ael, right_bound->bot); + AddJoin(right_bound->outrec->pts, op); + } + + if (IsHorizontal(*right_bound)) + PushHorz(*right_bound); + else + InsertScanline(right_bound->top.y); + } + else if (contributing) + { + StartOpenPath(*left_bound, left_bound->bot); + } + + if (IsHorizontal(*left_bound)) + PushHorz(*left_bound); + else + InsertScanline(left_bound->top.y); + } // while (PopLocalMinima()) + } + + + inline void ClipperBase::PushHorz(Active& e) + { + e.next_in_sel = (sel_ ? sel_ : nullptr); + sel_ = &e; + } + + + inline bool ClipperBase::PopHorz(Active*& e) + { + e = sel_; + if (!e) return false; + sel_ = sel_->next_in_sel; + return true; + } + + + OutPt* ClipperBase::AddLocalMinPoly(Active& e1, Active& e2, + const Point64& pt, bool is_new) + { + OutRec* outrec = new OutRec(); + outrec->idx = (unsigned)outrec_list_.size(); + outrec_list_.push_back(outrec); + outrec->pts = nullptr; + outrec->polypath = nullptr; + e1.outrec = outrec; + e2.outrec = outrec; + + //Setting the owner and inner/outer states (above) is an essential + //precursor to setting edge 'sides' (ie left and right sides of output + //polygons) and hence the orientation of output paths ... + + if (IsOpen(e1)) + { + outrec->owner = nullptr; + outrec->is_open = true; + if (e1.wind_dx > 0) + SetSides(*outrec, e1, e2); + else + SetSides(*outrec, e2, e1); + } + else + { + Active* prevHotEdge = GetPrevHotEdge(e1); + //e.windDx is the winding direction of the **input** paths + //and unrelated to the winding direction of output polygons. + //Output orientation is determined by e.outrec.frontE which is + //the ascending edge (see AddLocalMinPoly). + if (prevHotEdge) + { + outrec->owner = prevHotEdge->outrec; + if (OutrecIsAscending(prevHotEdge) == is_new) + SetSides(*outrec, e2, e1); + else + SetSides(*outrec, e1, e2); + } + else + { + outrec->owner = nullptr; + if (is_new) + SetSides(*outrec, e1, e2); + else + SetSides(*outrec, e2, e1); + } + } + + OutPt* op = new OutPt(pt, outrec); + outrec->pts = op; + return op; + } + + + OutPt* ClipperBase::AddLocalMaxPoly(Active& e1, Active& e2, const Point64& pt) + { + if (IsFront(e1) == IsFront(e2)) + { + if (IsOpenEnd(e1)) + SwapFrontBackSides(*e1.outrec); + else if (IsOpenEnd(e2)) + SwapFrontBackSides(*e2.outrec); + else + { + succeeded_ = false; + return nullptr; + } + } + + OutPt* result = AddOutPt(e1, pt); + if (e1.outrec == e2.outrec) + { + OutRec& outrec = *e1.outrec; + outrec.pts = result; + + UncoupleOutRec(e1); + if (!IsOpen(e1)) CleanCollinear(&outrec); + result = outrec.pts; + if (using_polytree_ && outrec.owner && !outrec.owner->front_edge) + outrec.owner = GetRealOutRec(outrec.owner->owner); + } + //and to preserve the winding orientation of outrec ... + else if (IsOpen(e1)) + { + if (e1.wind_dx < 0) + JoinOutrecPaths(e1, e2); + else + JoinOutrecPaths(e2, e1); + } + else if (e1.outrec->idx < e2.outrec->idx) + JoinOutrecPaths(e1, e2); + else + JoinOutrecPaths(e2, e1); + + return result; + } + + void ClipperBase::JoinOutrecPaths(Active& e1, Active& e2) + { + //join e2 outrec path onto e1 outrec path and then delete e2 outrec path + //pointers. (NB Only very rarely do the joining ends share the same coords.) + OutPt* p1_st = e1.outrec->pts; + OutPt* p2_st = e2.outrec->pts; + OutPt* p1_end = p1_st->next; + OutPt* p2_end = p2_st->next; + if (IsFront(e1)) + { + p2_end->prev = p1_st; + p1_st->next = p2_end; + p2_st->next = p1_end; + p1_end->prev = p2_st; + e1.outrec->pts = p2_st; + e1.outrec->front_edge = e2.outrec->front_edge; + if (e1.outrec->front_edge) + e1.outrec->front_edge->outrec = e1.outrec; + } + else + { + p1_end->prev = p2_st; + p2_st->next = p1_end; + p1_st->next = p2_end; + p2_end->prev = p1_st; + e1.outrec->back_edge = e2.outrec->back_edge; + if (e1.outrec->back_edge) + e1.outrec->back_edge->outrec = e1.outrec; + } + + //an owner must have a lower idx otherwise + //it can't be a valid owner + if (e2.outrec->owner && e2.outrec->owner->idx < e1.outrec->idx) + { + if (!e1.outrec->owner || e2.outrec->owner->idx < e1.outrec->owner->idx) + e1.outrec->owner = e2.outrec->owner; + } + + //after joining, the e2.OutRec must contains no vertices ... + e2.outrec->front_edge = nullptr; + e2.outrec->back_edge = nullptr; + e2.outrec->pts = nullptr; + e2.outrec->owner = e1.outrec; + + if (IsOpenEnd(e1)) + { + e2.outrec->pts = e1.outrec->pts; + e1.outrec->pts = nullptr; + } + + //and e1 and e2 are maxima and are about to be dropped from the Actives list. + e1.outrec = nullptr; + e2.outrec = nullptr; + } + + + OutPt* ClipperBase::AddOutPt(const Active& e, const Point64& pt) + { + OutPt* new_op = nullptr; + + //Outrec.OutPts: a circular doubly-linked-list of POutPt where ... + //op_front[.Prev]* ~~~> op_back & op_back == op_front.Next + OutRec* outrec = e.outrec; + bool to_front = IsFront(e); + OutPt* op_front = outrec->pts; + OutPt* op_back = op_front->next; + + if (to_front && (pt == op_front->pt)) + new_op = op_front; + else if (!to_front && (pt == op_back->pt)) + new_op = op_back; + else + { + new_op = new OutPt(pt, outrec); + op_back->prev = new_op; + new_op->prev = op_front; + new_op->next = op_back; + op_front->next = new_op; + if (to_front) outrec->pts = new_op; + } + return new_op; + } + + + bool ClipperBase::ValidateClosedPathEx(OutPt*& outpt) + { + if (IsValidClosedPath(outpt)) return true; + if (outpt) SafeDisposeOutPts(outpt); + return false; + } + + + void ClipperBase::CleanCollinear(OutRec* outrec) + { + outrec = GetRealOutRec(outrec); + if (!outrec || outrec->is_open || + outrec->front_edge || !ValidateClosedPathEx(outrec->pts)) return; + + OutPt* startOp = outrec->pts, * op2 = startOp; + for (; ; ) + { + if (op2->joiner) return; + + //NB if preserveCollinear == true, then only remove 180 deg. spikes + if ((CrossProduct(op2->prev->pt, op2->pt, op2->next->pt) == 0) && + (op2->pt == op2->prev->pt || + op2->pt == op2->next->pt || !PreserveCollinear || + DotProduct(op2->prev->pt, op2->pt, op2->next->pt) < 0)) + { + + if (op2 == outrec->pts) outrec->pts = op2->prev; + + op2 = DisposeOutPt(op2); + if (!ValidateClosedPathEx(op2)) + { + outrec->pts = nullptr; + return; + } + startOp = op2; + continue; + } + op2 = op2->next; + if (op2 == startOp) break; + } + FixSelfIntersects(outrec); + } + + OutPt* ClipperBase::DoSplitOp(OutPt* outRecOp, OutPt* splitOp) + { + OutPt* prevOp = splitOp->prev; + OutPt* nextNextOp = splitOp->next->next; + OutPt* result = prevOp; + PointD ipD; + GetIntersectPoint(prevOp->pt, + splitOp->pt, splitOp->next->pt, nextNextOp->pt, ipD); + Point64 ip = Point64(ipD); +#ifdef USINGZ + if (zCallback_) + zCallback_(prevOp->pt, splitOp->pt, splitOp->next->pt, nextNextOp->pt, ip); +#endif + double area1 = Area(outRecOp); + double area2 = AreaTriangle(ip, splitOp->pt, splitOp->next->pt); + + if (ip == prevOp->pt || ip == nextNextOp->pt) + { + nextNextOp->prev = prevOp; + prevOp->next = nextNextOp; + } + else + { + OutPt* newOp2 = new OutPt(ip, prevOp->outrec); + newOp2->prev = prevOp; + newOp2->next = nextNextOp; + nextNextOp->prev = newOp2; + prevOp->next = newOp2; + } + + SafeDeleteOutPtJoiners(splitOp->next); + SafeDeleteOutPtJoiners(splitOp); + + double absArea2 = std::abs(area2); + if ((absArea2 >= 1) && + ((absArea2 > std::abs(area1) || ((area2 > 0) == (area1 > 0))))) + { + OutRec* newOutRec = new OutRec(); + newOutRec->idx = outrec_list_.size(); + outrec_list_.push_back(newOutRec); + newOutRec->owner = prevOp->outrec->owner; + newOutRec->polypath = nullptr; + splitOp->outrec = newOutRec; + splitOp->next->outrec = newOutRec; + + OutPt* newOp = new OutPt(ip, newOutRec); + newOp->prev = splitOp->next; + newOp->next = splitOp; + newOutRec->pts = newOp; + splitOp->prev = newOp; + splitOp->next->next = newOp; + } + else + { + delete splitOp->next; + delete splitOp; + } + return result; + } + + + void ClipperBase::FixSelfIntersects(OutRec* outrec) + { + OutPt* op2 = outrec->pts; + for (; ; ) + { + // triangles can't self-intersect + if (op2->prev == op2->next->next) break; + if (SegmentsIntersect(op2->prev->pt, + op2->pt, op2->next->pt, op2->next->next->pt)) + { + if (op2 == outrec->pts || op2->next == outrec->pts) + outrec->pts = outrec->pts->prev; + op2 = DoSplitOp(outrec->pts, op2); + outrec->pts = op2; + continue; + } + else + op2 = op2->next; + + if (op2 == outrec->pts) break; + } + } + + + inline void UpdateOutrecOwner(OutRec* outrec) + { + OutPt* opCurr = outrec->pts; + for (; ; ) + { + opCurr->outrec = outrec; + opCurr = opCurr->next; + if (opCurr == outrec->pts) return; + } + } + + + void ClipperBase::SafeDisposeOutPts(OutPt*& op) + { + OutRec* outrec = GetRealOutRec(op->outrec); + if (outrec->front_edge) + outrec->front_edge->outrec = nullptr; + if (outrec->back_edge) + outrec->back_edge->outrec = nullptr; + + op->prev->next = nullptr; + while (op) + { + SafeDeleteOutPtJoiners(op); + OutPt* tmp = op->next; + delete op; + op = tmp; + } + outrec->pts = nullptr; + } + + + void ClipperBase::CompleteSplit(OutPt* op1, OutPt* op2, OutRec& outrec) + { + double area1 = Area(op1); + double area2 = Area(op2); + bool signs_change = (area1 > 0) == (area2 < 0); + + if (area1 == 0 || (signs_change && std::abs(area1) < 2)) + { + SafeDisposeOutPts(op1); + outrec.pts = op2; + } + else if (area2 == 0 || (signs_change && std::abs(area2) < 2)) + { + SafeDisposeOutPts(op2); + outrec.pts = op1; + } + else + { + OutRec* newOr = new OutRec(); + newOr->idx = outrec_list_.size(); + outrec_list_.push_back(newOr); + newOr->polypath = nullptr; + + if (using_polytree_) + { + if (!outrec.splits) outrec.splits = new OutRecList(); + outrec.splits->push_back(newOr); + } + + if (std::abs(area1) >= std::abs(area2)) + { + outrec.pts = op1; + newOr->pts = op2; + } + else + { + outrec.pts = op2; + newOr->pts = op1; + } + + if ((area1 > 0) == (area2 > 0)) + newOr->owner = outrec.owner; + else + newOr->owner = &outrec; + + UpdateOutrecOwner(newOr); + CleanCollinear(newOr); + } + } + + + OutPt* ClipperBase::StartOpenPath(Active& e, const Point64& pt) + { + OutRec* outrec = new OutRec(); + outrec->idx = outrec_list_.size(); + outrec_list_.push_back(outrec); + outrec->owner = nullptr; + outrec->is_open = true; + outrec->pts = nullptr; + outrec->polypath = nullptr; + + if (e.wind_dx > 0) + { + outrec->front_edge = &e; + outrec->back_edge = nullptr; + } + else + { + outrec->front_edge = nullptr; + outrec->back_edge =& e; + } + + e.outrec = outrec; + + OutPt* op = new OutPt(pt, outrec); + outrec->pts = op; + return op; + } + + + inline void ClipperBase::UpdateEdgeIntoAEL(Active* e) + { + e->bot = e->top; + e->vertex_top = NextVertex(*e); + e->top = e->vertex_top->pt; + e->curr_x = e->bot.x; + SetDx(*e); + if (IsHorizontal(*e)) return; + InsertScanline(e->top.y); + if (TestJoinWithPrev1(*e)) + { + OutPt* op1 = AddOutPt(*e->prev_in_ael, e->bot); + OutPt* op2 = AddOutPt(*e, e->bot); + AddJoin(op1, op2); + } + } + + + Active* FindEdgeWithMatchingLocMin(Active* e) + { + Active* result = e->next_in_ael; + while (result) + { + if (result->local_min == e->local_min) return result; + else if (!IsHorizontal(*result) && e->bot != result->bot) result = nullptr; + else result = result->next_in_ael; + } + result = e->prev_in_ael; + while (result) + { + if (result->local_min == e->local_min) return result; + else if (!IsHorizontal(*result) && e->bot != result->bot) return nullptr; + else result = result->prev_in_ael; + } + return result; + } + + + OutPt* ClipperBase::IntersectEdges(Active& e1, Active& e2, const Point64& pt) + { + //MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... + if (has_open_paths_ && (IsOpen(e1) || IsOpen(e2))) + { + if (IsOpen(e1) && IsOpen(e2)) return nullptr; + + Active* edge_o, * edge_c; + if (IsOpen(e1)) + { + edge_o = &e1; + edge_c = &e2; + } + else + { + edge_o = &e2; + edge_c = &e1; + } + + if (abs(edge_c->wind_cnt) != 1) return nullptr; + switch (cliptype_) + { + case ClipType::Union: + if (!IsHotEdge(*edge_c)) return nullptr; + break; + default: + if (edge_c->local_min->polytype == PathType::Subject) + return nullptr; + } + + switch (fillrule_) + { + case FillRule::Positive: if (edge_c->wind_cnt != 1) return nullptr; break; + case FillRule::Negative: if (edge_c->wind_cnt != -1) return nullptr; break; + default: if (std::abs(edge_c->wind_cnt) != 1) return nullptr; break; + } + + //toggle contribution ... + if (IsHotEdge(*edge_o)) + { + OutPt* resultOp = AddOutPt(*edge_o, pt); +#ifdef USINGZ + if (zCallback_) SetZ(e1, e2, resultOp->pt); +#endif + if (IsFront(*edge_o)) edge_o->outrec->front_edge = nullptr; + else edge_o->outrec->back_edge = nullptr; + edge_o->outrec = nullptr; + return resultOp; + } + + //horizontal edges can pass under open paths at a LocMins + else if (pt == edge_o->local_min->vertex->pt && + !IsOpenEnd(*edge_o->local_min->vertex)) + { + //find the other side of the LocMin and + //if it's 'hot' join up with it ... + Active* e3 = FindEdgeWithMatchingLocMin(edge_o); + if (e3 && IsHotEdge(*e3)) + { + edge_o->outrec = e3->outrec; + if (edge_o->wind_dx > 0) + SetSides(*e3->outrec, *edge_o, *e3); + else + SetSides(*e3->outrec, *e3, *edge_o); + return e3->outrec->pts; + } + else + return StartOpenPath(*edge_o, pt); + } + else + return StartOpenPath(*edge_o, pt); + } + + + //MANAGING CLOSED PATHS FROM HERE ON + + //UPDATE WINDING COUNTS... + + int old_e1_windcnt, old_e2_windcnt; + if (e1.local_min->polytype == e2.local_min->polytype) + { + if (fillrule_ == FillRule::EvenOdd) + { + old_e1_windcnt = e1.wind_cnt; + e1.wind_cnt = e2.wind_cnt; + e2.wind_cnt = old_e1_windcnt; + } + else + { + if (e1.wind_cnt + e2.wind_dx == 0) + e1.wind_cnt = -e1.wind_cnt; + else + e1.wind_cnt += e2.wind_dx; + if (e2.wind_cnt - e1.wind_dx == 0) + e2.wind_cnt = -e2.wind_cnt; + else + e2.wind_cnt -= e1.wind_dx; + } + } + else + { + if (fillrule_ != FillRule::EvenOdd) + { + e1.wind_cnt2 += e2.wind_dx; + e2.wind_cnt2 -= e1.wind_dx; + } + else + { + e1.wind_cnt2 = (e1.wind_cnt2 == 0 ? 1 : 0); + e2.wind_cnt2 = (e2.wind_cnt2 == 0 ? 1 : 0); + } + } + + switch (fillrule_) + { + case FillRule::EvenOdd: + case FillRule::NonZero: + old_e1_windcnt = abs(e1.wind_cnt); + old_e2_windcnt = abs(e2.wind_cnt); + break; + default: + if (fillrule_ == fillpos) + { + old_e1_windcnt = e1.wind_cnt; + old_e2_windcnt = e2.wind_cnt; + } + else + { + old_e1_windcnt = -e1.wind_cnt; + old_e2_windcnt = -e2.wind_cnt; + } + break; + } + + const bool e1_windcnt_in_01 = old_e1_windcnt == 0 || old_e1_windcnt == 1; + const bool e2_windcnt_in_01 = old_e2_windcnt == 0 || old_e2_windcnt == 1; + + if ((!IsHotEdge(e1) && !e1_windcnt_in_01) || (!IsHotEdge(e2) && !e2_windcnt_in_01)) + { + return nullptr; + } + + //NOW PROCESS THE INTERSECTION ... + OutPt* resultOp = nullptr; + //if both edges are 'hot' ... + if (IsHotEdge(e1) && IsHotEdge(e2)) + { + if ((old_e1_windcnt != 0 && old_e1_windcnt != 1) || (old_e2_windcnt != 0 && old_e2_windcnt != 1) || + (e1.local_min->polytype != e2.local_min->polytype && cliptype_ != ClipType::Xor)) + { + resultOp = AddLocalMaxPoly(e1, e2, pt); +#ifdef USINGZ + if (zCallback_ && resultOp) SetZ(e1, e2, resultOp->pt); +#endif + } + else if (IsFront(e1) || (e1.outrec == e2.outrec)) + { + //this 'else if' condition isn't strictly needed but + //it's sensible to split polygons that ony touch at + //a common vertex (not at common edges). + + resultOp = AddLocalMaxPoly(e1, e2, pt); + OutPt* op2 = AddLocalMinPoly(e1, e2, pt); +#ifdef USINGZ + if (zCallback_ && resultOp) SetZ(e1, e2, resultOp->pt); + if (zCallback_) SetZ(e1, e2, op2->pt); +#endif + if (resultOp && resultOp->pt == op2->pt && + !IsHorizontal(e1) && !IsHorizontal(e2) && + (CrossProduct(e1.bot, resultOp->pt, e2.bot) == 0)) + AddJoin(resultOp, op2); + } + else + { + resultOp = AddOutPt(e1, pt); +#ifdef USINGZ + OutPt* op2 = AddOutPt(e2, pt); + if (zCallback_) + { + SetZ(e1, e2, resultOp->pt); + SetZ(e1, e2, op2->pt); + } +#else + AddOutPt(e2, pt); +#endif + SwapOutrecs(e1, e2); + } + } + else if (IsHotEdge(e1)) + { + resultOp = AddOutPt(e1, pt); +#ifdef USINGZ + if (zCallback_) SetZ(e1, e2, resultOp->pt); +#endif + SwapOutrecs(e1, e2); + } + else if (IsHotEdge(e2)) + { + resultOp = AddOutPt(e2, pt); +#ifdef USINGZ + if (zCallback_) SetZ(e1, e2, resultOp->pt); +#endif + SwapOutrecs(e1, e2); + } + else + { + int64_t e1Wc2, e2Wc2; + switch (fillrule_) + { + case FillRule::EvenOdd: + case FillRule::NonZero: + e1Wc2 = abs(e1.wind_cnt2); + e2Wc2 = abs(e2.wind_cnt2); + break; + default: + if (fillrule_ == fillpos) + { + e1Wc2 = e1.wind_cnt2; + e2Wc2 = e2.wind_cnt2; + } + else + { + e1Wc2 = -e1.wind_cnt2; + e2Wc2 = -e2.wind_cnt2; + } + break; + } + + if (!IsSamePolyType(e1, e2)) + { + resultOp = AddLocalMinPoly(e1, e2, pt, false); +#ifdef USINGZ + if (zCallback_) SetZ(e1, e2, resultOp->pt); +#endif + } + else if (old_e1_windcnt == 1 && old_e2_windcnt == 1) + { + resultOp = nullptr; + switch (cliptype_) + { + case ClipType::Union: + if (e1Wc2 <= 0 && e2Wc2 <= 0) + resultOp = AddLocalMinPoly(e1, e2, pt, false); + break; + case ClipType::Difference: + if (((GetPolyType(e1) == PathType::Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((GetPolyType(e1) == PathType::Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + { + resultOp = AddLocalMinPoly(e1, e2, pt, false); + } + break; + case ClipType::Xor: + resultOp = AddLocalMinPoly(e1, e2, pt, false); + break; + default: + if (e1Wc2 > 0 && e2Wc2 > 0) + resultOp = AddLocalMinPoly(e1, e2, pt, false); + break; + } +#ifdef USINGZ + if (resultOp && zCallback_) SetZ(e1, e2, resultOp->pt); +#endif + } + } + return resultOp; + } + + + inline void ClipperBase::DeleteFromAEL(Active& e) + { + Active* prev = e.prev_in_ael; + Active* next = e.next_in_ael; + if (!prev && !next && (&e != actives_)) return; // already deleted + if (prev) + prev->next_in_ael = next; + else + actives_ = next; + if (next) next->prev_in_ael = prev; + delete& e; + } + + + inline void ClipperBase::AdjustCurrXAndCopyToSEL(const int64_t top_y) + { + Active* e = actives_; + sel_ = e; + while (e) + { + e->prev_in_sel = e->prev_in_ael; + e->next_in_sel = e->next_in_ael; + e->jump = e->next_in_sel; + e->curr_x = TopX(*e, top_y); + e = e->next_in_ael; + } + } + + + bool ClipperBase::ExecuteInternal(ClipType ct, FillRule fillrule, bool use_polytrees) + { + cliptype_ = ct; + fillrule_ = fillrule; + using_polytree_ = use_polytrees; + Reset(); + int64_t y; + if (ct == ClipType::None || !PopScanline(y)) return true; + + while (succeeded_) + { + InsertLocalMinimaIntoAEL(y); + Active* e; + while (PopHorz(e)) DoHorizontal(*e); + if (horz_joiners_) ConvertHorzTrialsToJoins(); + bot_y_ = y; // bot_y_ == bottom of scanbeam + if (!PopScanline(y)) break; // y new top of scanbeam + DoIntersections(y); + DoTopOfScanbeam(y); + while (PopHorz(e)) DoHorizontal(*e); + } + ProcessJoinerList(); + return succeeded_; + } + + + bool ClipperBase::Execute(ClipType clip_type, + FillRule fill_rule, Paths64& solution_closed) + { + solution_closed.clear(); + if (ExecuteInternal(clip_type, fill_rule, false)) + BuildPaths(solution_closed, nullptr); + CleanUp(); + return succeeded_; + } + + + bool ClipperBase::Execute(ClipType clip_type, FillRule fill_rule, + Paths64& solution_closed, Paths64& solution_open) + { + solution_closed.clear(); + solution_open.clear(); + if (ExecuteInternal(clip_type, fill_rule, false)) + BuildPaths(solution_closed, &solution_open); + CleanUp(); + return succeeded_; + } + + + bool ClipperBase::Execute(ClipType clip_type, FillRule fill_rule, PolyTree64& polytree) + { + Paths64 dummy; + polytree.Clear(); + if (ExecuteInternal(clip_type, fill_rule, true)) + BuildTree(polytree, dummy); + CleanUp(); + return succeeded_; + } + + bool ClipperBase::Execute(ClipType clip_type, + FillRule fill_rule, PolyTree64& polytree, Paths64& solution_open) + { + polytree.Clear(); + solution_open.clear(); + if (ExecuteInternal(clip_type, fill_rule, true)) + BuildTree(polytree, solution_open); + CleanUp(); + return succeeded_; + } + + + void ClipperBase::DoIntersections(const int64_t top_y) + { + if (BuildIntersectList(top_y)) + { + ProcessIntersectList(); + intersect_nodes_.clear(); + } + } + + void ClipperBase::AddNewIntersectNode(Active& e1, Active& e2, int64_t top_y) + { + Point64 pt = GetIntersectPoint(e1, e2); + + //rounding errors can occasionally place the calculated intersection + //point either below or above the scanbeam, so check and correct ... + if (pt.y > bot_y_) + { + //e.curr.y is still the bottom of scanbeam + pt.y = bot_y_; + //use the more vertical of the 2 edges to derive pt.x ... + if (abs(e1.dx) < abs(e2.dx)) + pt.x = TopX(e1, bot_y_); + else + pt.x = TopX(e2, bot_y_); + } + else if (pt.y < top_y) + { + //top_y is at the top of the scanbeam + pt.y = top_y; + if (e1.top.y == top_y) + pt.x = e1.top.x; + else if (e2.top.y == top_y) + pt.x = e2.top.x; + else if (abs(e1.dx) < abs(e2.dx)) + pt.x = e1.curr_x; + else + pt.x = e2.curr_x; + } + + intersect_nodes_.push_back(IntersectNode(&e1, &e2, pt)); + } + + + bool ClipperBase::BuildIntersectList(const int64_t top_y) + { + if (!actives_ || !actives_->next_in_ael) return false; + + //Calculate edge positions at the top of the current scanbeam, and from this + //we will determine the intersections required to reach these new positions. + AdjustCurrXAndCopyToSEL(top_y); + //Find all edge intersections in the current scanbeam using a stable merge + //sort that ensures only adjacent edges are intersecting. Intersect info is + //stored in FIntersectList ready to be processed in ProcessIntersectList. + //Re merge sorts see https://stackoverflow.com/a/46319131/359538 + + Active* left = sel_, * right, * l_end, * r_end, * curr_base, * tmp; + + while (left && left->jump) + { + Active* prev_base = nullptr; + while (left && left->jump) + { + curr_base = left; + right = left->jump; + l_end = right; + r_end = right->jump; + left->jump = r_end; + while (left != l_end && right != r_end) + { + if (right->curr_x < left->curr_x) + { + tmp = right->prev_in_sel; + for (; ; ) + { + AddNewIntersectNode(*tmp, *right, top_y); + if (tmp == left) break; + tmp = tmp->prev_in_sel; + } + + tmp = right; + right = ExtractFromSEL(tmp); + l_end = right; + Insert1Before2InSEL(tmp, left); + if (left == curr_base) + { + curr_base = tmp; + curr_base->jump = r_end; + if (!prev_base) sel_ = curr_base; + else prev_base->jump = curr_base; + } + } + else left = left->next_in_sel; + } + prev_base = curr_base; + left = r_end; + } + left = sel_; + } + return intersect_nodes_.size() > 0; + } + + void ClipperBase::ProcessIntersectList() + { + //We now have a list of intersections required so that edges will be + //correctly positioned at the top of the scanbeam. However, it's important + //that edge intersections are processed from the bottom up, but it's also + //crucial that intersections only occur between adjacent edges. + + //First we do a quicksort so intersections proceed in a bottom up order ... + std::sort(intersect_nodes_.begin(), intersect_nodes_.end(), IntersectListSort); + //Now as we process these intersections, we must sometimes adjust the order + //to ensure that intersecting edges are always adjacent ... + + std::vector::iterator node_iter, node_iter2; + for (node_iter = intersect_nodes_.begin(); + node_iter != intersect_nodes_.end(); ++node_iter) + { + if (!EdgesAdjacentInAEL(*node_iter)) + { + node_iter2 = node_iter + 1; + while (!EdgesAdjacentInAEL(*node_iter2)) ++node_iter2; + std::swap(*node_iter, *node_iter2); + } + + IntersectNode& node = *node_iter; + IntersectEdges(*node.edge1, *node.edge2, node.pt); + SwapPositionsInAEL(*node.edge1, *node.edge2); + + if (TestJoinWithPrev2(*node.edge2, node.pt)) + { + OutPt* op1 = AddOutPt(*node.edge2->prev_in_ael, node.pt); + OutPt* op2 = AddOutPt(*node.edge2, node.pt); + if (op1 != op2) AddJoin(op1, op2); + } + else if (TestJoinWithNext2(*node.edge1, node.pt)) + { + OutPt* op1 = AddOutPt(*node.edge1, node.pt); + OutPt* op2 = AddOutPt(*node.edge1->next_in_ael, node.pt); + if (op1 != op2) AddJoin(op1, op2); + } + } + } + + + void ClipperBase::SwapPositionsInAEL(Active& e1, Active& e2) + { + //preconditon: e1 must be immediately to the left of e2 + Active* next = e2.next_in_ael; + if (next) next->prev_in_ael = &e1; + Active* prev = e1.prev_in_ael; + if (prev) prev->next_in_ael = &e2; + e2.prev_in_ael = prev; + e2.next_in_ael = &e1; + e1.prev_in_ael = &e2; + e1.next_in_ael = next; + if (!e2.prev_in_ael) actives_ = &e2; + } + + + bool ClipperBase::ResetHorzDirection(const Active& horz, + const Active* max_pair, int64_t& horz_left, int64_t& horz_right) + { + if (horz.bot.x == horz.top.x) + { + //the horizontal edge is going nowhere ... + horz_left = horz.curr_x; + horz_right = horz.curr_x; + Active* e = horz.next_in_ael; + while (e && e != max_pair) e = e->next_in_ael; + return e != nullptr; + } + else if (horz.curr_x < horz.top.x) + { + horz_left = horz.curr_x; + horz_right = horz.top.x; + return true; + } + else + { + horz_left = horz.top.x; + horz_right = horz.curr_x; + return false; // right to left + } + } + + inline bool HorzIsSpike(const Active& horzEdge) + { + Point64 nextPt = NextVertex(horzEdge)->pt; + return (nextPt.y == horzEdge.bot.y) && + (horzEdge.bot.x < horzEdge.top.x) != (horzEdge.top.x < nextPt.x); + } + + inline void TrimHorz(Active& horzEdge, bool preserveCollinear) + { + bool wasTrimmed = false; + Point64 pt = NextVertex(horzEdge)->pt; + while (pt.y == horzEdge.top.y) + { + //always trim 180 deg. spikes (in closed paths) + //but otherwise break if preserveCollinear = true + if (preserveCollinear && + ((pt.x < horzEdge.top.x) != (horzEdge.bot.x < horzEdge.top.x))) + break; + + horzEdge.vertex_top = NextVertex(horzEdge); + horzEdge.top = pt; + wasTrimmed = true; + if (IsMaxima(horzEdge)) break; + pt = NextVertex(horzEdge)->pt; + } + + if (wasTrimmed) SetDx(horzEdge); // +/-infinity + } + + + void ClipperBase::DoHorizontal(Active& horz) + /******************************************************************************* + * Notes: Horizontal edges (HEs) at scanline intersections (ie at the top or * + * bottom of a scanbeam) are processed as if layered.The order in which HEs * + * are processed doesn't matter. HEs intersect with the bottom vertices of * + * other HEs[#] and with non-horizontal edges [*]. Once these intersections * + * are completed, intermediate HEs are 'promoted' to the next edge in their * + * bounds, and they in turn may be intersected[%] by other HEs. * + * * + * eg: 3 horizontals at a scanline: / | / / * + * | / | (HE3)o ========%========== o * + * o ======= o(HE2) / | / / * + * o ============#=========*======*========#=========o (HE1) * + * / | / | / * + *******************************************************************************/ + { + Point64 pt; + bool horzIsOpen = IsOpen(horz); + int64_t y = horz.bot.y; + Vertex* vertex_max = nullptr; + Active* max_pair = nullptr; + + if (!horzIsOpen) + { + vertex_max = GetCurrYMaximaVertex(horz); + if (vertex_max) + { + max_pair = GetHorzMaximaPair(horz, vertex_max); + //remove 180 deg.spikes and also simplify + //consecutive horizontals when PreserveCollinear = true + if (vertex_max != horz.vertex_top) + TrimHorz(horz, PreserveCollinear); + } + } + + int64_t horz_left, horz_right; + bool is_left_to_right = + ResetHorzDirection(horz, max_pair, horz_left, horz_right); + + if (IsHotEdge(horz)) + AddOutPt(horz, Point64(horz.curr_x, y)); + + OutPt* op; + while (true) // loop through consec. horizontal edges + { + if (horzIsOpen && IsMaxima(horz) && !IsOpenEnd(horz)) + { + vertex_max = GetCurrYMaximaVertex(horz); + if (vertex_max) + max_pair = GetHorzMaximaPair(horz, vertex_max); + } + + Active* e; + if (is_left_to_right) e = horz.next_in_ael; + else e = horz.prev_in_ael; + + while (e) + { + + if (e == max_pair) + { + if (IsHotEdge(horz)) + { + while (horz.vertex_top != e->vertex_top) + { + AddOutPt(horz, horz.top); + UpdateEdgeIntoAEL(&horz); + } + op = AddLocalMaxPoly(horz, *e, horz.top); + if (op && !IsOpen(horz) && op->pt == horz.top) + AddTrialHorzJoin(op); + } + DeleteFromAEL(*e); + DeleteFromAEL(horz); + return; + } + + //if horzEdge is a maxima, keep going until we reach + //its maxima pair, otherwise check for break conditions + if (vertex_max != horz.vertex_top || IsOpenEnd(horz)) + { + //otherwise stop when 'ae' is beyond the end of the horizontal line + if ((is_left_to_right && e->curr_x > horz_right) || + (!is_left_to_right && e->curr_x < horz_left)) break; + + if (e->curr_x == horz.top.x && !IsHorizontal(*e)) + { + pt = NextVertex(horz)->pt; + if (is_left_to_right) + { + //with open paths we'll only break once past horz's end + if (IsOpen(*e) && !IsSamePolyType(*e, horz) && !IsHotEdge(*e)) + { + if (TopX(*e, pt.y) > pt.x) break; + } + //otherwise we'll only break when horz's outslope is greater than e's + else if (TopX(*e, pt.y) >= pt.x) break; + } + else + { + if (IsOpen(*e) && !IsSamePolyType(*e, horz) && !IsHotEdge(*e)) + { + if (TopX(*e, pt.y) < pt.x) break; + } + else if (TopX(*e, pt.y) <= pt.x) break; + } + } + } + + pt = Point64(e->curr_x, horz.bot.y); + + if (is_left_to_right) + { + op = IntersectEdges(horz, *e, pt); + SwapPositionsInAEL(horz, *e); + // todo: check if op->pt == pt test is still needed + // expect op != pt only after AddLocalMaxPoly when horz.outrec == nullptr + if (IsHotEdge(horz) && op && !IsOpen(horz) && op->pt == pt) + AddTrialHorzJoin(op); + + if (!IsHorizontal(*e) && TestJoinWithPrev1(*e)) + { + op = AddOutPt(*e->prev_in_ael, pt); + OutPt* op2 = AddOutPt(*e, pt); + AddJoin(op, op2); + } + + horz.curr_x = e->curr_x; + e = horz.next_in_ael; + } + else + { + op = IntersectEdges(*e, horz, pt); + SwapPositionsInAEL(*e, horz); + + if (IsHotEdge(horz) && op && + !IsOpen(horz) && op->pt == pt) + AddTrialHorzJoin(op); + + if (!IsHorizontal(*e) && TestJoinWithNext1(*e)) + { + op = AddOutPt(*e, pt); + OutPt* op2 = AddOutPt(*e->next_in_ael, pt); + AddJoin(op, op2); + } + + horz.curr_x = e->curr_x; + e = horz.prev_in_ael; + } + } + + //check if we've finished with (consecutive) horizontals ... + if (horzIsOpen && IsOpenEnd(horz)) // ie open at top + { + if (IsHotEdge(horz)) + { + AddOutPt(horz, horz.top); + if (IsFront(horz)) + horz.outrec->front_edge = nullptr; + else + horz.outrec->back_edge = nullptr; + horz.outrec = nullptr; + } + DeleteFromAEL(horz); + return; + } + else if (NextVertex(horz)->pt.y != horz.top.y) + break; + + //still more horizontals in bound to process ... + if (IsHotEdge(horz)) + AddOutPt(horz, horz.top); + UpdateEdgeIntoAEL(&horz); + + if (PreserveCollinear && !horzIsOpen && HorzIsSpike(horz)) + TrimHorz(horz, true); + + is_left_to_right = + ResetHorzDirection(horz, max_pair, horz_left, horz_right); + } + + if (IsHotEdge(horz)) + { + op = AddOutPt(horz, horz.top); + if (!IsOpen(horz)) + AddTrialHorzJoin(op); + } + else + op = nullptr; + + if ((horzIsOpen && !IsOpenEnd(horz)) || + (!horzIsOpen && vertex_max != horz.vertex_top)) + { + UpdateEdgeIntoAEL(&horz); // this is the end of an intermediate horiz. + if (IsOpen(horz)) return; + + if (is_left_to_right && TestJoinWithNext1(horz)) + { + OutPt* op2 = AddOutPt(*horz.next_in_ael, horz.bot); + AddJoin(op, op2); + } + else if (!is_left_to_right && TestJoinWithPrev1(horz)) + { + OutPt* op2 = AddOutPt(*horz.prev_in_ael, horz.bot); + AddJoin(op2, op); + } + } + else if (IsHotEdge(horz)) + AddLocalMaxPoly(horz, *max_pair, horz.top); + else + { + DeleteFromAEL(*max_pair); + DeleteFromAEL(horz); + } + } + + + void ClipperBase::DoTopOfScanbeam(const int64_t y) + { + sel_ = nullptr; // sel_ is reused to flag horizontals (see PushHorz below) + Active* e = actives_; + while (e) + { + //nb: 'e' will never be horizontal here + if (e->top.y == y) + { + e->curr_x = e->top.x; + if (IsMaxima(*e)) + { + e = DoMaxima(*e); // TOP OF BOUND (MAXIMA) + continue; + } + else + { + //INTERMEDIATE VERTEX ... + if (IsHotEdge(*e)) AddOutPt(*e, e->top); + UpdateEdgeIntoAEL(e); + if (IsHorizontal(*e)) + PushHorz(*e); // horizontals are processed later + } + } + else // i.e. not the top of the edge + e->curr_x = TopX(*e, y); + + e = e->next_in_ael; + } + } + + + Active* ClipperBase::DoMaxima(Active& e) + { + Active* next_e, * prev_e, * max_pair; + prev_e = e.prev_in_ael; + next_e = e.next_in_ael; + if (IsOpenEnd(e)) + { + if (IsHotEdge(e)) AddOutPt(e, e.top); + if (!IsHorizontal(e)) + { + if (IsHotEdge(e)) + { + if (IsFront(e)) + e.outrec->front_edge = nullptr; + else + e.outrec->back_edge = nullptr; + e.outrec = nullptr; + } + DeleteFromAEL(e); + } + return next_e; + } + else + { + max_pair = GetMaximaPair(e); + if (!max_pair) return next_e; // eMaxPair is horizontal + } + + //only non-horizontal maxima here. + //process any edges between maxima pair ... + while (next_e != max_pair) + { + IntersectEdges(e, *next_e, e.top); + SwapPositionsInAEL(e, *next_e); + next_e = e.next_in_ael; + } + + if (IsOpen(e)) + { + if (IsHotEdge(e)) + AddLocalMaxPoly(e, *max_pair, e.top); + DeleteFromAEL(*max_pair); + DeleteFromAEL(e); + return (prev_e ? prev_e->next_in_ael : actives_); + } + + //here E.next_in_ael == ENext == EMaxPair ... + if (IsHotEdge(e)) + AddLocalMaxPoly(e, *max_pair, e.top); + + DeleteFromAEL(e); + DeleteFromAEL(*max_pair); + return (prev_e ? prev_e->next_in_ael : actives_); + } + + + void ClipperBase::SafeDeleteOutPtJoiners(OutPt* op) + { + Joiner* joiner = op->joiner; + if (!joiner) return; + + while (joiner) + { + if (joiner->idx < 0) + DeleteTrialHorzJoin(op); + else if (horz_joiners_) + { + if (OutPtInTrialHorzList(joiner->op1)) + DeleteTrialHorzJoin(joiner->op1); + if (OutPtInTrialHorzList(joiner->op2)) + DeleteTrialHorzJoin(joiner->op2); + DeleteJoin(joiner); + } + else + DeleteJoin(joiner); + joiner = op->joiner; + } + } + + + Joiner* ClipperBase::GetHorzTrialParent(const OutPt* op) + { + Joiner* joiner = op->joiner; + while (joiner) + { + if (joiner->op1 == op) + { + if (joiner->next1 && joiner->next1->idx < 0) return joiner; + else joiner = joiner->next1; + } + else + { + if (joiner->next2 && joiner->next2->idx < 0) return joiner; + else joiner = joiner->next1; + } + } + return joiner; + } + + + bool ClipperBase::OutPtInTrialHorzList(OutPt* op) + { + return op->joiner && ((op->joiner->idx < 0) || GetHorzTrialParent(op)); + } + + + void ClipperBase::AddTrialHorzJoin(OutPt* op) + { + //make sure 'op' isn't added more than once + if (!op->outrec->is_open && !OutPtInTrialHorzList(op)) + horz_joiners_ = new Joiner(op, nullptr, horz_joiners_); + } + + + Joiner* FindTrialJoinParent(Joiner*& joiner, const OutPt* op) + { + Joiner* parent = joiner; + while (parent) + { + if (op == parent->op1) + { + if (parent->next1 && parent->next1->idx < 0) + { + joiner = parent->next1; + return parent; + } + parent = parent->next1; + } + else + { + if (parent->next2 && parent->next2->idx < 0) + { + joiner = parent->next2; + return parent; + } + parent = parent->next2; + } + } + return nullptr; + } + + + void ClipperBase::DeleteTrialHorzJoin(OutPt* op) + { + if (!horz_joiners_) return; + + Joiner* joiner = op->joiner; + Joiner* parentH, * parentOp = nullptr; + while (joiner) + { + if (joiner->idx < 0) + { + //first remove joiner from FHorzTrials + if (joiner == horz_joiners_) + horz_joiners_ = joiner->nextH; + else + { + parentH = horz_joiners_; + while (parentH->nextH != joiner) + parentH = parentH->nextH; + parentH->nextH = joiner->nextH; + } + + //now remove joiner from op's joiner list + if (!parentOp) + { + //joiner must be first one in list + op->joiner = joiner->next1; + delete joiner; + joiner = op->joiner; + } + else + { + //the trial joiner isn't first + if (op == parentOp->op1) + parentOp->next1 = joiner->next1; + else + parentOp->next2 = joiner->next1; + delete joiner; + joiner = parentOp; + } + } + else + { + //not a trial join so look further along the linked list + parentOp = FindTrialJoinParent(joiner, op); + if (!parentOp) break; + } + //loop in case there's more than one trial join + } + } + + + inline bool GetHorzExtendedHorzSeg(OutPt*& op, OutPt*& op2) + { + OutRec* outrec = GetRealOutRec(op->outrec); + op2 = op; + if (outrec->front_edge) + { + while (op->prev != outrec->pts && + op->prev->pt.y == op->pt.y) op = op->prev; + while (op2 != outrec->pts && + op2->next->pt.y == op2->pt.y) op2 = op2->next; + return op2 != op; + } + else + { + while (op->prev != op2 && op->prev->pt.y == op->pt.y) + op = op->prev; + while (op2->next != op && op2->next->pt.y == op2->pt.y) + op2 = op2->next; + return op2 != op && op2->next != op; + } + } + + + inline bool HorzEdgesOverlap(int64_t x1a, int64_t x1b, int64_t x2a, int64_t x2b) + { + const int64_t minOverlap = 2; + if (x1a > x1b + minOverlap) + { + if (x2a > x2b + minOverlap) + return !((x1a <= x2b) || (x2a <= x1b)); + else + return !((x1a <= x2a) || (x2b <= x1b)); + } + else if (x1b > x1a + minOverlap) + { + if (x2a > x2b + minOverlap) + return !((x1b <= x2b) || (x2a <= x1a)); + else + return !((x1b <= x2a) || (x2b <= x1a)); + } + else + return false; + } + + + inline bool ValueBetween(int64_t val, int64_t end1, int64_t end2) + { + //NB accommodates axis aligned between where end1 == end2 + return ((val != end1) == (val != end2)) && + ((val > end1) == (val < end2)); + } + + + inline bool ValueEqualOrBetween(int64_t val, int64_t end1, int64_t end2) + { + return (val == end1) || (val == end2) || ((val > end1) == (val < end2)); + } + + + inline bool PointBetween(Point64 pt, Point64 corner1, Point64 corner2) + { + //NB points may not be collinear + return ValueBetween(pt.x, corner1.x, corner2.x) && + ValueBetween(pt.y, corner1.y, corner2.y); + } + + inline bool PointEqualOrBetween(Point64 pt, Point64 corner1, Point64 corner2) + { + //NB points may not be collinear + return ValueEqualOrBetween(pt.x, corner1.x, corner2.x) && + ValueEqualOrBetween(pt.y, corner1.y, corner2.y); + } + + + Joiner* FindJoinParent(const Joiner* joiner, OutPt* op) + { + Joiner* result = op->joiner; + for (; ; ) + { + if (op == result->op1) + { + if (result->next1 == joiner) return result; + else result = result->next1; + } + else + { + if (result->next2 == joiner) return result; + else result = result->next2; + } + } + } + + + void ClipperBase::ConvertHorzTrialsToJoins() + { + while (horz_joiners_) + { + Joiner* joiner = horz_joiners_; + horz_joiners_ = horz_joiners_->nextH; + OutPt* op1a = joiner->op1; + if (op1a->joiner == joiner) + { + op1a->joiner = joiner->next1; + } + else + { + Joiner* joinerParent = FindJoinParent(joiner, op1a); + if (joinerParent->op1 == op1a) + joinerParent->next1 = joiner->next1; + else + joinerParent->next2 = joiner->next1; + } + delete joiner; + + OutPt* op1b; + if (!GetHorzExtendedHorzSeg(op1a, op1b)) + { + CleanCollinear(op1a->outrec); + continue; + } + + bool joined = false; + joiner = horz_joiners_; + while (joiner) + { + OutPt* op2a = joiner->op1, * op2b; + if (GetHorzExtendedHorzSeg(op2a, op2b) && + HorzEdgesOverlap(op1a->pt.x, op1b->pt.x, op2a->pt.x, op2b->pt.x)) + { + //overlap found so promote to a 'real' join + joined = true; + if (op1a->pt == op2b->pt) + AddJoin(op1a, op2b); + else if (op1b->pt == op2a->pt) + AddJoin(op1b, op2a); + else if (op1a->pt == op2a->pt) + AddJoin(op1a, op2a); + else if (op1b->pt == op2b->pt) + AddJoin(op1b, op2b); + else if (ValueBetween(op1a->pt.x, op2a->pt.x, op2b->pt.x)) + AddJoin(op1a, InsertOp(op1a->pt, op2a)); + else if (ValueBetween(op1b->pt.x, op2a->pt.x, op2b->pt.x)) + AddJoin(op1b, InsertOp(op1b->pt, op2a)); + else if (ValueBetween(op2a->pt.x, op1a->pt.x, op1b->pt.x)) + AddJoin(op2a, InsertOp(op2a->pt, op1a)); + else if (ValueBetween(op2b->pt.x, op1a->pt.x, op1b->pt.x)) + AddJoin(op2b, InsertOp(op2b->pt, op1a)); + break; + } + joiner = joiner->nextH; + } + if (!joined) + CleanCollinear(op1a->outrec); + } + } + + + void ClipperBase::AddJoin(OutPt* op1, OutPt* op2) + { + if ((op1->outrec == op2->outrec) && ((op1 == op2) || + //unless op1.next or op1.prev crosses the start-end divide + //don't waste time trying to join adjacent vertices + ((op1->next == op2) && (op1 != op1->outrec->pts)) || + ((op2->next == op1) && (op2 != op1->outrec->pts)))) return; + + Joiner* j = new Joiner(op1, op2, nullptr); + j->idx = static_cast(joiner_list_.size()); + joiner_list_.push_back(j); + } + + + void ClipperBase::DeleteJoin(Joiner* joiner) + { + //This method deletes a single join, and it doesn't check for or + //delete trial horz. joins. For that, use the following method. + OutPt* op1 = joiner->op1, * op2 = joiner->op2; + + Joiner* parent_joiner; + if (op1->joiner != joiner) + { + parent_joiner = FindJoinParent(joiner, op1); + if (parent_joiner->op1 == op1) + parent_joiner->next1 = joiner->next1; + else + parent_joiner->next2 = joiner->next1; + } + else + op1->joiner = joiner->next1; + + if (op2->joiner != joiner) + { + parent_joiner = FindJoinParent(joiner, op2); + if (parent_joiner->op1 == op2) + parent_joiner->next1 = joiner->next2; + else + parent_joiner->next2 = joiner->next2; + } + else + op2->joiner = joiner->next2; + + joiner_list_[joiner->idx] = nullptr; + delete joiner; + } + + + void ClipperBase::ProcessJoinerList() + { + for (Joiner* j : joiner_list_) + { + if (!j) continue; + if (succeeded_) + { + OutRec* outrec = ProcessJoin(j); + CleanCollinear(outrec); + } + else + delete j; + } + + joiner_list_.resize(0); + } + + + bool CheckDisposeAdjacent(OutPt*& op, const OutPt* guard, OutRec& outRec) + { + bool result = false; + while (op->prev != op) + { + if (op->pt == op->prev->pt && op != guard && + op->prev->joiner && !op->joiner) + { + if (op == outRec.pts) outRec.pts = op->prev; + op = DisposeOutPt(op); + op = op->prev; + } + else + break; + } + + while (op->next != op) + { + if (op->pt == op->next->pt && op != guard && + op->next->joiner && !op->joiner) + { + if (op == outRec.pts) outRec.pts = op->prev; + op = DisposeOutPt(op); + op = op->prev; + } + else + break; + } + return result; + } + + + inline bool IsValidPath(OutPt* op) + { + return (op && op->next != op); + } + + + bool CollinearSegsOverlap(const Point64& seg1a, const Point64& seg1b, + const Point64& seg2a, const Point64& seg2b) + { + //precondition: seg1 and seg2 are collinear + if (seg1a.x == seg1b.x) + { + if (seg2a.x != seg1a.x || seg2a.x != seg2b.x) return false; + } + else if (seg1a.x < seg1b.x) + { + if (seg2a.x < seg2b.x) + { + if (seg2a.x >= seg1b.x || seg2b.x <= seg1a.x) return false; + } + else + { + if (seg2b.x >= seg1b.x || seg2a.x <= seg1a.x) return false; + } + } + else + { + if (seg2a.x < seg2b.x) + { + if (seg2a.x >= seg1a.x || seg2b.x <= seg1b.x) return false; + } + else + { + if (seg2b.x >= seg1a.x || seg2a.x <= seg1b.x) return false; + } + } + + if (seg1a.y == seg1b.y) + { + if (seg2a.y != seg1a.y || seg2a.y != seg2b.y) return false; + } + else if (seg1a.y < seg1b.y) + { + if (seg2a.y < seg2b.y) + { + if (seg2a.y >= seg1b.y || seg2b.y <= seg1a.y) return false; + } + else + { + if (seg2b.y >= seg1b.y || seg2a.y <= seg1a.y) return false; + } + } + else + { + if (seg2a.y < seg2b.y) + { + if (seg2a.y >= seg1a.y || seg2b.y <= seg1b.y) return false; + } + else + { + if (seg2b.y >= seg1a.y || seg2a.y <= seg1b.y) return false; + } + } + return true; + } + + OutRec* ClipperBase::ProcessJoin(Joiner* joiner) + { + OutPt* op1 = joiner->op1, * op2 = joiner->op2; + OutRec* or1 = GetRealOutRec(op1->outrec); + OutRec* or2 = GetRealOutRec(op2->outrec); + DeleteJoin(joiner); + + if (or2->pts == nullptr) return or1; + else if (!IsValidClosedPath(op2)) + { + SafeDisposeOutPts(op2); + return or1; + } + else if ((or1->pts == nullptr) || !IsValidClosedPath(op1)) + { + SafeDisposeOutPts(op1); + return or2; + } + else if (or1 == or2 && + ((op1 == op2) || (op1->next == op2) || (op1->prev == op2))) return or1; + + CheckDisposeAdjacent(op1, op2, *or1); + CheckDisposeAdjacent(op2, op1, *or2); + if (op1->next == op2 || op2->next == op1) return or1; + OutRec* result = or1; + + for (; ; ) + { + if (!IsValidPath(op1) || !IsValidPath(op2) || + (or1 == or2 && (op1->prev == op2 || op1->next == op2))) return or1; + + if (op1->prev->pt == op2->next->pt || + ((CrossProduct(op1->prev->pt, op1->pt, op2->next->pt) == 0) && + CollinearSegsOverlap(op1->prev->pt, op1->pt, op2->pt, op2->next->pt))) + { + if (or1 == or2) + { + //SPLIT REQUIRED + //make sure op1.prev and op2.next match positions + //by inserting an extra vertex if needed + if (op1->prev->pt != op2->next->pt) + { + if (PointEqualOrBetween(op1->prev->pt, op2->pt, op2->next->pt)) + op2->next = InsertOp(op1->prev->pt, op2); + else + op1->prev = InsertOp(op2->next->pt, op1->prev); + } + + //current to new + //op1.p[opA] >>> op1 ... opA \ / op1 + //op2.n[opB] <<< op2 ... opB / \ op2 + OutPt* opA = op1->prev, * opB = op2->next; + opA->next = opB; + opB->prev = opA; + op1->prev = op2; + op2->next = op1; + CompleteSplit(op1, opA, *or1); + } + else + { + //JOIN, NOT SPLIT + OutPt* opA = op1->prev, * opB = op2->next; + opA->next = opB; + opB->prev = opA; + op1->prev = op2; + op2->next = op1; + + //SafeDeleteOutPtJoiners(op2); + //DisposeOutPt(op2); + + if (or1->idx < or2->idx) + { + or1->pts = op1; + or2->pts = nullptr; + if (or1->owner && (!or2->owner || + or2->owner->idx < or1->owner->idx)) + or1->owner = or2->owner; + or2->owner = or1; + } + else + { + result = or2; + or2->pts = op1; + or1->pts = nullptr; + if (or2->owner && (!or1->owner || + or1->owner->idx < or2->owner->idx)) + or2->owner = or1->owner; + or1->owner = or2; + } + } + break; + } + else if (op1->next->pt == op2->prev->pt || + ((CrossProduct(op1->next->pt, op2->pt, op2->prev->pt) == 0) && + CollinearSegsOverlap(op1->next->pt, op1->pt, op2->pt, op2->prev->pt))) + { + if (or1 == or2) + { + //SPLIT REQUIRED + //make sure op2.prev and op1.next match positions + //by inserting an extra vertex if needed + if (op2->prev->pt != op1->next->pt) + { + if (PointEqualOrBetween(op2->prev->pt, op1->pt, op1->next->pt)) + op1->next = InsertOp(op2->prev->pt, op1); + else + op2->prev = InsertOp(op1->next->pt, op2->prev); + } + + //current to new + //op2.p[opA] >>> op2 ... opA \ / op2 + //op1.n[opB] <<< op1 ... opB / \ op1 + OutPt* opA = op2->prev, * opB = op1->next; + opA->next = opB; + opB->prev = opA; + op2->prev = op1; + op1->next = op2; + CompleteSplit(op1, opA, *or1); + } + else + { + //JOIN, NOT SPLIT + OutPt* opA = op1->next, * opB = op2->prev; + opA->prev = opB; + opB->next = opA; + op1->next = op2; + op2->prev = op1; + + //SafeDeleteOutPtJoiners(op2); + //DisposeOutPt(op2); + + if (or1->idx < or2->idx) + { + or1->pts = op1; + or2->pts = nullptr; + if (or1->owner && (!or2->owner || + or2->owner->idx < or1->owner->idx)) + or1->owner = or2->owner; + or2->owner = or1; + } + else + { + result = or2; + or2->pts = op1; + or1->pts = nullptr; + if (or2->owner && (!or1->owner || + or1->owner->idx < or2->owner->idx)) + or2->owner = or1->owner; + or1->owner = or2; + } + } + break; + } + else if (PointBetween(op1->next->pt, op2->pt, op2->prev->pt) && + DistanceFromLineSqrd(op1->next->pt, op2->pt, op2->prev->pt) < 2.01) + { + InsertOp(op1->next->pt, op2->prev); + continue; + } + else if (PointBetween(op2->next->pt, op1->pt, op1->prev->pt) && + DistanceFromLineSqrd(op2->next->pt, op1->pt, op1->prev->pt) < 2.01) + { + InsertOp(op2->next->pt, op1->prev); + continue; + } + else if (PointBetween(op1->prev->pt, op2->pt, op2->next->pt) && + DistanceFromLineSqrd(op1->prev->pt, op2->pt, op2->next->pt) < 2.01) + { + InsertOp(op1->prev->pt, op2); + continue; + } + else if (PointBetween(op2->prev->pt, op1->pt, op1->next->pt) && + DistanceFromLineSqrd(op2->prev->pt, op1->pt, op1->next->pt) < 2.01) + { + InsertOp(op2->prev->pt, op1); + continue; + } + + //something odd needs tidying up + if (CheckDisposeAdjacent(op1, op2, *or1)) continue; + else if (CheckDisposeAdjacent(op2, op1, *or1)) continue; + else if (op1->prev->pt != op2->next->pt && + (DistanceSqr(op1->prev->pt, op2->next->pt) < 2.01)) + { + op1->prev->pt = op2->next->pt; + continue; + } + else if (op1->next->pt != op2->prev->pt && + (DistanceSqr(op1->next->pt, op2->prev->pt) < 2.01)) + { + op2->prev->pt = op1->next->pt; + continue; + } + else + { + //OK, there doesn't seem to be a way to join after all + //so just tidy up the polygons + or1->pts = op1; + if (or2 != or1) + { + or2->pts = op2; + CleanCollinear(or2); + } + break; + } + } + return result; + + } + + + bool BuildPath(OutPt* op, bool reverse, bool isOpen, Path64& path) + { + if (op->next == op || (!isOpen && op->next == op->prev)) return false; + path.resize(0); + Point64 lastPt; + OutPt* op2; + if (reverse) + { + lastPt = op->pt; + op2 = op->prev; + } + else + { + op = op->next; + lastPt = op->pt; + op2 = op->next; + } + path.push_back(lastPt); + + while (op2 != op) + { + if (op2->pt != lastPt) + { + lastPt = op2->pt; + path.push_back(lastPt); + } + if (reverse) + op2 = op2->prev; + else + op2 = op2->next; + } + return true; + } + + + void ClipperBase::BuildPaths(Paths64& solutionClosed, Paths64* solutionOpen) + { + solutionClosed.resize(0); + solutionClosed.reserve(outrec_list_.size()); + if (solutionOpen) + { + solutionOpen->resize(0); + solutionOpen->reserve(outrec_list_.size()); + } + + for (OutRec* outrec : outrec_list_) + { + if (outrec->pts == nullptr) continue; + + Path64 path; + if (solutionOpen && outrec->is_open) + { + if (BuildPath(outrec->pts, ReverseSolution, true, path)) + solutionOpen->emplace_back(std::move(path)); + } + else + { + //closed paths should always return a Positive orientation + if (BuildPath(outrec->pts, ReverseSolution, false, path)) + solutionClosed.emplace_back(std::move(path)); + } + } + } + + + inline bool Path1InsidePath2(const OutRec* or1, const OutRec* or2) + { + PointInPolygonResult result = PointInPolygonResult::IsOn; + OutPt* op = or1->pts; + do + { + result = PointInPolygon(op->pt, or2->path); + if (result != PointInPolygonResult::IsOn) break; + op = op->next; + } while (op != or1->pts); + if (result == PointInPolygonResult::IsOn) + return Area(op) < Area(or2->pts); + else + return result == PointInPolygonResult::IsInside; + } + + inline Rect64 GetBounds(const Path64& path) + { + if (path.empty()) return Rect64(); + Rect64 result = invalid_rect; + for(const Point64& pt : path) + { + if (pt.x < result.left) result.left = pt.x; + if (pt.x > result.right) result.right = pt.x; + if (pt.y < result.top) result.top = pt.y; + if (pt.y > result.bottom) result.bottom = pt.y; + } + return result; + } + + + bool ClipperBase::DeepCheckOwner(OutRec* outrec, OutRec* owner) + { + if (owner->bounds.IsEmpty()) owner->bounds = GetBounds(owner->path); + bool is_inside_owner_bounds = owner->bounds.Contains(outrec->bounds); + + // while looking for the correct owner, check the owner's + // splits **before** checking the owner itself because + // splits can occur internally, and checking the owner + // first would miss the inner split's true ownership + if (owner->splits) + { + for (OutRec* split : *owner->splits) + { + split = GetRealOutRec(split); + if (!split || split->idx <= owner->idx || split == outrec) continue; + + if (split->splits && DeepCheckOwner(outrec, split)) return true; + + if (!split->path.size()) + BuildPath(split->pts, ReverseSolution, false, split->path); + if (split->bounds.IsEmpty()) split->bounds = GetBounds(split->path); + + if (split->bounds.Contains(outrec->bounds) && + Path1InsidePath2(outrec, split)) + { + outrec->owner = split; + return true; + } + } + } + + // only continue past here when not inside recursion + if (owner != outrec->owner) return false; + + for (;;) + { + if (is_inside_owner_bounds && Path1InsidePath2(outrec, outrec->owner)) + return true; + // otherwise keep trying with owner's owner + outrec->owner = outrec->owner->owner; + if (!outrec->owner) return true; // true or false + is_inside_owner_bounds = outrec->owner->bounds.Contains(outrec->bounds); + } + } + + + void ClipperBase::BuildTree(PolyPath64& polytree, Paths64& open_paths) + { + polytree.Clear(); + open_paths.resize(0); + if (has_open_paths_) + open_paths.reserve(outrec_list_.size()); + + for (OutRec* outrec : outrec_list_) + { + if (!outrec || !outrec->pts) continue; + if (outrec->is_open) + { + Path64 path; + if (BuildPath(outrec->pts, ReverseSolution, true, path)) + open_paths.push_back(path); + continue; + } + + if (!BuildPath(outrec->pts, ReverseSolution, false, outrec->path)) + continue; + if (outrec->bounds.IsEmpty()) outrec->bounds = GetBounds(outrec->path); + outrec->owner = GetRealOutRec(outrec->owner); + if (outrec->owner) DeepCheckOwner(outrec, outrec->owner); + + // swap the order when a child preceeds its owner + // (because owners must preceed children in polytrees) + if (outrec->owner && outrec->idx < outrec->owner->idx) + { + OutRec* tmp = outrec->owner; + outrec_list_[outrec->owner->idx] = outrec; + outrec_list_[outrec->idx] = tmp; + size_t tmp_idx = outrec->idx; + outrec->idx = tmp->idx; + tmp->idx = tmp_idx; + outrec = tmp; + outrec->owner = GetRealOutRec(outrec->owner); + BuildPath(outrec->pts, ReverseSolution, false, outrec->path); + if (outrec->bounds.IsEmpty()) outrec->bounds = GetBounds(outrec->path); + if (outrec->owner) DeepCheckOwner(outrec, outrec->owner); + } + + PolyPath64* owner_polypath; + if (outrec->owner && outrec->owner->polypath) + owner_polypath = outrec->owner->polypath; + else + owner_polypath = &polytree; + outrec->polypath = owner_polypath->AddChild(outrec->path); + } + } + + static void PolyPath64ToPolyPathD(const PolyPath64& polypath, PolyPathD& result) + { + for (auto child : polypath) + { + PolyPathD* res_child = result.AddChild( + Path64ToPathD(child->Polygon())); + PolyPath64ToPolyPathD(*child, *res_child); + } + } + + inline void Polytree64ToPolytreeD(const PolyPath64& polytree, PolyPathD& result) + { + result.Clear(); + PolyPath64ToPolyPathD(polytree, result); + } + +} // namespace clipper2lib diff --git a/thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp b/thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp new file mode 100644 index 0000000000..a19a6ff459 --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp @@ -0,0 +1,485 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : Path Offset (Inflate/Shrink) * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#include +#include "clipper2/clipper.h" +#include "clipper2/clipper.offset.h" + +namespace Clipper2Lib { + +const double default_arc_tolerance = 0.25; +const double floating_point_tolerance = 1e-12; + +//------------------------------------------------------------------------------ +// Miscellaneous methods +//------------------------------------------------------------------------------ + +Paths64::size_type GetLowestPolygonIdx(const Paths64& paths) +{ + Paths64::size_type result = 0; + Point64 lp = Point64(static_cast(0), + std::numeric_limits::min()); + + for (Paths64::size_type i = 0 ; i < paths.size(); ++i) + for (const Point64& p : paths[i]) + { + if (p.y < lp.y || (p.y == lp.y && p.x >= lp.x)) continue; + result = i; + lp = p; + } + return result; +} + +PointD GetUnitNormal(const Point64& pt1, const Point64& pt2) +{ + double dx, dy, inverse_hypot; + if (pt1 == pt2) return PointD(0.0, 0.0); + dx = static_cast(pt2.x - pt1.x); + dy = static_cast(pt2.y - pt1.y); + inverse_hypot = 1.0 / hypot(dx, dy); + dx *= inverse_hypot; + dy *= inverse_hypot; + return PointD(dy, -dx); +} + +inline bool AlmostZero(double value, double epsilon = 0.001) +{ + return std::fabs(value) < epsilon; +} + +inline double Hypot(double x, double y) +{ + //see https://stackoverflow.com/a/32436148/359538 + return std::sqrt(x * x + y * y); +} + +inline PointD NormalizeVector(const PointD& vec) +{ + + double h = Hypot(vec.x, vec.y); + if (AlmostZero(h)) return PointD(0,0); + double inverseHypot = 1 / h; + return PointD(vec.x * inverseHypot, vec.y * inverseHypot); +} + +inline PointD GetAvgUnitVector(const PointD& vec1, const PointD& vec2) +{ + return NormalizeVector(PointD(vec1.x + vec2.x, vec1.y + vec2.y)); +} + +inline bool IsClosedPath(EndType et) +{ + return et == EndType::Polygon || et == EndType::Joined; +} + +inline Point64 GetPerpendic(const Point64& pt, const PointD& norm, double delta) +{ + return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta); +} + +inline PointD GetPerpendicD(const Point64& pt, const PointD& norm, double delta) +{ + return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta); +} + +//------------------------------------------------------------------------------ +// ClipperOffset methods +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPath(const Path64& path, JoinType jt_, EndType et_) +{ + Paths64 paths; + paths.push_back(path); + AddPaths(paths, jt_, et_); +} + +void ClipperOffset::AddPaths(const Paths64 &paths, JoinType jt_, EndType et_) +{ + if (paths.size() == 0) return; + groups_.push_back(Group(paths, jt_, et_)); +} + +void ClipperOffset::AddPath(const Clipper2Lib::PathD& path, JoinType jt_, EndType et_) +{ + PathsD paths; + paths.push_back(path); + AddPaths(paths, jt_, et_); +} + +void ClipperOffset::AddPaths(const PathsD& paths, JoinType jt_, EndType et_) +{ + if (paths.size() == 0) return; + groups_.push_back(Group(PathsDToPaths64(paths), jt_, et_)); +} + +void ClipperOffset::BuildNormals(const Path64& path) +{ + norms.clear(); + norms.reserve(path.size()); + if (path.size() == 0) return; + Path64::const_iterator path_iter, path_last_iter = --path.cend(); + for (path_iter = path.cbegin(); path_iter != path_last_iter; ++path_iter) + norms.push_back(GetUnitNormal(*path_iter,*(path_iter +1))); + norms.push_back(GetUnitNormal(*path_last_iter, *(path.cbegin()))); +} + +inline PointD TranslatePoint(const PointD& pt, double dx, double dy) +{ + return PointD(pt.x + dx, pt.y + dy); +} + +inline PointD ReflectPoint(const PointD& pt, const PointD& pivot) +{ + return PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); +} + +PointD IntersectPoint(const PointD& pt1a, const PointD& pt1b, + const PointD& pt2a, const PointD& pt2b) +{ + if (pt1a.x == pt1b.x) //vertical + { + if (pt2a.x == pt2b.x) return PointD(0, 0); + + double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); + double b2 = pt2a.y - m2 * pt2a.x; + return PointD(pt1a.x, m2 * pt1a.x + b2); + } + else if (pt2a.x == pt2b.x) //vertical + { + double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); + double b1 = pt1a.y - m1 * pt1a.x; + return PointD(pt2a.x, m1 * pt2a.x + b1); + } + else + { + double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); + double b1 = pt1a.y - m1 * pt1a.x; + double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); + double b2 = pt2a.y - m2 * pt2a.x; + if (m1 == m2) return PointD(0, 0); + double x = (b2 - b1) / (m1 - m2); + return PointD(x, m1 * x + b1); + } +} + +void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t k) +{ + PointD vec; + if (j == k) + vec = PointD(norms[0].y, -norms[0].x); + else + vec = GetAvgUnitVector( + PointD(-norms[k].y, norms[k].x), + PointD(norms[j].y, -norms[j].x)); + + // now offset the original vertex delta units along unit vector + PointD ptQ = PointD(path[j]); + ptQ = TranslatePoint(ptQ, abs_group_delta_ * vec.x, abs_group_delta_ * vec.y); + // get perpendicular vertices + PointD pt1 = TranslatePoint(ptQ, group_delta_ * vec.y, group_delta_ * -vec.x); + PointD pt2 = TranslatePoint(ptQ, group_delta_ * -vec.y, group_delta_ * vec.x); + // get 2 vertices along one edge offset + PointD pt3 = GetPerpendicD(path[k], norms[k], group_delta_); + if (j == k) + { + PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_); + PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); + //get the second intersect point through reflecion + group.path_.push_back(Point64(ReflectPoint(pt, ptQ))); + group.path_.push_back(Point64(pt)); + } + else + { + PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_); + PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); + group.path_.push_back(Point64(pt)); + //get the second intersect point through reflecion + group.path_.push_back(Point64(ReflectPoint(pt, ptQ))); + } +} + +void ClipperOffset::DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a) +{ + double q = group_delta_ / (cos_a + 1); + group.path_.push_back(Point64( + path[j].x + (norms[k].x + norms[j].x) * q, + path[j].y + (norms[k].y + norms[j].y) * q)); +} + +void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle) +{ + //even though angle may be negative this is a convex join + Point64 pt = path[j]; + int steps = static_cast(std::ceil(steps_per_rad_ * std::abs(angle))); + double step_sin = std::sin(angle / steps); + double step_cos = std::cos(angle / steps); + + PointD pt2 = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_); + if (j == k) pt2.Negate(); + + group.path_.push_back(Point64(pt.x + pt2.x, pt.y + pt2.y)); + for (int i = 0; i < steps; i++) + { + pt2 = PointD(pt2.x * step_cos - step_sin * pt2.y, + pt2.x * step_sin + pt2.y * step_cos); + group.path_.push_back(Point64(pt.x + pt2.x, pt.y + pt2.y)); + } + group.path_.push_back(GetPerpendic(path[j], norms[j], group_delta_)); +} + +void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) +{ + // Let A = change in angle where edges join + // A == 0: ie no change in angle (flat join) + // A == PI: edges 'spike' + // sin(A) < 0: right turning + // cos(A) < 0: change in angle is more than 90 degree + + if (path[j] == path[k]) { k = j; return; } + + double sin_a = CrossProduct(norms[j], norms[k]); + double cos_a = DotProduct(norms[j], norms[k]); + if (sin_a > 1.0) sin_a = 1.0; + else if (sin_a < -1.0) sin_a = -1.0; + + bool almostNoAngle = AlmostZero(sin_a) && cos_a > 0; + // when there's almost no angle of deviation or it's concave + if (almostNoAngle || (sin_a * group_delta_ < 0)) + { + Point64 p1 = Point64( + path[j].x + norms[k].x * group_delta_, + path[j].y + norms[k].y * group_delta_); + Point64 p2 = Point64( + path[j].x + norms[j].x * group_delta_, + path[j].y + norms[j].y * group_delta_); + group.path_.push_back(p1); + if (p1 != p2) + { + // when concave add an extra vertex to ensure neat clipping + if (!almostNoAngle) group.path_.push_back(path[j]); + group.path_.push_back(p2); + } + } + else // it's convex + { + if (join_type_ == JoinType::Round) + DoRound(group, path, j, k, std::atan2(sin_a, cos_a)); + else if (join_type_ == JoinType::Miter) + { + // miter unless the angle is so acute the miter would exceeds ML + if (cos_a > temp_lim_ - 1) DoMiter(group, path, j, k, cos_a); + else DoSquare(group, path, j, k); + } + // don't bother squaring angles that deviate < ~20 degrees because + // squaring will be indistinguishable from mitering and just be a lot slower + else if (cos_a > 0.9) + DoMiter(group, path, j, k, cos_a); + else + DoSquare(group, path, j, k); + } + k = j; +} + +void ClipperOffset::OffsetPolygon(Group& group, Path64& path) +{ + group.path_.clear(); + for (Path64::size_type i = 0, j = path.size() -1; i < path.size(); j = i, ++i) + OffsetPoint(group, path, i, j); + group.paths_out_.push_back(group.path_); +} + +void ClipperOffset::OffsetOpenJoined(Group& group, Path64& path) +{ + OffsetPolygon(group, path); + std::reverse(path.begin(), path.end()); + BuildNormals(path); + OffsetPolygon(group, path); +} + +void ClipperOffset::OffsetOpenPath(Group& group, Path64& path, EndType end_type) +{ + group.path_.clear(); + + // do the line start cap + switch (end_type) + { + case EndType::Butt: + group.path_.push_back(Point64( + path[0].x - norms[0].x * group_delta_, + path[0].y - norms[0].y * group_delta_)); + group.path_.push_back(GetPerpendic(path[0], norms[0], group_delta_)); + break; + case EndType::Round: + DoRound(group, path, 0,0, PI); + break; + default: + DoSquare(group, path, 0, 0); + break; + } + + size_t highI = path.size() - 1; + + // offset the left side going forward + for (Path64::size_type i = 1, k = 0; i < highI; ++i) + OffsetPoint(group, path, i, k); + + // reverse normals + for (size_t i = highI; i > 0; --i) + norms[i] = PointD(-norms[i - 1].x, -norms[i - 1].y); + norms[0] = norms[highI]; + + // do the line end cap + switch (end_type) + { + case EndType::Butt: + group.path_.push_back(Point64( + path[highI].x - norms[highI].x * group_delta_, + path[highI].y - norms[highI].y * group_delta_)); + group.path_.push_back(GetPerpendic(path[highI], norms[highI], group_delta_)); + break; + case EndType::Round: + DoRound(group, path, highI, highI, PI); + break; + default: + DoSquare(group, path, highI, highI); + break; + } + + for (size_t i = highI, k = 0; i > 0; --i) + OffsetPoint(group, path, i, k); + group.paths_out_.push_back(group.path_); +} + +void ClipperOffset::DoGroupOffset(Group& group, double delta) +{ + if (group.end_type_ != EndType::Polygon) delta = std::abs(delta) * 0.5; + bool isClosedPaths = IsClosedPath(group.end_type_); + + if (isClosedPaths) + { + //the lowermost polygon must be an outer polygon. So we can use that as the + //designated orientation for outer polygons (needed for tidy-up clipping) + Paths64::size_type lowestIdx = GetLowestPolygonIdx(group.paths_in_); + // nb: don't use the default orientation here ... + double area = Area(group.paths_in_[lowestIdx]); + if (area == 0) return; + group.is_reversed_ = (area < 0); + if (group.is_reversed_) delta = -delta; + } + else + group.is_reversed_ = false; + + group_delta_ = delta; + abs_group_delta_ = std::abs(group_delta_); + join_type_ = group.join_type_; + + double arcTol = (arc_tolerance_ > floating_point_tolerance ? arc_tolerance_ + : std::log10(2 + abs_group_delta_) * default_arc_tolerance); // empirically derived + +//calculate a sensible number of steps (for 360 deg for the given offset + if (group.join_type_ == JoinType::Round || group.end_type_ == EndType::Round) + { + steps_per_rad_ = PI / std::acos(1 - arcTol / abs_group_delta_) / (PI *2); + } + + bool is_closed_path = IsClosedPath(group.end_type_); + Paths64::const_iterator path_iter; + for(path_iter = group.paths_in_.cbegin(); path_iter != group.paths_in_.cend(); ++path_iter) + { + Path64 path = StripDuplicates(*path_iter, is_closed_path); + Path64::size_type cnt = path.size(); + if (cnt == 0) continue; + + if (cnt == 1) // single point - only valid with open paths + { + group.path_ = Path64(); + //single vertex so build a circle or square ... + if (group.join_type_ == JoinType::Round) + { + double radius = abs_group_delta_; + group.path_ = Ellipse(path[0], radius, radius); + } + else + { + int d = (int)std::ceil(abs_group_delta_); + Rect64 r = Rect64(path[0].x - d, path[0].y - d, path[0].x + d, path[0].y + d); + group.path_ = r.AsPath(); + } + group.paths_out_.push_back(group.path_); + } + else + { + BuildNormals(path); + if (group.end_type_ == EndType::Polygon) OffsetPolygon(group, path); + else if (group.end_type_ == EndType::Joined) OffsetOpenJoined(group, path); + else OffsetOpenPath(group, path, group.end_type_); + } + } + + if (!merge_groups_) + { + //clean up self-intersections ... + Clipper64 c; + c.PreserveCollinear = false; + //the solution should retain the orientation of the input + c.ReverseSolution = reverse_solution_ != group.is_reversed_; + c.AddSubject(group.paths_out_); + if (group.is_reversed_) + c.Execute(ClipType::Union, FillRule::Negative, group.paths_out_); + else + c.Execute(ClipType::Union, FillRule::Positive, group.paths_out_); + } + + solution.reserve(solution.size() + group.paths_out_.size()); + copy(group.paths_out_.begin(), group.paths_out_.end(), back_inserter(solution)); + group.paths_out_.clear(); +} + +Paths64 ClipperOffset::Execute(double delta) +{ + solution.clear(); + if (std::abs(delta) < default_arc_tolerance) + { + for (const Group& group : groups_) + { + solution.reserve(solution.size() + group.paths_in_.size()); + copy(group.paths_in_.begin(), group.paths_in_.end(), back_inserter(solution)); + } + return solution; + } + + temp_lim_ = (miter_limit_ <= 1) ? + 2.0 : + 2.0 / (miter_limit_ * miter_limit_); + + std::vector::iterator groups_iter; + for (groups_iter = groups_.begin(); + groups_iter != groups_.end(); ++groups_iter) + { + DoGroupOffset(*groups_iter, delta); + } + + if (merge_groups_ && groups_.size() > 0) + { + //clean up self-intersections ... + Clipper64 c; + c.PreserveCollinear = false; + //the solution should retain the orientation of the input + c.ReverseSolution = reverse_solution_ != groups_[0].is_reversed_; + + c.AddSubject(solution); + if (groups_[0].is_reversed_) + c.Execute(ClipType::Union, FillRule::Negative, solution); + else + c.Execute(ClipType::Union, FillRule::Positive, solution); + } + return solution; +} + +} // namespace diff --git a/thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp b/thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp new file mode 100644 index 0000000000..e58dbecce6 --- /dev/null +++ b/thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp @@ -0,0 +1,480 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 15 October 2022 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2022 * +* Purpose : FAST rectangular clipping * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#include +#include "clipper2/clipper.h" +#include "clipper2/clipper.rectclip.h" + +namespace Clipper2Lib { + + //------------------------------------------------------------------------------ + // Miscellaneous methods + //------------------------------------------------------------------------------ + + inline PointInPolygonResult Path1ContainsPath2(Path64 path1, Path64 path2) + { + PointInPolygonResult result = PointInPolygonResult::IsOn; + for(const Point64& pt : path2) + { + result = PointInPolygon(pt, path1); + if (result != PointInPolygonResult::IsOn) break; + } + return result; + } + + inline bool GetLocation(const Rect64& rec, + const Point64& pt, Location& loc) + { + if (pt.x == rec.left && pt.y >= rec.top && pt.y <= rec.bottom) + { + loc = Location::Left; + return false; + } + else if (pt.x == rec.right && pt.y >= rec.top && pt.y <= rec.bottom) + { + loc = Location::Right; + return false; + } + else if (pt.y == rec.top && pt.x >= rec.left && pt.x <= rec.right) + { + loc = Location::Top; + return false; + } + else if (pt.y == rec.bottom && pt.x >= rec.left && pt.x <= rec.right) + { + loc = Location::Bottom; + return false; + } + else if (pt.x < rec.left) loc = Location::Left; + else if (pt.x > rec.right) loc = Location::Right; + else if (pt.y < rec.top) loc = Location::Top; + else if (pt.y > rec.bottom) loc = Location::Bottom; + else loc = Location::Inside; + return true; + } + + Point64 GetIntersectPoint64(const Point64& ln1a, const Point64& ln1b, + const Point64& ln2a, const Point64& ln2b) + { + // see http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/ + if (ln1b.x == ln1a.x) + { + if (ln2b.x == ln2a.x) return Point64(); // parallel lines + double m2 = static_cast(ln2b.y - ln2a.y) / (ln2b.x - ln2a.x); + double b2 = ln2a.y - m2 * ln2a.x; + return Point64(ln1a.x, static_cast(std::round(m2 * ln1a.x + b2))); + } + else if (ln2b.x == ln2a.x) + { + double m1 = static_cast(ln1b.y - ln1a.y) / (ln1b.x - ln1a.x); + double b1 = ln1a.y - m1 * ln1a.x; + return Point64(ln2a.x, static_cast(std::round(m1 * ln2a.x + b1))); + } + else + { + double m1 = static_cast(ln1b.y - ln1a.y) / (ln1b.x - ln1a.x); + double b1 = ln1a.y - m1 * ln1a.x; + double m2 = static_cast(ln2b.y - ln2a.y) / (ln2b.x - ln2a.x); + double b2 = ln2a.y - m2 * ln2a.x; + if (std::fabs(m1 - m2) > 1.0E-15) + { + double x = (b2 - b1) / (m1 - m2); + return Point64(x, m1 * x + b1); + } + else + return Point64((ln1a.x + ln1b.x) * 0.5, (ln1a.y + ln1b.y) * 0.5); + } + } + + inline bool GetIntersection(const Path64& rectPath, + const Point64& p, const Point64& p2, Location& loc, Point64& ip) + { + // gets the intersection closest to 'p' + // when Result = false, loc will remain unchanged + switch (loc) + { + case Location::Left: + if (SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[3]); + else if (p.y < rectPath[0].y && + SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[1]); + loc = Location::Top; + } + else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[2], rectPath[3]); + loc = Location::Bottom; + } + else return false; + break; + + case Location::Top: + if (SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[1]); + else if (p.x < rectPath[0].x && + SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[3]); + loc = Location::Left; + } + else if (p.x > rectPath[1].x && + SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[1], rectPath[2]); + loc = Location::Right; + } + else return false; + break; + + case Location::Right: + if (SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + ip = GetIntersectPoint64(p, p2, rectPath[1], rectPath[2]); + else if (p.y < rectPath[0].y && + SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[1]); + loc = Location::Top; + } + else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[2], rectPath[3]); + loc = Location::Bottom; + } + else return false; + break; + + case Location::Bottom: + if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + ip = GetIntersectPoint64(p, p2, rectPath[2], rectPath[3]); + else if (p.x < rectPath[3].x && + SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[3]); + loc = Location::Left; + } + else if (p.x > rectPath[2].x && + SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[1], rectPath[2]); + loc = Location::Right; + } + else return false; + break; + + default: // loc == rInside + if (SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[3]); + loc = Location::Left; + } + else if (SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[0], rectPath[1]); + loc = Location::Top; + } + else if (SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[1], rectPath[2]); + loc = Location::Right; + } + else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + { + ip = GetIntersectPoint64(p, p2, rectPath[2], rectPath[3]); + loc = Location::Bottom; + } + else return false; + break; + } + + return true; + } + + inline Location GetAdjacentLocation(Location loc, bool isClockwise) + { + int delta = (isClockwise) ? 1 : 3; + return static_cast((static_cast(loc) + delta) % 4); + } + + inline bool HeadingClockwise(Location prev, Location curr) + { + return (static_cast(prev) + 1) % 4 == static_cast(curr); + } + + inline bool AreOpposites(Location prev, Location curr) + { + return abs(static_cast(prev) - static_cast(curr)) == 2; + } + + inline bool IsClockwise(Location prev, Location curr, + Point64 prev_pt, Point64 curr_pt, Point64 rect_mp) + { + if (AreOpposites(prev, curr)) + return CrossProduct(prev_pt, rect_mp, curr_pt) < 0; + else + return HeadingClockwise(prev, curr); + } + + //---------------------------------------------------------------------------- + // RectClip64 + //---------------------------------------------------------------------------- + + inline void RectClip64::Reset() + { + result_.clear(); + start_locs_.clear(); + } + + void RectClip64::AddCorner(Location prev, Location curr) + { + if (HeadingClockwise(prev, curr)) + result_.push_back(rectPath_[static_cast(prev)]); + else + result_.push_back(rectPath_[static_cast(curr)]); + } + + void RectClip64::AddCorner(Location& loc, bool isClockwise) + { + if (isClockwise) + { + result_.push_back(rectPath_[static_cast(loc)]); + loc = GetAdjacentLocation(loc, true); + } + else + { + loc = GetAdjacentLocation(loc, false); + result_.push_back(rectPath_[static_cast(loc)]); + } + } + + void RectClip64::GetNextLocation(const Path64& path, + Location& loc, int& i, int highI) + { + switch (loc) + { + case Location::Left: + while (i <= highI && path[i].x <= rect_.left) ++i; + if (i > highI) break; + else if (path[i].x >= rect_.right) loc = Location::Right; + else if (path[i].y <= rect_.top) loc = Location::Top; + else if (path[i].y >= rect_.bottom) loc = Location::Bottom; + else loc = Location::Inside; + break; + + case Location::Top: + while (i <= highI && path[i].y <= rect_.top) ++i; + if (i > highI) break; + else if (path[i].y >= rect_.bottom) loc = Location::Bottom; + else if (path[i].x <= rect_.left) loc = Location::Left; + else if (path[i].x >= rect_.right) loc = Location::Right; + else loc = Location::Inside; + break; + + case Location::Right: + while (i <= highI && path[i].x >= rect_.right) ++i; + if (i > highI) break; + else if (path[i].x <= rect_.left) loc = Location::Left; + else if (path[i].y <= rect_.top) loc = Location::Top; + else if (path[i].y >= rect_.bottom) loc = Location::Bottom; + else loc = Location::Inside; + break; + + case Location::Bottom: + while (i <= highI && path[i].y >= rect_.bottom) ++i; + if (i > highI) break; + else if (path[i].y <= rect_.top) loc = Location::Top; + else if (path[i].x <= rect_.left) loc = Location::Left; + else if (path[i].x >= rect_.right) loc = Location::Right; + else loc = Location::Inside; + break; + + case Location::Inside: + while (i <= highI) + { + if (path[i].x < rect_.left) loc = Location::Left; + else if (path[i].x > rect_.right) loc = Location::Right; + else if (path[i].y > rect_.bottom) loc = Location::Bottom; + else if (path[i].y < rect_.top) loc = Location::Top; + else { result_.push_back(path[i]); ++i; continue; } + break; //inner loop + } + break; + } //switch + } + + Path64 RectClip64::Execute(const Path64& path) + { + if (rect_.IsEmpty() || path.size() < 3) return Path64(); + + Reset(); + int i = 0, highI = static_cast(path.size()) - 1; + Location prev = Location::Inside, loc; + Location crossing_loc = Location::Inside; + Location first_cross_ = Location::Inside; + if (!GetLocation(rect_, path[highI], loc)) + { + i = highI - 1; + while (i >= 0 && !GetLocation(rect_, path[i], prev)) --i; + if (i < 0) return path; + if (prev == Location::Inside) loc = Location::Inside; + i = 0; + } + Location starting_loc = loc; + + /////////////////////////////////////////////////// + while (i <= highI) + { + prev = loc; + Location crossing_prev = crossing_loc; + + GetNextLocation(path, loc, i, highI); + + if (i > highI) break; + Point64 ip, ip2; + Point64 prev_pt = (i) ? path[static_cast(i - 1)] : path[highI]; + + crossing_loc = loc; + if (!GetIntersection(rectPath_, path[i], prev_pt, crossing_loc, ip)) + { + // ie remaining outside + + if (crossing_prev == Location::Inside) + { + bool isClockw = IsClockwise(prev, loc, prev_pt, path[i], mp_); + do { + start_locs_.push_back(prev); + prev = GetAdjacentLocation(prev, isClockw); + } while (prev != loc); + crossing_loc = crossing_prev; // still not crossed + } + else if (prev != Location::Inside && prev != loc) + { + bool isClockw = IsClockwise(prev, loc, prev_pt, path[i], mp_); + do { + AddCorner(prev, isClockw); + } while (prev != loc); + } + ++i; + continue; + } + + //////////////////////////////////////////////////// + // we must be crossing the rect boundary to get here + //////////////////////////////////////////////////// + + if (loc == Location::Inside) // path must be entering rect + { + if (first_cross_ == Location::Inside) + { + first_cross_ = crossing_loc; + start_locs_.push_back(prev); + } + else if (prev != crossing_loc) + { + bool isClockw = IsClockwise(prev, crossing_loc, prev_pt, path[i], mp_); + do { + AddCorner(prev, isClockw); + } while (prev != crossing_loc); + } + } + else if (prev != Location::Inside) + { + // passing right through rect. 'ip' here will be the second + // intersect pt but we'll also need the first intersect pt (ip2) + loc = prev; + GetIntersection(rectPath_, prev_pt, path[i], loc, ip2); + if (crossing_prev != Location::Inside) + AddCorner(crossing_prev, loc); + + if (first_cross_ == Location::Inside) + { + first_cross_ = loc; + start_locs_.push_back(prev); + } + + loc = crossing_loc; + result_.push_back(ip2); + if (ip == ip2) + { + // it's very likely that path[i] is on rect + GetLocation(rect_, path[i], loc); + AddCorner(crossing_loc, loc); + crossing_loc = loc; + continue; + } + } + else // path must be exiting rect + { + loc = crossing_loc; + if (first_cross_ == Location::Inside) + first_cross_ = crossing_loc; + } + + result_.push_back(ip); + + } //while i <= highI + /////////////////////////////////////////////////// + + if (first_cross_ == Location::Inside) + { + if (starting_loc == Location::Inside) return path; + Rect64 tmp_rect = Bounds(path); + if (tmp_rect.Contains(rect_) && + Path1ContainsPath2(path, rectPath_) != + PointInPolygonResult::IsOutside) return rectPath_; + else + return Path64(); + } + + if (loc != Location::Inside && + (loc != first_cross_ || start_locs_.size() > 2)) + { + if (start_locs_.size() > 0) + { + prev = loc; + for (auto loc2 : start_locs_) + { + if (prev == loc2) continue; + AddCorner(prev, HeadingClockwise(prev, loc2)); + prev = loc2; + } + loc = prev; + } + if (loc != first_cross_) + AddCorner(loc, HeadingClockwise(loc, first_cross_)); + } + + if (result_.size() < 3) return Path64(); + + // tidy up duplicates and collinear segments + Path64 res; + res.reserve(result_.size()); + size_t k = 0; highI = static_cast(result_.size()) - 1; + Point64 prev_pt = result_[highI]; + res.push_back(result_[0]); + Path64::const_iterator cit; + for (cit = result_.cbegin() + 1; cit != result_.cend(); ++cit) + { + if (CrossProduct(prev_pt, res[k], *cit)) + { + prev_pt = res[k++]; + res.push_back(*cit); + } + else + res[k] = *cit; + } + + if (k < 2) return Path64(); + // and a final check for collinearity + else if (!CrossProduct(res[0], res[k - 1], res[k])) res.pop_back(); + return res; + } + +} // namespace diff --git a/thirdparty/clipper2/LICENSE b/thirdparty/clipper2/LICENSE new file mode 100644 index 0000000000..36b7cd93cd --- /dev/null +++ b/thirdparty/clipper2/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE.