From 8d856ff08be2497eae2855ad368412329eb1748e Mon Sep 17 00:00:00 2001 From: WenmuZhou Date: Fri, 29 Jan 2021 16:32:30 +0800 Subject: [PATCH] add lite --- deploy/lite/Makefile | 80 + deploy/lite/clipper.cpp | 4394 ++++++++++++++++++++++++++++++++ deploy/lite/clipper.hpp | 423 +++ deploy/lite/cls_process.cc | 43 + deploy/lite/cls_process.h | 29 + deploy/lite/config.txt | 5 + deploy/lite/crnn_process.cc | 115 + deploy/lite/crnn_process.h | 38 + deploy/lite/db_post_process.cc | 301 +++ deploy/lite/db_post_process.h | 62 + deploy/lite/ocr_db_crnn.cc | 409 +++ deploy/lite/prepare.sh | 9 + deploy/lite/readme.md | 269 ++ deploy/lite/readme_en.md | 246 ++ doc/imgs_results/lite_demo.png | Bin 0 -> 96358 bytes 15 files changed, 6423 insertions(+) create mode 100644 deploy/lite/Makefile create mode 100644 deploy/lite/clipper.cpp create mode 100644 deploy/lite/clipper.hpp create mode 100644 deploy/lite/cls_process.cc create mode 100644 deploy/lite/cls_process.h create mode 100644 deploy/lite/config.txt create mode 100644 deploy/lite/crnn_process.cc create mode 100644 deploy/lite/crnn_process.h create mode 100644 deploy/lite/db_post_process.cc create mode 100644 deploy/lite/db_post_process.h create mode 100644 deploy/lite/ocr_db_crnn.cc create mode 100644 deploy/lite/prepare.sh create mode 100644 deploy/lite/readme.md create mode 100644 deploy/lite/readme_en.md create mode 100644 doc/imgs_results/lite_demo.png diff --git a/deploy/lite/Makefile b/deploy/lite/Makefile new file mode 100644 index 00000000..4c30d644 --- /dev/null +++ b/deploy/lite/Makefile @@ -0,0 +1,80 @@ +ARM_ABI = arm8 +export ARM_ABI + +include ../Makefile.def + +LITE_ROOT=../../../ + +THIRD_PARTY_DIR=${LITE_ROOT}/third_party + +OPENCV_VERSION=opencv4.1.0 + +OPENCV_LIBS = ../../../third_party/${OPENCV_VERSION}/arm64-v8a/libs/libopencv_imgcodecs.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/libs/libopencv_imgproc.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/libs/libopencv_core.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libtegra_hal.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibjpeg-turbo.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibwebp.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibpng.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibjasper.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/liblibtiff.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libIlmImf.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libtbb.a \ + ../../../third_party/${OPENCV_VERSION}/arm64-v8a/3rdparty/libs/libcpufeatures.a + +OPENCV_INCLUDE = -I../../../third_party/${OPENCV_VERSION}/arm64-v8a/include + +CXX_INCLUDES = $(INCLUDES) ${OPENCV_INCLUDE} -I$(LITE_ROOT)/cxx/include + +CXX_LIBS = ${OPENCV_LIBS} -L$(LITE_ROOT)/cxx/lib/ -lpaddle_light_api_shared $(SYSTEM_LIBS) + +############################################################### +# How to use one of static libaray: # +# `libpaddle_api_full_bundled.a` # +# `libpaddle_api_light_bundled.a` # +############################################################### +# Note: default use lite's shared library. # +############################################################### +# 1. Comment above line using `libpaddle_light_api_shared.so` +# 2. Undo comment below line using `libpaddle_api_light_bundled.a` + +#CXX_LIBS = $(LITE_ROOT)/cxx/lib/libpaddle_api_light_bundled.a $(SYSTEM_LIBS) + +ocr_db_crnn: fetch_opencv ocr_db_crnn.o crnn_process.o db_post_process.o clipper.o cls_process.o + $(CC) $(SYSROOT_LINK) $(CXXFLAGS_LINK) ocr_db_crnn.o crnn_process.o db_post_process.o clipper.o cls_process.o -o ocr_db_crnn $(CXX_LIBS) $(LDFLAGS) + +ocr_db_crnn.o: ocr_db_crnn.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o ocr_db_crnn.o -c ocr_db_crnn.cc + +crnn_process.o: fetch_opencv crnn_process.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o crnn_process.o -c crnn_process.cc + +cls_process.o: fetch_opencv cls_process.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o cls_process.o -c cls_process.cc + +db_post_process.o: fetch_clipper fetch_opencv db_post_process.cc + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o db_post_process.o -c db_post_process.cc + +clipper.o: fetch_clipper + $(CC) $(SYSROOT_COMPLILE) $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o clipper.o -c clipper.cpp + +fetch_clipper: + @test -e clipper.hpp || \ + ( echo "Fetch clipper " && \ + wget -c https://paddle-inference-dist.cdn.bcebos.com/PaddleLite/Clipper/clipper.hpp) + @ test -e clipper.cpp || \ + wget -c https://paddle-inference-dist.cdn.bcebos.com/PaddleLite/Clipper/clipper.cpp + +fetch_opencv: + @ test -d ${THIRD_PARTY_DIR} || mkdir ${THIRD_PARTY_DIR} + @ test -e ${THIRD_PARTY_DIR}/${OPENCV_VERSION}.tar.gz || \ + (echo "fetch opencv libs" && \ + wget -P ${THIRD_PARTY_DIR} https://paddle-inference-dist.bj.bcebos.com/${OPENCV_VERSION}.tar.gz) + @ test -d ${THIRD_PARTY_DIR}/${OPENCV_VERSION} || \ + tar -zxvf ${THIRD_PARTY_DIR}/${OPENCV_VERSION}.tar.gz -C ${THIRD_PARTY_DIR} + + +.PHONY: clean +clean: + rm -f ocr_db_crnn.o clipper.o db_post_process.o crnn_process.o cls_process.o + rm -f ocr_db_crnn diff --git a/deploy/lite/clipper.cpp b/deploy/lite/clipper.cpp new file mode 100644 index 00000000..176d8654 --- /dev/null +++ b/deploy/lite/clipper.cpp @@ -0,0 +1,4394 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/******************************************************************************* +* * +* Author : Angus Johnson * +* Version : 6.4.2 * +* Date : 27 February 2017 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2017 * +* * +* License: * +* Use, modification & distribution is subject to Boost Software License Ver 1. * +* http://www.boost.org/LICENSE_1_0.txt * +* * +* Attributions: * +* The code in this library is an extension of Bala Vatti's clipping algorithm: * +* "A generic solution to polygon clipping" * +* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * +* http://portal.acm.org/citation.cfm?id=129906 * +* * +* Computer graphics and geometric modeling: implementation and algorithms * +* By Max K. Agoston * +* Springer; 1 edition (January 4, 2005) * +* http://books.google.com/books?q=vatti+clipping+agoston * +* * +* See also: * +* "Polygon Offsetting by Computing Winding Numbers" * +* Paper no. DETC2005-85513 pp. 565-575 * +* ASME 2005 International Design Engineering Technical Conferences * +* and Computers and Information in Engineering Conference (IDETC/CIE2005) * +* September 24-28, 2005 , Long Beach, California, USA * +* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * +* * +*******************************************************************************/ + +/******************************************************************************* +* * +* This is a translation of the Delphi Clipper library and the naming style * +* used has retained a Delphi flavour. * +* * +*******************************************************************************/ + +#include "clipper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +static double const pi = 3.141592653589793238; +static double const two_pi = pi * 2; +static double const def_arc_tolerance = 0.25; + +enum Direction { dRightToLeft, dLeftToRight }; + +static int const Unassigned = -1; // edge not currently 'owning' a solution +static int const Skip = -2; // edge that would otherwise close a path + +#define HORIZONTAL (-1.0E+40) +#define TOLERANCE (1.0e-20) +#define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) + +struct TEdge { + IntPoint Bot; + IntPoint Curr; // current (updated for every new scanbeam) + IntPoint Top; + double Dx; + PolyType PolyTyp; + EdgeSide Side; // side only refers to current side of solution poly + int WindDelta; // 1 or -1 depending on winding direction + int WindCnt; + int WindCnt2; // winding count of the opposite polytype + int OutIdx; + TEdge *Next; + TEdge *Prev; + TEdge *NextInLML; + TEdge *NextInAEL; + TEdge *PrevInAEL; + TEdge *NextInSEL; + TEdge *PrevInSEL; +}; + +struct IntersectNode { + TEdge *Edge1; + TEdge *Edge2; + IntPoint Pt; +}; + +struct LocalMinimum { + cInt Y; + TEdge *LeftBound; + TEdge *RightBound; +}; + +struct OutPt; + +// OutRec: contains a path in the clipping solution. Edges in the AEL will +// carry a pointer to an OutRec when they are part of the clipping solution. +struct OutRec { + int Idx; + bool IsHole; + bool IsOpen; + OutRec *FirstLeft; // see comments in clipper.pas + PolyNode *PolyNd; + OutPt *Pts; + OutPt *BottomPt; +}; + +struct OutPt { + int Idx; + IntPoint Pt; + OutPt *Next; + OutPt *Prev; +}; + +struct Join { + OutPt *OutPt1; + OutPt *OutPt2; + IntPoint OffPt; +}; + +struct LocMinSorter { + inline bool operator()(const LocalMinimum &locMin1, + const LocalMinimum &locMin2) { + return locMin2.Y < locMin1.Y; + } +}; + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +inline cInt Round(double val) { + if ((val < 0)) + return static_cast(val - 0.5); + else + return static_cast(val + 0.5); +} +//------------------------------------------------------------------------------ + +inline cInt Abs(cInt val) { return val < 0 ? -val : val; } + +//------------------------------------------------------------------------------ +// PolyTree methods ... +//------------------------------------------------------------------------------ + +void PolyTree::Clear() { + for (PolyNodes::size_type i = 0; i < AllNodes.size(); ++i) + delete AllNodes[i]; + AllNodes.resize(0); + Childs.resize(0); +} +//------------------------------------------------------------------------------ + +PolyNode *PolyTree::GetFirst() const { + if (!Childs.empty()) + return Childs[0]; + else + return 0; +} +//------------------------------------------------------------------------------ + +int PolyTree::Total() const { + int result = (int)AllNodes.size(); + // with negative offsets, ignore the hidden outer polygon ... + if (result > 0 && Childs[0] != AllNodes[0]) + result--; + return result; +} + +//------------------------------------------------------------------------------ +// PolyNode methods ... +//------------------------------------------------------------------------------ + +PolyNode::PolyNode() : Parent(0), Index(0), m_IsOpen(false) {} +//------------------------------------------------------------------------------ + +int PolyNode::ChildCount() const { return (int)Childs.size(); } +//------------------------------------------------------------------------------ + +void PolyNode::AddChild(PolyNode &child) { + unsigned cnt = (unsigned)Childs.size(); + Childs.push_back(&child); + child.Parent = this; + child.Index = cnt; +} +//------------------------------------------------------------------------------ + +PolyNode *PolyNode::GetNext() const { + if (!Childs.empty()) + return Childs[0]; + else + return GetNextSiblingUp(); +} +//------------------------------------------------------------------------------ + +PolyNode *PolyNode::GetNextSiblingUp() const { + if (!Parent) // protects against PolyTree.GetNextSiblingUp() + return 0; + else if (Index == Parent->Childs.size() - 1) + return Parent->GetNextSiblingUp(); + else + return Parent->Childs[Index + 1]; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsHole() const { + bool result = true; + PolyNode *node = Parent; + while (node) { + result = !result; + node = node->Parent; + } + return result; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsOpen() const { return m_IsOpen; } +//------------------------------------------------------------------------------ + +#ifndef use_int32 + +//------------------------------------------------------------------------------ +// Int128 class (enables safe math on signed 64bit integers) +// eg Int128 val1((long64)9223372036854775807); //ie 2^63 -1 +// Int128 val2((long64)9223372036854775807); +// Int128 val3 = val1 * val2; +// val3.AsString => "85070591730234615847396907784232501249" (8.5e+37) +//------------------------------------------------------------------------------ + +class Int128 { +public: + ulong64 lo; + long64 hi; + + Int128(long64 _lo = 0) { + lo = (ulong64)_lo; + if (_lo < 0) + hi = -1; + else + hi = 0; + } + + Int128(const Int128 &val) : lo(val.lo), hi(val.hi) {} + + Int128(const long64 &_hi, const ulong64 &_lo) : lo(_lo), hi(_hi) {} + + Int128 &operator=(const long64 &val) { + lo = (ulong64)val; + if (val < 0) + hi = -1; + else + hi = 0; + return *this; + } + + bool operator==(const Int128 &val) const { + return (hi == val.hi && lo == val.lo); + } + + bool operator!=(const Int128 &val) const { return !(*this == val); } + + bool operator>(const Int128 &val) const { + if (hi != val.hi) + return hi > val.hi; + else + return lo > val.lo; + } + + bool operator<(const Int128 &val) const { + if (hi != val.hi) + return hi < val.hi; + else + return lo < val.lo; + } + + bool operator>=(const Int128 &val) const { return !(*this < val); } + + bool operator<=(const Int128 &val) const { return !(*this > val); } + + Int128 &operator+=(const Int128 &rhs) { + hi += rhs.hi; + lo += rhs.lo; + if (lo < rhs.lo) + hi++; + return *this; + } + + Int128 operator+(const Int128 &rhs) const { + Int128 result(*this); + result += rhs; + return result; + } + + Int128 &operator-=(const Int128 &rhs) { + *this += -rhs; + return *this; + } + + Int128 operator-(const Int128 &rhs) const { + Int128 result(*this); + result -= rhs; + return result; + } + + Int128 operator-() const // unary negation + { + if (lo == 0) + return Int128(-hi, 0); + else + return Int128(~hi, ~lo + 1); + } + + operator double() const { + const double shift64 = 18446744073709551616.0; // 2^64 + if (hi < 0) { + if (lo == 0) + return (double)hi * shift64; + else + return -(double)(~lo + ~hi * shift64); + } else + return (double)(lo + hi * shift64); + } +}; +//------------------------------------------------------------------------------ + +Int128 Int128Mul(long64 lhs, long64 rhs) { + bool negate = (lhs < 0) != (rhs < 0); + + if (lhs < 0) + lhs = -lhs; + ulong64 int1Hi = ulong64(lhs) >> 32; + ulong64 int1Lo = ulong64(lhs & 0xFFFFFFFF); + + if (rhs < 0) + rhs = -rhs; + ulong64 int2Hi = ulong64(rhs) >> 32; + ulong64 int2Lo = ulong64(rhs & 0xFFFFFFFF); + + // nb: see comments in clipper.pas + ulong64 a = int1Hi * int2Hi; + ulong64 b = int1Lo * int2Lo; + ulong64 c = int1Hi * int2Lo + int1Lo * int2Hi; + + Int128 tmp; + tmp.hi = long64(a + (c >> 32)); + tmp.lo = long64(c << 32); + tmp.lo += long64(b); + if (tmp.lo < b) + tmp.hi++; + if (negate) + tmp = -tmp; + return tmp; +}; +#endif + +//------------------------------------------------------------------------------ +// Miscellaneous global functions +//------------------------------------------------------------------------------ + +bool Orientation(const Path &poly) { return Area(poly) >= 0; } +//------------------------------------------------------------------------------ + +double Area(const Path &poly) { + int size = (int)poly.size(); + if (size < 3) + return 0; + + double a = 0; + for (int i = 0, j = size - 1; i < size; ++i) { + a += ((double)poly[j].X + poly[i].X) * ((double)poly[j].Y - poly[i].Y); + j = i; + } + return -a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutPt *op) { + const OutPt *startOp = op; + if (!op) + return 0; + double a = 0; + do { + a += (double)(op->Prev->Pt.X + op->Pt.X) * + (double)(op->Prev->Pt.Y - op->Pt.Y); + op = op->Next; + } while (op != startOp); + return a * 0.5; +} +//------------------------------------------------------------------------------ + +double Area(const OutRec &outRec) { return Area(outRec.Pts); } +//------------------------------------------------------------------------------ + +bool PointIsVertex(const IntPoint &Pt, OutPt *pp) { + OutPt *pp2 = pp; + do { + if (pp2->Pt == Pt) + return true; + pp2 = pp2->Next; + } while (pp2 != pp); + return false; +} +//------------------------------------------------------------------------------ + +// See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & +// Agathos +// http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf +int PointInPolygon(const IntPoint &pt, const Path &path) { + // returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + size_t cnt = path.size(); + if (cnt < 3) + return 0; + IntPoint ip = path[0]; + for (size_t i = 1; i <= cnt; ++i) { + IntPoint ipNext = (i == cnt ? path[0] : path[i]); + if (ipNext.Y == pt.Y) { + if ((ipNext.X == pt.X) || + (ip.Y == pt.Y && ((ipNext.X > pt.X) == (ip.X < pt.X)))) + return -1; + } + if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) { + if (ip.X >= pt.X) { + if (ipNext.X > pt.X) + result = 1 - result; + else { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } else { + if (ipNext.X > pt.X) { + double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - + (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } + } + ip = ipNext; + } + return result; +} +//------------------------------------------------------------------------------ + +int PointInPolygon(const IntPoint &pt, OutPt *op) { + // returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + OutPt *startOp = op; + for (;;) { + if (op->Next->Pt.Y == pt.Y) { + if ((op->Next->Pt.X == pt.X) || + (op->Pt.Y == pt.Y && ((op->Next->Pt.X > pt.X) == (op->Pt.X < pt.X)))) + return -1; + } + if ((op->Pt.Y < pt.Y) != (op->Next->Pt.Y < pt.Y)) { + if (op->Pt.X >= pt.X) { + if (op->Next->Pt.X > pt.X) + result = 1 - result; + else { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) + result = 1 - result; + } + } else { + if (op->Next->Pt.X > pt.X) { + double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - + (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); + if (!d) + return -1; + if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) + result = 1 - result; + } + } + } + op = op->Next; + if (startOp == op) + break; + } + return result; +} +//------------------------------------------------------------------------------ + +bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) { + OutPt *op = OutPt1; + do { + // nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon + int res = PointInPolygon(op->Pt, OutPt2); + if (res >= 0) + return res > 0; + op = op->Next; + } while (op != OutPt1); + return true; +} +//---------------------------------------------------------------------- + +bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(e1.Top.Y - e1.Bot.Y, e2.Top.X - e2.Bot.X) == + Int128Mul(e1.Top.X - e1.Bot.X, e2.Top.Y - e2.Bot.Y); + else +#endif + return (e1.Top.Y - e1.Bot.Y) * (e2.Top.X - e2.Bot.X) == + (e1.Top.X - e1.Bot.X) * (e2.Top.Y - e2.Bot.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, + bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X) == + Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y); + else +#endif + return (pt1.Y - pt2.Y) * (pt2.X - pt3.X) == + (pt1.X - pt2.X) * (pt2.Y - pt3.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, + const IntPoint pt4, bool UseFullInt64Range) { +#ifndef use_int32 + if (UseFullInt64Range) + return Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X) == + Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y); + else +#endif + return (pt1.Y - pt2.Y) * (pt3.X - pt4.X) == + (pt1.X - pt2.X) * (pt3.Y - pt4.Y); +} +//------------------------------------------------------------------------------ + +inline bool IsHorizontal(TEdge &e) { return e.Dx == HORIZONTAL; } +//------------------------------------------------------------------------------ + +inline double GetDx(const IntPoint pt1, const IntPoint pt2) { + return (pt1.Y == pt2.Y) ? HORIZONTAL + : (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y); +} +//--------------------------------------------------------------------------- + +inline void SetDx(TEdge &e) { + cInt dy = (e.Top.Y - e.Bot.Y); + if (dy == 0) + e.Dx = HORIZONTAL; + else + e.Dx = (double)(e.Top.X - e.Bot.X) / dy; +} +//--------------------------------------------------------------------------- + +inline void SwapSides(TEdge &Edge1, TEdge &Edge2) { + EdgeSide Side = Edge1.Side; + Edge1.Side = Edge2.Side; + Edge2.Side = Side; +} +//------------------------------------------------------------------------------ + +inline void SwapPolyIndexes(TEdge &Edge1, TEdge &Edge2) { + int OutIdx = Edge1.OutIdx; + Edge1.OutIdx = Edge2.OutIdx; + Edge2.OutIdx = OutIdx; +} +//------------------------------------------------------------------------------ + +inline cInt TopX(TEdge &edge, const cInt currentY) { + return (currentY == edge.Top.Y) + ? edge.Top.X + : edge.Bot.X + Round(edge.Dx * (currentY - edge.Bot.Y)); +} +//------------------------------------------------------------------------------ + +void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) { +#ifdef use_xyz + ip.Z = 0; +#endif + + double b1, b2; + if (Edge1.Dx == Edge2.Dx) { + ip.Y = Edge1.Curr.Y; + ip.X = TopX(Edge1, ip.Y); + return; + } else if (Edge1.Dx == 0) { + ip.X = Edge1.Bot.X; + if (IsHorizontal(Edge2)) + ip.Y = Edge2.Bot.Y; + else { + b2 = Edge2.Bot.Y - (Edge2.Bot.X / Edge2.Dx); + ip.Y = Round(ip.X / Edge2.Dx + b2); + } + } else if (Edge2.Dx == 0) { + ip.X = Edge2.Bot.X; + if (IsHorizontal(Edge1)) + ip.Y = Edge1.Bot.Y; + else { + b1 = Edge1.Bot.Y - (Edge1.Bot.X / Edge1.Dx); + ip.Y = Round(ip.X / Edge1.Dx + b1); + } + } else { + b1 = Edge1.Bot.X - Edge1.Bot.Y * Edge1.Dx; + b2 = Edge2.Bot.X - Edge2.Bot.Y * Edge2.Dx; + double q = (b2 - b1) / (Edge1.Dx - Edge2.Dx); + ip.Y = Round(q); + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = Round(Edge1.Dx * q + b1); + else + ip.X = Round(Edge2.Dx * q + b2); + } + + if (ip.Y < Edge1.Top.Y || ip.Y < Edge2.Top.Y) { + if (Edge1.Top.Y > Edge2.Top.Y) + ip.Y = Edge1.Top.Y; + else + ip.Y = Edge2.Top.Y; + if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) + ip.X = TopX(Edge1, ip.Y); + else + ip.X = TopX(Edge2, ip.Y); + } + // finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ... + if (ip.Y > Edge1.Curr.Y) { + ip.Y = Edge1.Curr.Y; + // use the more vertical edge to derive X ... + if (std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx)) + ip.X = TopX(Edge2, ip.Y); + else + ip.X = TopX(Edge1, ip.Y); + } +} +//------------------------------------------------------------------------------ + +void ReversePolyPtLinks(OutPt *pp) { + if (!pp) + return; + OutPt *pp1, *pp2; + pp1 = pp; + do { + pp2 = pp1->Next; + pp1->Next = pp1->Prev; + pp1->Prev = pp2; + pp1 = pp2; + } while (pp1 != pp); +} +//------------------------------------------------------------------------------ + +void DisposeOutPts(OutPt *&pp) { + if (pp == 0) + return; + pp->Prev->Next = 0; + while (pp) { + OutPt *tmpPp = pp; + pp = pp->Next; + delete tmpPp; + } +} +//------------------------------------------------------------------------------ + +inline void InitEdge(TEdge *e, TEdge *eNext, TEdge *ePrev, const IntPoint &Pt) { + std::memset(e, 0, sizeof(TEdge)); + e->Next = eNext; + e->Prev = ePrev; + e->Curr = Pt; + e->OutIdx = Unassigned; +} +//------------------------------------------------------------------------------ + +void InitEdge2(TEdge &e, PolyType Pt) { + if (e.Curr.Y >= e.Next->Curr.Y) { + e.Bot = e.Curr; + e.Top = e.Next->Curr; + } else { + e.Top = e.Curr; + e.Bot = e.Next->Curr; + } + SetDx(e); + e.PolyTyp = Pt; +} +//------------------------------------------------------------------------------ + +TEdge *RemoveEdge(TEdge *e) { + // removes e from double_linked_list (but without removing from memory) + e->Prev->Next = e->Next; + e->Next->Prev = e->Prev; + TEdge *result = e->Next; + e->Prev = 0; // flag as removed (see ClipperBase.Clear) + return result; +} +//------------------------------------------------------------------------------ + +inline void ReverseHorizontal(TEdge &e) { + // swap horizontal edges' Top and Bottom x's so they follow the natural + // progression of the bounds - ie so their xbots will align with the + // adjoining lower edge. [Helpful in the ProcessHorizontal() method.] + std::swap(e.Top.X, e.Bot.X); +#ifdef use_xyz + std::swap(e.Top.Z, e.Bot.Z); +#endif +} +//------------------------------------------------------------------------------ + +void SwapPoints(IntPoint &pt1, IntPoint &pt2) { + IntPoint tmp = pt1; + pt1 = pt2; + pt2 = tmp; +} +//------------------------------------------------------------------------------ + +bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a, + IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) { + // precondition: segments are Collinear. + if (Abs(pt1a.X - pt1b.X) > Abs(pt1a.Y - pt1b.Y)) { + if (pt1a.X > pt1b.X) + SwapPoints(pt1a, pt1b); + if (pt2a.X > pt2b.X) + SwapPoints(pt2a, pt2b); + if (pt1a.X > pt2a.X) + pt1 = pt1a; + else + pt1 = pt2a; + if (pt1b.X < pt2b.X) + pt2 = pt1b; + else + pt2 = pt2b; + return pt1.X < pt2.X; + } else { + if (pt1a.Y < pt1b.Y) + SwapPoints(pt1a, pt1b); + if (pt2a.Y < pt2b.Y) + SwapPoints(pt2a, pt2b); + if (pt1a.Y < pt2a.Y) + pt1 = pt1a; + else + pt1 = pt2a; + if (pt1b.Y > pt2b.Y) + pt2 = pt1b; + else + pt2 = pt2b; + return pt1.Y > pt2.Y; + } +} +//------------------------------------------------------------------------------ + +bool FirstIsBottomPt(const OutPt *btmPt1, const OutPt *btmPt2) { + OutPt *p = btmPt1->Prev; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) + p = p->Prev; + double dx1p = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + p = btmPt1->Next; + while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) + p = p->Next; + double dx1n = std::fabs(GetDx(btmPt1->Pt, p->Pt)); + + p = btmPt2->Prev; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) + p = p->Prev; + double dx2p = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + p = btmPt2->Next; + while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) + p = p->Next; + double dx2n = std::fabs(GetDx(btmPt2->Pt, p->Pt)); + + if (std::max(dx1p, dx1n) == std::max(dx2p, dx2n) && + std::min(dx1p, dx1n) == std::min(dx2p, dx2n)) + return Area(btmPt1) > 0; // if otherwise identical use orientation + else + return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); +} +//------------------------------------------------------------------------------ + +OutPt *GetBottomPt(OutPt *pp) { + OutPt *dups = 0; + OutPt *p = pp->Next; + while (p != pp) { + if (p->Pt.Y > pp->Pt.Y) { + pp = p; + dups = 0; + } else if (p->Pt.Y == pp->Pt.Y && p->Pt.X <= pp->Pt.X) { + if (p->Pt.X < pp->Pt.X) { + dups = 0; + pp = p; + } else { + if (p->Next != pp && p->Prev != pp) + dups = p; + } + } + p = p->Next; + } + if (dups) { + // there appears to be at least 2 vertices at BottomPt so ... + while (dups != p) { + if (!FirstIsBottomPt(p, dups)) + pp = dups; + dups = dups->Next; + while (dups->Pt != pp->Pt) + dups = dups->Next; + } + } + return pp; +} +//------------------------------------------------------------------------------ + +bool Pt2IsBetweenPt1AndPt3(const IntPoint pt1, const IntPoint pt2, + const IntPoint pt3) { + if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2)) + return false; + else if (pt1.X != pt3.X) + return (pt2.X > pt1.X) == (pt2.X < pt3.X); + else + return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); +} +//------------------------------------------------------------------------------ + +bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b) { + if (seg1a > seg1b) + std::swap(seg1a, seg1b); + if (seg2a > seg2b) + std::swap(seg2a, seg2b); + return (seg1a < seg2b) && (seg2a < seg1b); +} + +//------------------------------------------------------------------------------ +// ClipperBase class methods ... +//------------------------------------------------------------------------------ + +ClipperBase::ClipperBase() // constructor +{ + m_CurrentLM = m_MinimaList.begin(); // begin() == end() here + m_UseFullRange = false; +} +//------------------------------------------------------------------------------ + +ClipperBase::~ClipperBase() // destructor +{ + Clear(); +} +//------------------------------------------------------------------------------ + +void RangeTest(const IntPoint &Pt, bool &useFullRange) { + if (useFullRange) { + if (Pt.X > hiRange || Pt.Y > hiRange || -Pt.X > hiRange || -Pt.Y > hiRange) + throw clipperException("Coordinate outside allowed range"); + } else if (Pt.X > loRange || Pt.Y > loRange || -Pt.X > loRange || + -Pt.Y > loRange) { + useFullRange = true; + RangeTest(Pt, useFullRange); + } +} +//------------------------------------------------------------------------------ + +TEdge *FindNextLocMin(TEdge *E) { + for (;;) { + while (E->Bot != E->Prev->Bot || E->Curr == E->Top) + E = E->Next; + if (!IsHorizontal(*E) && !IsHorizontal(*E->Prev)) + break; + while (IsHorizontal(*E->Prev)) + E = E->Prev; + TEdge *E2 = E; + while (IsHorizontal(*E)) + E = E->Next; + if (E->Top.Y == E->Prev->Bot.Y) + continue; // ie just an intermediate horz. + if (E2->Prev->Bot.X < E->Bot.X) + E = E2; + break; + } + return E; +} +//------------------------------------------------------------------------------ + +TEdge *ClipperBase::ProcessBound(TEdge *E, bool NextIsForward) { + TEdge *Result = E; + TEdge *Horz = 0; + + if (E->OutIdx == Skip) { + // if edges still remain in the current bound beyond the skip edge then + // create another LocMin and call ProcessBound once more + if (NextIsForward) { + while (E->Top.Y == E->Next->Bot.Y) + E = E->Next; + // don't include top horizontals when parsing a bound a second time, + // they will be contained in the opposite bound ... + while (E != Result && IsHorizontal(*E)) + E = E->Prev; + } else { + while (E->Top.Y == E->Prev->Bot.Y) + E = E->Prev; + while (E != Result && IsHorizontal(*E)) + E = E->Next; + } + + if (E == Result) { + if (NextIsForward) + Result = E->Next; + else + Result = E->Prev; + } else { + // there are more edges in the bound beyond result starting with E + if (NextIsForward) + E = Result->Next; + else + E = Result->Prev; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + E->WindDelta = 0; + Result = ProcessBound(E, NextIsForward); + m_MinimaList.push_back(locMin); + } + return Result; + } + + TEdge *EStart; + + if (IsHorizontal(*E)) { + // We need to be careful with open paths because this may not be a + // true local minima (ie E may be following a skip edge). + // Also, consecutive horz. edges may start heading left before going right. + if (NextIsForward) + EStart = E->Prev; + else + EStart = E->Next; + if (IsHorizontal(*EStart)) // ie an adjoining horizontal skip edge + { + if (EStart->Bot.X != E->Bot.X && EStart->Top.X != E->Bot.X) + ReverseHorizontal(*E); + } else if (EStart->Bot.X != E->Bot.X) + ReverseHorizontal(*E); + } + + EStart = E; + if (NextIsForward) { + while (Result->Top.Y == Result->Next->Bot.Y && Result->Next->OutIdx != Skip) + Result = Result->Next; + if (IsHorizontal(*Result) && Result->Next->OutIdx != Skip) { + // nb: at the top of a bound, horizontals are added to the bound + // only when the preceding edge attaches to the horizontal's left vertex + // unless a Skip edge is encountered when that becomes the top divide + Horz = Result; + while (IsHorizontal(*Horz->Prev)) + Horz = Horz->Prev; + if (Horz->Prev->Top.X > Result->Next->Top.X) + Result = Horz->Prev; + } + while (E != Result) { + E->NextInLML = E->Next; + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + E = E->Next; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + Result = Result->Next; // move to the edge just beyond current bound + } else { + while (Result->Top.Y == Result->Prev->Bot.Y && Result->Prev->OutIdx != Skip) + Result = Result->Prev; + if (IsHorizontal(*Result) && Result->Prev->OutIdx != Skip) { + Horz = Result; + while (IsHorizontal(*Horz->Next)) + Horz = Horz->Next; + if (Horz->Next->Top.X == Result->Prev->Top.X || + Horz->Next->Top.X > Result->Prev->Top.X) + Result = Horz->Next; + } + + while (E != Result) { + E->NextInLML = E->Prev; + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + E = E->Prev; + } + if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) + ReverseHorizontal(*E); + Result = Result->Prev; // move to the edge just beyond current bound + } + + return Result; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) { +#ifdef use_lines + if (!Closed && PolyTyp == ptClip) + throw clipperException("AddPath: Open paths must be subject."); +#else + if (!Closed) + throw clipperException("AddPath: Open paths have been disabled."); +#endif + + int highI = (int)pg.size() - 1; + if (Closed) + while (highI > 0 && (pg[highI] == pg[0])) + --highI; + while (highI > 0 && (pg[highI] == pg[highI - 1])) + --highI; + if ((Closed && highI < 2) || (!Closed && highI < 1)) + return false; + + // create a new edge array ... + TEdge *edges = new TEdge[highI + 1]; + + bool IsFlat = true; + // 1. Basic (first) edge initialization ... + try { + edges[1].Curr = pg[1]; + RangeTest(pg[0], m_UseFullRange); + RangeTest(pg[highI], m_UseFullRange); + InitEdge(&edges[0], &edges[1], &edges[highI], pg[0]); + InitEdge(&edges[highI], &edges[0], &edges[highI - 1], pg[highI]); + for (int i = highI - 1; i >= 1; --i) { + RangeTest(pg[i], m_UseFullRange); + InitEdge(&edges[i], &edges[i + 1], &edges[i - 1], pg[i]); + } + } catch (...) { + delete[] edges; + throw; // range test fails + } + TEdge *eStart = &edges[0]; + + // 2. Remove duplicate vertices, and (when closed) collinear edges ... + TEdge *E = eStart, *eLoopStop = eStart; + for (;;) { + // nb: allows matching start and end points when not Closed ... + if (E->Curr == E->Next->Curr && (Closed || E->Next != eStart)) { + if (E == E->Next) + break; + if (E == eStart) + eStart = E->Next; + E = RemoveEdge(E); + eLoopStop = E; + continue; + } + if (E->Prev == E->Next) + break; // only two vertices + else if (Closed && SlopesEqual(E->Prev->Curr, E->Curr, E->Next->Curr, + m_UseFullRange) && + (!m_PreserveCollinear || + !Pt2IsBetweenPt1AndPt3(E->Prev->Curr, E->Curr, E->Next->Curr))) { + // Collinear edges are allowed for open paths but in closed paths + // the default is to merge adjacent collinear edges into a single edge. + // However, if the PreserveCollinear property is enabled, only overlapping + // collinear edges (ie spikes) will be removed from closed paths. + if (E == eStart) + eStart = E->Next; + E = RemoveEdge(E); + E = E->Prev; + eLoopStop = E; + continue; + } + E = E->Next; + if ((E == eLoopStop) || (!Closed && E->Next == eStart)) + break; + } + + if ((!Closed && (E == E->Next)) || (Closed && (E->Prev == E->Next))) { + delete[] edges; + return false; + } + + if (!Closed) { + m_HasOpenPaths = true; + eStart->Prev->OutIdx = Skip; + } + + // 3. Do second stage of edge initialization ... + E = eStart; + do { + InitEdge2(*E, PolyTyp); + E = E->Next; + if (IsFlat && E->Curr.Y != eStart->Curr.Y) + IsFlat = false; + } while (E != eStart); + + // 4. Finally, add edge bounds to LocalMinima list ... + + // Totally flat paths must be handled differently when adding them + // to LocalMinima list to avoid endless loops etc ... + if (IsFlat) { + if (Closed) { + delete[] edges; + return false; + } + E->Prev->OutIdx = Skip; + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + locMin.LeftBound = 0; + locMin.RightBound = E; + locMin.RightBound->Side = esRight; + locMin.RightBound->WindDelta = 0; + for (;;) { + if (E->Bot.X != E->Prev->Top.X) + ReverseHorizontal(*E); + if (E->Next->OutIdx == Skip) + break; + E->NextInLML = E->Next; + E = E->Next; + } + m_MinimaList.push_back(locMin); + m_edges.push_back(edges); + return true; + } + + m_edges.push_back(edges); + bool leftBoundIsForward; + TEdge *EMin = 0; + + // workaround to avoid an endless loop in the while loop below when + // open paths have matching start and end points ... + if (E->Prev->Bot == E->Prev->Top) + E = E->Next; + + for (;;) { + E = FindNextLocMin(E); + if (E == EMin) + break; + else if (!EMin) + EMin = E; + + // E and E.Prev now share a local minima (left aligned if horizontal). + // Compare their slopes to find which starts which bound ... + MinimaList::value_type locMin; + locMin.Y = E->Bot.Y; + if (E->Dx < E->Prev->Dx) { + locMin.LeftBound = E->Prev; + locMin.RightBound = E; + leftBoundIsForward = false; // Q.nextInLML = Q.prev + } else { + locMin.LeftBound = E; + locMin.RightBound = E->Prev; + leftBoundIsForward = true; // Q.nextInLML = Q.next + } + + if (!Closed) + locMin.LeftBound->WindDelta = 0; + else if (locMin.LeftBound->Next == locMin.RightBound) + locMin.LeftBound->WindDelta = -1; + else + locMin.LeftBound->WindDelta = 1; + locMin.RightBound->WindDelta = -locMin.LeftBound->WindDelta; + + E = ProcessBound(locMin.LeftBound, leftBoundIsForward); + if (E->OutIdx == Skip) + E = ProcessBound(E, leftBoundIsForward); + + TEdge *E2 = ProcessBound(locMin.RightBound, !leftBoundIsForward); + if (E2->OutIdx == Skip) + E2 = ProcessBound(E2, !leftBoundIsForward); + + if (locMin.LeftBound->OutIdx == Skip) + locMin.LeftBound = 0; + else if (locMin.RightBound->OutIdx == Skip) + locMin.RightBound = 0; + m_MinimaList.push_back(locMin); + if (!leftBoundIsForward) + E = E2; + } + return true; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) { + bool result = false; + for (Paths::size_type i = 0; i < ppg.size(); ++i) + if (AddPath(ppg[i], PolyTyp, Closed)) + result = true; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Clear() { + DisposeLocalMinimaList(); + for (EdgeList::size_type i = 0; i < m_edges.size(); ++i) { + TEdge *edges = m_edges[i]; + delete[] edges; + } + m_edges.clear(); + m_UseFullRange = false; + m_HasOpenPaths = false; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Reset() { + m_CurrentLM = m_MinimaList.begin(); + if (m_CurrentLM == m_MinimaList.end()) + return; // ie nothing to process + std::sort(m_MinimaList.begin(), m_MinimaList.end(), LocMinSorter()); + + m_Scanbeam = ScanbeamList(); // clears/resets priority_queue + // reset all edges ... + for (MinimaList::iterator lm = m_MinimaList.begin(); lm != m_MinimaList.end(); + ++lm) { + InsertScanbeam(lm->Y); + TEdge *e = lm->LeftBound; + if (e) { + e->Curr = e->Bot; + e->Side = esLeft; + e->OutIdx = Unassigned; + } + + e = lm->RightBound; + if (e) { + e->Curr = e->Bot; + e->Side = esRight; + e->OutIdx = Unassigned; + } + } + m_ActiveEdges = 0; + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeLocalMinimaList() { + m_MinimaList.clear(); + m_CurrentLM = m_MinimaList.begin(); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::PopLocalMinima(cInt Y, const LocalMinimum *&locMin) { + if (m_CurrentLM == m_MinimaList.end() || (*m_CurrentLM).Y != Y) + return false; + locMin = &(*m_CurrentLM); + ++m_CurrentLM; + return true; +} +//------------------------------------------------------------------------------ + +IntRect ClipperBase::GetBounds() { + IntRect result; + MinimaList::iterator lm = m_MinimaList.begin(); + if (lm == m_MinimaList.end()) { + result.left = result.top = result.right = result.bottom = 0; + return result; + } + result.left = lm->LeftBound->Bot.X; + result.top = lm->LeftBound->Bot.Y; + result.right = lm->LeftBound->Bot.X; + result.bottom = lm->LeftBound->Bot.Y; + while (lm != m_MinimaList.end()) { + // todo - needs fixing for open paths + result.bottom = std::max(result.bottom, lm->LeftBound->Bot.Y); + TEdge *e = lm->LeftBound; + for (;;) { + TEdge *bottomE = e; + while (e->NextInLML) { + if (e->Bot.X < result.left) + result.left = e->Bot.X; + if (e->Bot.X > result.right) + result.right = e->Bot.X; + e = e->NextInLML; + } + result.left = std::min(result.left, e->Bot.X); + result.right = std::max(result.right, e->Bot.X); + result.left = std::min(result.left, e->Top.X); + result.right = std::max(result.right, e->Top.X); + result.top = std::min(result.top, e->Top.Y); + if (bottomE == lm->LeftBound) + e = lm->RightBound; + else + break; + } + ++lm; + } + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::InsertScanbeam(const cInt Y) { m_Scanbeam.push(Y); } +//------------------------------------------------------------------------------ + +bool ClipperBase::PopScanbeam(cInt &Y) { + if (m_Scanbeam.empty()) + return false; + Y = m_Scanbeam.top(); + m_Scanbeam.pop(); + while (!m_Scanbeam.empty() && Y == m_Scanbeam.top()) { + m_Scanbeam.pop(); + } // Pop duplicates. + return true; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeAllOutRecs() { + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + DisposeOutRec(i); + m_PolyOuts.clear(); +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeOutRec(PolyOutList::size_type index) { + OutRec *outRec = m_PolyOuts[index]; + if (outRec->Pts) + DisposeOutPts(outRec->Pts); + delete outRec; + m_PolyOuts[index] = 0; +} +//------------------------------------------------------------------------------ + +void ClipperBase::DeleteFromAEL(TEdge *e) { + TEdge *AelPrev = e->PrevInAEL; + TEdge *AelNext = e->NextInAEL; + if (!AelPrev && !AelNext && (e != m_ActiveEdges)) + return; // already deleted + if (AelPrev) + AelPrev->NextInAEL = AelNext; + else + m_ActiveEdges = AelNext; + if (AelNext) + AelNext->PrevInAEL = AelPrev; + e->NextInAEL = 0; + e->PrevInAEL = 0; +} +//------------------------------------------------------------------------------ + +OutRec *ClipperBase::CreateOutRec() { + OutRec *result = new OutRec; + result->IsHole = false; + result->IsOpen = false; + result->FirstLeft = 0; + result->Pts = 0; + result->BottomPt = 0; + result->PolyNd = 0; + m_PolyOuts.push_back(result); + result->Idx = (int)m_PolyOuts.size() - 1; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::SwapPositionsInAEL(TEdge *Edge1, TEdge *Edge2) { + // check that one or other edge hasn't already been removed from AEL ... + if (Edge1->NextInAEL == Edge1->PrevInAEL || + Edge2->NextInAEL == Edge2->PrevInAEL) + return; + + if (Edge1->NextInAEL == Edge2) { + TEdge *Next = Edge2->NextInAEL; + if (Next) + Next->PrevInAEL = Edge1; + TEdge *Prev = Edge1->PrevInAEL; + if (Prev) + Prev->NextInAEL = Edge2; + Edge2->PrevInAEL = Prev; + Edge2->NextInAEL = Edge1; + Edge1->PrevInAEL = Edge2; + Edge1->NextInAEL = Next; + } else if (Edge2->NextInAEL == Edge1) { + TEdge *Next = Edge1->NextInAEL; + if (Next) + Next->PrevInAEL = Edge2; + TEdge *Prev = Edge2->PrevInAEL; + if (Prev) + Prev->NextInAEL = Edge1; + Edge1->PrevInAEL = Prev; + Edge1->NextInAEL = Edge2; + Edge2->PrevInAEL = Edge1; + Edge2->NextInAEL = Next; + } else { + TEdge *Next = Edge1->NextInAEL; + TEdge *Prev = Edge1->PrevInAEL; + Edge1->NextInAEL = Edge2->NextInAEL; + if (Edge1->NextInAEL) + Edge1->NextInAEL->PrevInAEL = Edge1; + Edge1->PrevInAEL = Edge2->PrevInAEL; + if (Edge1->PrevInAEL) + Edge1->PrevInAEL->NextInAEL = Edge1; + Edge2->NextInAEL = Next; + if (Edge2->NextInAEL) + Edge2->NextInAEL->PrevInAEL = Edge2; + Edge2->PrevInAEL = Prev; + if (Edge2->PrevInAEL) + Edge2->PrevInAEL->NextInAEL = Edge2; + } + + if (!Edge1->PrevInAEL) + m_ActiveEdges = Edge1; + else if (!Edge2->PrevInAEL) + m_ActiveEdges = Edge2; +} +//------------------------------------------------------------------------------ + +void ClipperBase::UpdateEdgeIntoAEL(TEdge *&e) { + if (!e->NextInLML) + throw clipperException("UpdateEdgeIntoAEL: invalid call"); + + e->NextInLML->OutIdx = e->OutIdx; + TEdge *AelPrev = e->PrevInAEL; + TEdge *AelNext = e->NextInAEL; + if (AelPrev) + AelPrev->NextInAEL = e->NextInLML; + else + m_ActiveEdges = e->NextInLML; + if (AelNext) + AelNext->PrevInAEL = e->NextInLML; + e->NextInLML->Side = e->Side; + e->NextInLML->WindDelta = e->WindDelta; + e->NextInLML->WindCnt = e->WindCnt; + e->NextInLML->WindCnt2 = e->WindCnt2; + e = e->NextInLML; + e->Curr = e->Bot; + e->PrevInAEL = AelPrev; + e->NextInAEL = AelNext; + if (!IsHorizontal(*e)) + InsertScanbeam(e->Top.Y); +} +//------------------------------------------------------------------------------ + +bool ClipperBase::LocalMinimaPending() { + return (m_CurrentLM != m_MinimaList.end()); +} + +//------------------------------------------------------------------------------ +// TClipper methods ... +//------------------------------------------------------------------------------ + +Clipper::Clipper(int initOptions) + : ClipperBase() // constructor +{ + m_ExecuteLocked = false; + m_UseFullRange = false; + m_ReverseOutput = ((initOptions & ioReverseSolution) != 0); + m_StrictSimple = ((initOptions & ioStrictlySimple) != 0); + m_PreserveCollinear = ((initOptions & ioPreserveCollinear) != 0); + m_HasOpenPaths = false; +#ifdef use_xyz + m_ZFill = 0; +#endif +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::ZFillFunction(ZFillCallback zFillFunc) { m_ZFill = zFillFunc; } +//------------------------------------------------------------------------------ +#endif + +bool Clipper::Execute(ClipType clipType, Paths &solution, + PolyFillType fillType) { + return Execute(clipType, solution, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree &polytree, + PolyFillType fillType) { + return Execute(clipType, polytree, fillType, fillType); +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, Paths &solution, + PolyFillType subjFillType, PolyFillType clipFillType) { + if (m_ExecuteLocked) + return false; + if (m_HasOpenPaths) + throw clipperException( + "Error: PolyTree struct is needed for open path clipping."); + m_ExecuteLocked = true; + solution.resize(0); + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = false; + bool succeeded = ExecuteInternal(); + if (succeeded) + BuildResult(solution); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree &polytree, + PolyFillType subjFillType, PolyFillType clipFillType) { + if (m_ExecuteLocked) + return false; + m_ExecuteLocked = true; + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = true; + bool succeeded = ExecuteInternal(); + if (succeeded) + BuildResult2(polytree); + DisposeAllOutRecs(); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::FixHoleLinkage(OutRec &outrec) { + // skip OutRecs that (a) contain outermost polygons or + //(b) already have the correct owner/child linkage ... + if (!outrec.FirstLeft || + (outrec.IsHole != outrec.FirstLeft->IsHole && outrec.FirstLeft->Pts)) + return; + + OutRec *orfl = outrec.FirstLeft; + while (orfl && ((orfl->IsHole == outrec.IsHole) || !orfl->Pts)) + orfl = orfl->FirstLeft; + outrec.FirstLeft = orfl; +} +//------------------------------------------------------------------------------ + +bool Clipper::ExecuteInternal() { + bool succeeded = true; + try { + Reset(); + m_Maxima = MaximaList(); + m_SortedEdges = 0; + + succeeded = true; + cInt botY, topY; + if (!PopScanbeam(botY)) + return false; + InsertLocalMinimaIntoAEL(botY); + while (PopScanbeam(topY) || LocalMinimaPending()) { + ProcessHorizontals(); + ClearGhostJoins(); + if (!ProcessIntersections(topY)) { + succeeded = false; + break; + } + ProcessEdgesAtTopOfScanbeam(topY); + botY = topY; + InsertLocalMinimaIntoAEL(botY); + } + } catch (...) { + succeeded = false; + } + + if (succeeded) { + // fix orientations ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts || outRec->IsOpen) + continue; + if ((outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) + ReversePolyPtLinks(outRec->Pts); + } + + if (!m_Joins.empty()) + JoinCommonEdges(); + + // unfortunately FixupOutPolygon() must be done after JoinCommonEdges() + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->Pts) + continue; + if (outRec->IsOpen) + FixupOutPolyline(*outRec); + else + FixupOutPolygon(*outRec); + } + + if (m_StrictSimple) + DoSimplePolygons(); + } + + ClearJoins(); + ClearGhostJoins(); + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::SetWindingCount(TEdge &edge) { + TEdge *e = edge.PrevInAEL; + // find the edge of the same polytype that immediately preceeds 'edge' in AEL + while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0))) + e = e->PrevInAEL; + if (!e) { + if (edge.WindDelta == 0) { + PolyFillType pft = + (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType); + edge.WindCnt = (pft == pftNegative ? -1 : 1); + } else + edge.WindCnt = edge.WindDelta; + edge.WindCnt2 = 0; + e = m_ActiveEdges; // ie get ready to calc WindCnt2 + } else if (edge.WindDelta == 0 && m_ClipType != ctUnion) { + edge.WindCnt = 1; + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } else if (IsEvenOddFillType(edge)) { + // EvenOdd filling ... + if (edge.WindDelta == 0) { + // are we inside a subj polygon ... + bool Inside = true; + TEdge *e2 = e->PrevInAEL; + while (e2) { + if (e2->PolyTyp == e->PolyTyp && e2->WindDelta != 0) + Inside = !Inside; + e2 = e2->PrevInAEL; + } + edge.WindCnt = (Inside ? 0 : 1); + } else { + edge.WindCnt = edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } else { + // nonZero, Positive or Negative filling ... + if (e->WindCnt * e->WindDelta < 0) { + // prev edge is 'decreasing' WindCount (WC) toward zero + // so we're outside the previous polygon ... + if (Abs(e->WindCnt) > 1) { + // outside prev poly but still inside another. + // when reversing direction of prev poly use the same WC + if (e->WindDelta * edge.WindDelta < 0) + edge.WindCnt = e->WindCnt; + // otherwise continue to 'decrease' WC ... + else + edge.WindCnt = e->WindCnt + edge.WindDelta; + } else + // now outside all polys of same polytype so set own WC ... + edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta); + } else { + // prev edge is 'increasing' WindCount (WC) away from zero + // so we're inside the previous polygon ... + if (edge.WindDelta == 0) + edge.WindCnt = (e->WindCnt < 0 ? e->WindCnt - 1 : e->WindCnt + 1); + // if wind direction is reversing prev then use same WC + else if (e->WindDelta * edge.WindDelta < 0) + edge.WindCnt = e->WindCnt; + // otherwise add to WC ... + else + edge.WindCnt = e->WindCnt + edge.WindDelta; + } + edge.WindCnt2 = e->WindCnt2; + e = e->NextInAEL; // ie get ready to calc WindCnt2 + } + + // update WindCnt2 ... + if (IsEvenOddAltFillType(edge)) { + // EvenOdd filling ... + while (e != &edge) { + if (e->WindDelta != 0) + edge.WindCnt2 = (edge.WindCnt2 == 0 ? 1 : 0); + e = e->NextInAEL; + } + } else { + // nonZero, Positive or Negative filling ... + while (e != &edge) { + edge.WindCnt2 += e->WindDelta; + e = e->NextInAEL; + } + } +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddFillType(const TEdge &edge) const { + if (edge.PolyTyp == ptSubject) + return m_SubjFillType == pftEvenOdd; + else + return m_ClipFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddAltFillType(const TEdge &edge) const { + if (edge.PolyTyp == ptSubject) + return m_ClipFillType == pftEvenOdd; + else + return m_SubjFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsContributing(const TEdge &edge) const { + PolyFillType pft, pft2; + if (edge.PolyTyp == ptSubject) { + pft = m_SubjFillType; + pft2 = m_ClipFillType; + } else { + pft = m_ClipFillType; + pft2 = m_SubjFillType; + } + + switch (pft) { + case pftEvenOdd: + // return false if a subj line has been flagged as inside a subj polygon + if (edge.WindDelta == 0 && edge.WindCnt != 1) + return false; + break; + case pftNonZero: + if (Abs(edge.WindCnt) != 1) + return false; + break; + case pftPositive: + if (edge.WindCnt != 1) + return false; + break; + default: // pftNegative + if (edge.WindCnt != -1) + return false; + } + + switch (m_ClipType) { + case ctIntersection: + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctUnion: + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + break; + case ctDifference: + if (edge.PolyTyp == ptSubject) + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 != 0); + case pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + break; + case ctXor: + if (edge.WindDelta == 0) // XOr always contributing unless open + switch (pft2) { + case pftEvenOdd: + case pftNonZero: + return (edge.WindCnt2 == 0); + case pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + return true; + break; + default: + return true; + } +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { + OutPt *result; + TEdge *e, *prevE; + if (IsHorizontal(*e2) || (e1->Dx > e2->Dx)) { + result = AddOutPt(e1, Pt); + e2->OutIdx = e1->OutIdx; + e1->Side = esLeft; + e2->Side = esRight; + e = e1; + if (e->PrevInAEL == e2) + prevE = e2->PrevInAEL; + else + prevE = e->PrevInAEL; + } else { + result = AddOutPt(e2, Pt); + e1->OutIdx = e2->OutIdx; + e1->Side = esRight; + e2->Side = esLeft; + e = e2; + if (e->PrevInAEL == e1) + prevE = e1->PrevInAEL; + else + prevE = e->PrevInAEL; + } + + if (prevE && prevE->OutIdx >= 0 && prevE->Top.Y < Pt.Y && e->Top.Y < Pt.Y) { + cInt xPrev = TopX(*prevE, Pt.Y); + cInt xE = TopX(*e, Pt.Y); + if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) && + SlopesEqual(IntPoint(xPrev, Pt.Y), prevE->Top, IntPoint(xE, Pt.Y), + e->Top, m_UseFullRange)) { + OutPt *outPt = AddOutPt(prevE, Pt); + AddJoin(result, outPt, e->Top); + } + } + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { + AddOutPt(e1, Pt); + if (e2->WindDelta == 0) + AddOutPt(e2, Pt); + if (e1->OutIdx == e2->OutIdx) { + e1->OutIdx = Unassigned; + e2->OutIdx = Unassigned; + } else if (e1->OutIdx < e2->OutIdx) + AppendPolygon(e1, e2); + else + AppendPolygon(e2, e1); +} +//------------------------------------------------------------------------------ + +void Clipper::AddEdgeToSEL(TEdge *edge) { + // SEL pointers in PEdge are reused to build a list of horizontal edges. + // However, we don't need to worry about order with horizontal edge + // processing. + if (!m_SortedEdges) { + m_SortedEdges = edge; + edge->PrevInSEL = 0; + edge->NextInSEL = 0; + } else { + edge->NextInSEL = m_SortedEdges; + edge->PrevInSEL = 0; + m_SortedEdges->PrevInSEL = edge; + m_SortedEdges = edge; + } +} +//------------------------------------------------------------------------------ + +bool Clipper::PopEdgeFromSEL(TEdge *&edge) { + if (!m_SortedEdges) + return false; + edge = m_SortedEdges; + DeleteFromSEL(m_SortedEdges); + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::CopyAELToSEL() { + TEdge *e = m_ActiveEdges; + m_SortedEdges = e; + while (e) { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::AddJoin(OutPt *op1, OutPt *op2, const IntPoint OffPt) { + Join *j = new Join; + j->OutPt1 = op1; + j->OutPt2 = op2; + j->OffPt = OffPt; + m_Joins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearJoins() { + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) + delete m_Joins[i]; + m_Joins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearGhostJoins() { + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); i++) + delete m_GhostJoins[i]; + m_GhostJoins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::AddGhostJoin(OutPt *op, const IntPoint OffPt) { + Join *j = new Join; + j->OutPt1 = op; + j->OutPt2 = 0; + j->OffPt = OffPt; + m_GhostJoins.push_back(j); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) { + const LocalMinimum *lm; + while (PopLocalMinima(botY, lm)) { + TEdge *lb = lm->LeftBound; + TEdge *rb = lm->RightBound; + + OutPt *Op1 = 0; + if (!lb) { + // nb: don't insert LB into either AEL or SEL + InsertEdgeIntoAEL(rb, 0); + SetWindingCount(*rb); + if (IsContributing(*rb)) + Op1 = AddOutPt(rb, rb->Bot); + } else if (!rb) { + InsertEdgeIntoAEL(lb, 0); + SetWindingCount(*lb); + if (IsContributing(*lb)) + Op1 = AddOutPt(lb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } else { + InsertEdgeIntoAEL(lb, 0); + InsertEdgeIntoAEL(rb, lb); + SetWindingCount(*lb); + rb->WindCnt = lb->WindCnt; + rb->WindCnt2 = lb->WindCnt2; + if (IsContributing(*lb)) + Op1 = AddLocalMinPoly(lb, rb, lb->Bot); + InsertScanbeam(lb->Top.Y); + } + + if (rb) { + if (IsHorizontal(*rb)) { + AddEdgeToSEL(rb); + if (rb->NextInLML) + InsertScanbeam(rb->NextInLML->Top.Y); + } else + InsertScanbeam(rb->Top.Y); + } + + if (!lb || !rb) + continue; + + // if any output polygons share an edge, they'll need joining later ... + if (Op1 && IsHorizontal(*rb) && m_GhostJoins.size() > 0 && + (rb->WindDelta != 0)) { + for (JoinList::size_type i = 0; i < m_GhostJoins.size(); ++i) { + Join *jr = m_GhostJoins[i]; + // if the horizontal Rb and a 'ghost' horizontal overlap, then convert + // the 'ghost' join to a real join ready for later ... + if (HorzSegmentsOverlap(jr->OutPt1->Pt.X, jr->OffPt.X, rb->Bot.X, + rb->Top.X)) + AddJoin(jr->OutPt1, Op1, jr->OffPt); + } + } + + if (lb->OutIdx >= 0 && lb->PrevInAEL && + lb->PrevInAEL->Curr.X == lb->Bot.X && lb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top, + m_UseFullRange) && + (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) { + OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot); + AddJoin(Op1, Op2, lb->Top); + } + + if (lb->NextInAEL != rb) { + + if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 && + SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr, + rb->Top, m_UseFullRange) && + (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) { + OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot); + AddJoin(Op1, Op2, rb->Top); + } + + TEdge *e = lb->NextInAEL; + if (e) { + while (e != rb) { + // nb: For calculating winding counts etc, IntersectEdges() assumes + // that param1 will be to the Right of param2 ABOVE the intersection + // ... + IntersectEdges(rb, e, lb->Curr); // order important here + e = e->NextInAEL; + } + } + } + } +} +//------------------------------------------------------------------------------ + +void Clipper::DeleteFromSEL(TEdge *e) { + TEdge *SelPrev = e->PrevInSEL; + TEdge *SelNext = e->NextInSEL; + if (!SelPrev && !SelNext && (e != m_SortedEdges)) + return; // already deleted + if (SelPrev) + SelPrev->NextInSEL = SelNext; + else + m_SortedEdges = SelNext; + if (SelNext) + SelNext->PrevInSEL = SelPrev; + e->NextInSEL = 0; + e->PrevInSEL = 0; +} +//------------------------------------------------------------------------------ + +#ifdef use_xyz +void Clipper::SetZ(IntPoint &pt, TEdge &e1, TEdge &e2) { + if (pt.Z != 0 || !m_ZFill) + return; + else if (pt == e1.Bot) + pt.Z = e1.Bot.Z; + else if (pt == e1.Top) + pt.Z = e1.Top.Z; + else if (pt == e2.Bot) + pt.Z = e2.Bot.Z; + else if (pt == e2.Top) + pt.Z = e2.Top.Z; + else + (*m_ZFill)(e1.Bot, e1.Top, e2.Bot, e2.Top, pt); +} +//------------------------------------------------------------------------------ +#endif + +void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) { + bool e1Contributing = (e1->OutIdx >= 0); + bool e2Contributing = (e2->OutIdx >= 0); + +#ifdef use_xyz + SetZ(Pt, *e1, *e2); +#endif + +#ifdef use_lines + // if either edge is on an OPEN path ... + if (e1->WindDelta == 0 || e2->WindDelta == 0) { + // ignore subject-subject open path intersections UNLESS they + // are both open paths, AND they are both 'contributing maximas' ... + if (e1->WindDelta == 0 && e2->WindDelta == 0) + return; + + // if intersecting a subj line with a subj poly ... + else if (e1->PolyTyp == e2->PolyTyp && e1->WindDelta != e2->WindDelta && + m_ClipType == ctUnion) { + if (e1->WindDelta == 0) { + if (e2Contributing) { + AddOutPt(e1, Pt); + if (e1Contributing) + e1->OutIdx = Unassigned; + } + } else { + if (e1Contributing) { + AddOutPt(e2, Pt); + if (e2Contributing) + e2->OutIdx = Unassigned; + } + } + } else if (e1->PolyTyp != e2->PolyTyp) { + // toggle subj open path OutIdx on/off when Abs(clip.WndCnt) == 1 ... + if ((e1->WindDelta == 0) && abs(e2->WindCnt) == 1 && + (m_ClipType != ctUnion || e2->WindCnt2 == 0)) { + AddOutPt(e1, Pt); + if (e1Contributing) + e1->OutIdx = Unassigned; + } else if ((e2->WindDelta == 0) && (abs(e1->WindCnt) == 1) && + (m_ClipType != ctUnion || e1->WindCnt2 == 0)) { + AddOutPt(e2, Pt); + if (e2Contributing) + e2->OutIdx = Unassigned; + } + } + return; + } +#endif + + // update winding counts... + // assumes that e1 will be to the Right of e2 ABOVE the intersection + if (e1->PolyTyp == e2->PolyTyp) { + if (IsEvenOddFillType(*e1)) { + int oldE1WindCnt = e1->WindCnt; + e1->WindCnt = e2->WindCnt; + e2->WindCnt = oldE1WindCnt; + } else { + if (e1->WindCnt + e2->WindDelta == 0) + e1->WindCnt = -e1->WindCnt; + else + e1->WindCnt += e2->WindDelta; + if (e2->WindCnt - e1->WindDelta == 0) + e2->WindCnt = -e2->WindCnt; + else + e2->WindCnt -= e1->WindDelta; + } + } else { + if (!IsEvenOddFillType(*e2)) + e1->WindCnt2 += e2->WindDelta; + else + e1->WindCnt2 = (e1->WindCnt2 == 0) ? 1 : 0; + if (!IsEvenOddFillType(*e1)) + e2->WindCnt2 -= e1->WindDelta; + else + e2->WindCnt2 = (e2->WindCnt2 == 0) ? 1 : 0; + } + + PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2; + if (e1->PolyTyp == ptSubject) { + e1FillType = m_SubjFillType; + e1FillType2 = m_ClipFillType; + } else { + e1FillType = m_ClipFillType; + e1FillType2 = m_SubjFillType; + } + if (e2->PolyTyp == ptSubject) { + e2FillType = m_SubjFillType; + e2FillType2 = m_ClipFillType; + } else { + e2FillType = m_ClipFillType; + e2FillType2 = m_SubjFillType; + } + + cInt e1Wc, e2Wc; + switch (e1FillType) { + case pftPositive: + e1Wc = e1->WindCnt; + break; + case pftNegative: + e1Wc = -e1->WindCnt; + break; + default: + e1Wc = Abs(e1->WindCnt); + } + switch (e2FillType) { + case pftPositive: + e2Wc = e2->WindCnt; + break; + case pftNegative: + e2Wc = -e2->WindCnt; + break; + default: + e2Wc = Abs(e2->WindCnt); + } + + if (e1Contributing && e2Contributing) { + if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || + (e1->PolyTyp != e2->PolyTyp && m_ClipType != ctXor)) { + AddLocalMaxPoly(e1, e2, Pt); + } else { + AddOutPt(e1, Pt); + AddOutPt(e2, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if (e1Contributing) { + if (e2Wc == 0 || e2Wc == 1) { + AddOutPt(e1, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if (e2Contributing) { + if (e1Wc == 0 || e1Wc == 1) { + AddOutPt(e2, Pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } else if ((e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) { + // neither edge is currently contributing ... + + cInt e1Wc2, e2Wc2; + switch (e1FillType2) { + case pftPositive: + e1Wc2 = e1->WindCnt2; + break; + case pftNegative: + e1Wc2 = -e1->WindCnt2; + break; + default: + e1Wc2 = Abs(e1->WindCnt2); + } + switch (e2FillType2) { + case pftPositive: + e2Wc2 = e2->WindCnt2; + break; + case pftNegative: + e2Wc2 = -e2->WindCnt2; + break; + default: + e2Wc2 = Abs(e2->WindCnt2); + } + + if (e1->PolyTyp != e2->PolyTyp) { + AddLocalMinPoly(e1, e2, Pt); + } else if (e1Wc == 1 && e2Wc == 1) + switch (m_ClipType) { + case ctIntersection: + if (e1Wc2 > 0 && e2Wc2 > 0) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctUnion: + if (e1Wc2 <= 0 && e2Wc2 <= 0) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctDifference: + if (((e1->PolyTyp == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1->PolyTyp == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + AddLocalMinPoly(e1, e2, Pt); + break; + case ctXor: + AddLocalMinPoly(e1, e2, Pt); + } + else + SwapSides(*e1, *e2); + } +} +//------------------------------------------------------------------------------ + +void Clipper::SetHoleState(TEdge *e, OutRec *outrec) { + TEdge *e2 = e->PrevInAEL; + TEdge *eTmp = 0; + while (e2) { + if (e2->OutIdx >= 0 && e2->WindDelta != 0) { + if (!eTmp) + eTmp = e2; + else if (eTmp->OutIdx == e2->OutIdx) + eTmp = 0; + } + e2 = e2->PrevInAEL; + } + if (!eTmp) { + outrec->FirstLeft = 0; + outrec->IsHole = false; + } else { + outrec->FirstLeft = m_PolyOuts[eTmp->OutIdx]; + outrec->IsHole = !outrec->FirstLeft->IsHole; + } +} +//------------------------------------------------------------------------------ + +OutRec *GetLowermostRec(OutRec *outRec1, OutRec *outRec2) { + // work out which polygon fragment has the correct hole state ... + if (!outRec1->BottomPt) + outRec1->BottomPt = GetBottomPt(outRec1->Pts); + if (!outRec2->BottomPt) + outRec2->BottomPt = GetBottomPt(outRec2->Pts); + OutPt *OutPt1 = outRec1->BottomPt; + OutPt *OutPt2 = outRec2->BottomPt; + if (OutPt1->Pt.Y > OutPt2->Pt.Y) + return outRec1; + else if (OutPt1->Pt.Y < OutPt2->Pt.Y) + return outRec2; + else if (OutPt1->Pt.X < OutPt2->Pt.X) + return outRec1; + else if (OutPt1->Pt.X > OutPt2->Pt.X) + return outRec2; + else if (OutPt1->Next == OutPt1) + return outRec2; + else if (OutPt2->Next == OutPt2) + return outRec1; + else if (FirstIsBottomPt(OutPt1, OutPt2)) + return outRec1; + else + return outRec2; +} +//------------------------------------------------------------------------------ + +bool OutRec1RightOfOutRec2(OutRec *outRec1, OutRec *outRec2) { + do { + outRec1 = outRec1->FirstLeft; + if (outRec1 == outRec2) + return true; + } while (outRec1); + return false; +} +//------------------------------------------------------------------------------ + +OutRec *Clipper::GetOutRec(int Idx) { + OutRec *outrec = m_PolyOuts[Idx]; + while (outrec != m_PolyOuts[outrec->Idx]) + outrec = m_PolyOuts[outrec->Idx]; + return outrec; +} +//------------------------------------------------------------------------------ + +void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) { + // get the start and ends of both output polygons ... + OutRec *outRec1 = m_PolyOuts[e1->OutIdx]; + OutRec *outRec2 = m_PolyOuts[e2->OutIdx]; + + OutRec *holeStateRec; + if (OutRec1RightOfOutRec2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + // get the start and ends of both output polygons and + // join e2 poly onto e1 poly and delete pointers to e2 ... + + OutPt *p1_lft = outRec1->Pts; + OutPt *p1_rt = p1_lft->Prev; + OutPt *p2_lft = outRec2->Pts; + OutPt *p2_rt = p2_lft->Prev; + + // join e2 poly onto e1 poly and delete pointers to e2 ... + if (e1->Side == esLeft) { + if (e2->Side == esLeft) { + // z y x a b c + ReversePolyPtLinks(p2_lft); + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + outRec1->Pts = p2_rt; + } else { + // x y z a b c + p2_rt->Next = p1_lft; + p1_lft->Prev = p2_rt; + p2_lft->Prev = p1_rt; + p1_rt->Next = p2_lft; + outRec1->Pts = p2_lft; + } + } else { + if (e2->Side == esRight) { + // a b c z y x + ReversePolyPtLinks(p2_lft); + p1_rt->Next = p2_rt; + p2_rt->Prev = p1_rt; + p2_lft->Next = p1_lft; + p1_lft->Prev = p2_lft; + } else { + // a b c x y z + p1_rt->Next = p2_lft; + p2_lft->Prev = p1_rt; + p1_lft->Prev = p2_rt; + p2_rt->Next = p1_lft; + } + } + + outRec1->BottomPt = 0; + if (holeStateRec == outRec2) { + if (outRec2->FirstLeft != outRec1) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec1->IsHole = outRec2->IsHole; + } + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->FirstLeft = outRec1; + + int OKIdx = e1->OutIdx; + int ObsoleteIdx = e2->OutIdx; + + e1->OutIdx = + Unassigned; // nb: safe because we only get here via AddLocalMaxPoly + e2->OutIdx = Unassigned; + + TEdge *e = m_ActiveEdges; + while (e) { + if (e->OutIdx == ObsoleteIdx) { + e->OutIdx = OKIdx; + e->Side = e1->Side; + break; + } + e = e->NextInAEL; + } + + outRec2->Idx = outRec1->Idx; +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::AddOutPt(TEdge *e, const IntPoint &pt) { + if (e->OutIdx < 0) { + OutRec *outRec = CreateOutRec(); + outRec->IsOpen = (e->WindDelta == 0); + OutPt *newOp = new OutPt; + outRec->Pts = newOp; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = newOp; + newOp->Prev = newOp; + if (!outRec->IsOpen) + SetHoleState(e, outRec); + e->OutIdx = outRec->Idx; + return newOp; + } else { + OutRec *outRec = m_PolyOuts[e->OutIdx]; + // OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' + OutPt *op = outRec->Pts; + + bool ToFront = (e->Side == esLeft); + if (ToFront && (pt == op->Pt)) + return op; + else if (!ToFront && (pt == op->Prev->Pt)) + return op->Prev; + + OutPt *newOp = new OutPt; + newOp->Idx = outRec->Idx; + newOp->Pt = pt; + newOp->Next = op; + newOp->Prev = op->Prev; + newOp->Prev->Next = newOp; + op->Prev = newOp; + if (ToFront) + outRec->Pts = newOp; + return newOp; + } +} +//------------------------------------------------------------------------------ + +OutPt *Clipper::GetLastOutPt(TEdge *e) { + OutRec *outRec = m_PolyOuts[e->OutIdx]; + if (e->Side == esLeft) + return outRec->Pts; + else + return outRec->Pts->Prev; +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessHorizontals() { + TEdge *horzEdge; + while (PopEdgeFromSEL(horzEdge)) + ProcessHorizontal(horzEdge); +} +//------------------------------------------------------------------------------ + +inline bool IsMinima(TEdge *e) { + return e && (e->Prev->NextInLML != e) && (e->Next->NextInLML != e); +} +//------------------------------------------------------------------------------ + +inline bool IsMaxima(TEdge *e, const cInt Y) { + return e && e->Top.Y == Y && !e->NextInLML; +} +//------------------------------------------------------------------------------ + +inline bool IsIntermediate(TEdge *e, const cInt Y) { + return e->Top.Y == Y && e->NextInLML; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPair(TEdge *e) { + if ((e->Next->Top == e->Top) && !e->Next->NextInLML) + return e->Next; + else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML) + return e->Prev; + else + return 0; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPairEx(TEdge *e) { + // as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's + // horizontal) + TEdge *result = GetMaximaPair(e); + if (result && + (result->OutIdx == Skip || + (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) + return 0; + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::SwapPositionsInSEL(TEdge *Edge1, TEdge *Edge2) { + if (!(Edge1->NextInSEL) && !(Edge1->PrevInSEL)) + return; + if (!(Edge2->NextInSEL) && !(Edge2->PrevInSEL)) + return; + + if (Edge1->NextInSEL == Edge2) { + TEdge *Next = Edge2->NextInSEL; + if (Next) + Next->PrevInSEL = Edge1; + TEdge *Prev = Edge1->PrevInSEL; + if (Prev) + Prev->NextInSEL = Edge2; + Edge2->PrevInSEL = Prev; + Edge2->NextInSEL = Edge1; + Edge1->PrevInSEL = Edge2; + Edge1->NextInSEL = Next; + } else if (Edge2->NextInSEL == Edge1) { + TEdge *Next = Edge1->NextInSEL; + if (Next) + Next->PrevInSEL = Edge2; + TEdge *Prev = Edge2->PrevInSEL; + if (Prev) + Prev->NextInSEL = Edge1; + Edge1->PrevInSEL = Prev; + Edge1->NextInSEL = Edge2; + Edge2->PrevInSEL = Edge1; + Edge2->NextInSEL = Next; + } else { + TEdge *Next = Edge1->NextInSEL; + TEdge *Prev = Edge1->PrevInSEL; + Edge1->NextInSEL = Edge2->NextInSEL; + if (Edge1->NextInSEL) + Edge1->NextInSEL->PrevInSEL = Edge1; + Edge1->PrevInSEL = Edge2->PrevInSEL; + if (Edge1->PrevInSEL) + Edge1->PrevInSEL->NextInSEL = Edge1; + Edge2->NextInSEL = Next; + if (Edge2->NextInSEL) + Edge2->NextInSEL->PrevInSEL = Edge2; + Edge2->PrevInSEL = Prev; + if (Edge2->PrevInSEL) + Edge2->PrevInSEL->NextInSEL = Edge2; + } + + if (!Edge1->PrevInSEL) + m_SortedEdges = Edge1; + else if (!Edge2->PrevInSEL) + m_SortedEdges = Edge2; +} +//------------------------------------------------------------------------------ + +TEdge *GetNextInAEL(TEdge *e, Direction dir) { + return dir == dLeftToRight ? e->NextInAEL : e->PrevInAEL; +} +//------------------------------------------------------------------------------ + +void GetHorzDirection(TEdge &HorzEdge, Direction &Dir, cInt &Left, + cInt &Right) { + if (HorzEdge.Bot.X < HorzEdge.Top.X) { + Left = HorzEdge.Bot.X; + Right = HorzEdge.Top.X; + Dir = dLeftToRight; + } else { + Left = HorzEdge.Top.X; + Right = HorzEdge.Bot.X; + Dir = dRightToLeft; + } +} +//------------------------------------------------------------------------ + +/******************************************************************************* +* 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 other HE Bot.Xs only [#] * +* (or they could intersect with Top.Xs only, ie EITHER Bot.Xs OR Top.Xs), * +* and with other non-horizontal edges [*]. Once these intersections are * +* processed, intermediate HEs then 'promote' the Edge above (NextInLML) into * +* the AEL. These 'promoted' edges may in turn intersect [%] with other HEs. * +*******************************************************************************/ + +void Clipper::ProcessHorizontal(TEdge *horzEdge) { + Direction dir; + cInt horzLeft, horzRight; + bool IsOpen = (horzEdge->WindDelta == 0); + + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + TEdge *eLastHorz = horzEdge, *eMaxPair = 0; + while (eLastHorz->NextInLML && IsHorizontal(*eLastHorz->NextInLML)) + eLastHorz = eLastHorz->NextInLML; + if (!eLastHorz->NextInLML) + eMaxPair = GetMaximaPair(eLastHorz); + + MaximaList::const_iterator maxIt; + MaximaList::const_reverse_iterator maxRit; + if (m_Maxima.size() > 0) { + // get the first maxima in range (X) ... + if (dir == dLeftToRight) { + maxIt = m_Maxima.begin(); + while (maxIt != m_Maxima.end() && *maxIt <= horzEdge->Bot.X) + maxIt++; + if (maxIt != m_Maxima.end() && *maxIt >= eLastHorz->Top.X) + maxIt = m_Maxima.end(); + } else { + maxRit = m_Maxima.rbegin(); + while (maxRit != m_Maxima.rend() && *maxRit > horzEdge->Bot.X) + maxRit++; + if (maxRit != m_Maxima.rend() && *maxRit <= eLastHorz->Top.X) + maxRit = m_Maxima.rend(); + } + } + + OutPt *op1 = 0; + + for (;;) // loop through consec. horizontal edges + { + + bool IsLastHorz = (horzEdge == eLastHorz); + TEdge *e = GetNextInAEL(horzEdge, dir); + while (e) { + + // this code block inserts extra coords into horizontal edges (in output + // polygons) whereever maxima touch these horizontal edges. This helps + //'simplifying' polygons (ie if the Simplify property is set). + if (m_Maxima.size() > 0) { + if (dir == dLeftToRight) { + while (maxIt != m_Maxima.end() && *maxIt < e->Curr.X) { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxIt, horzEdge->Bot.Y)); + maxIt++; + } + } else { + while (maxRit != m_Maxima.rend() && *maxRit > e->Curr.X) { + if (horzEdge->OutIdx >= 0 && !IsOpen) + AddOutPt(horzEdge, IntPoint(*maxRit, horzEdge->Bot.Y)); + maxRit++; + } + } + }; + + if ((dir == dLeftToRight && e->Curr.X > horzRight) || + (dir == dRightToLeft && e->Curr.X < horzLeft)) + break; + + // Also break if we've got to the end of an intermediate horizontal edge + // ... + // nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. + if (e->Curr.X == horzEdge->Top.X && horzEdge->NextInLML && + e->Dx < horzEdge->NextInLML->Dx) + break; + + if (horzEdge->OutIdx >= 0 && !IsOpen) // note: may be done multiple times + { +#ifdef use_xyz + if (dir == dLeftToRight) + SetZ(e->Curr, *horzEdge, *e); + else + SetZ(e->Curr, *e, *horzEdge); +#endif + op1 = AddOutPt(horzEdge, e->Curr); + TEdge *eNextHorz = m_SortedEdges; + while (eNextHorz) { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, + eNextHorz->Bot.X, eNextHorz->Top.X)) { + OutPt *op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Bot); + } + + // OK, so far we're still in range of the horizontal Edge but make sure + // we're at the last of consec. horizontals when matching with eMaxPair + if (e == eMaxPair && IsLastHorz) { + if (horzEdge->OutIdx >= 0) + AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge->Top); + DeleteFromAEL(horzEdge); + DeleteFromAEL(eMaxPair); + return; + } + + if (dir == dLeftToRight) { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges(horzEdge, e, Pt); + } else { + IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); + IntersectEdges(e, horzEdge, Pt); + } + TEdge *eNext = GetNextInAEL(e, dir); + SwapPositionsInAEL(horzEdge, e); + e = eNext; + } // end while(e) + + // Break out of loop if HorzEdge.NextInLML is not also horizontal ... + if (!horzEdge->NextInLML || !IsHorizontal(*horzEdge->NextInLML)) + break; + + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->OutIdx >= 0) + AddOutPt(horzEdge, horzEdge->Bot); + GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); + + } // end for (;;) + + if (horzEdge->OutIdx >= 0 && !op1) { + op1 = GetLastOutPt(horzEdge); + TEdge *eNextHorz = m_SortedEdges; + while (eNextHorz) { + if (eNextHorz->OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, + eNextHorz->Bot.X, eNextHorz->Top.X)) { + OutPt *op2 = GetLastOutPt(eNextHorz); + AddJoin(op2, op1, eNextHorz->Top); + } + eNextHorz = eNextHorz->NextInSEL; + } + AddGhostJoin(op1, horzEdge->Top); + } + + if (horzEdge->NextInLML) { + if (horzEdge->OutIdx >= 0) { + op1 = AddOutPt(horzEdge, horzEdge->Top); + UpdateEdgeIntoAEL(horzEdge); + if (horzEdge->WindDelta == 0) + return; + // nb: HorzEdge is no longer horizontal here + TEdge *ePrev = horzEdge->PrevInAEL; + TEdge *eNext = horzEdge->NextInAEL; + if (ePrev && ePrev->Curr.X == horzEdge->Bot.X && + ePrev->Curr.Y == horzEdge->Bot.Y && ePrev->WindDelta != 0 && + (ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(*horzEdge, *ePrev, m_UseFullRange))) { + OutPt *op2 = AddOutPt(ePrev, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } else if (eNext && eNext->Curr.X == horzEdge->Bot.X && + eNext->Curr.Y == horzEdge->Bot.Y && eNext->WindDelta != 0 && + eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(*horzEdge, *eNext, m_UseFullRange)) { + OutPt *op2 = AddOutPt(eNext, horzEdge->Bot); + AddJoin(op1, op2, horzEdge->Top); + } + } else + UpdateEdgeIntoAEL(horzEdge); + } else { + if (horzEdge->OutIdx >= 0) + AddOutPt(horzEdge, horzEdge->Top); + DeleteFromAEL(horzEdge); + } +} +//------------------------------------------------------------------------------ + +bool Clipper::ProcessIntersections(const cInt topY) { + if (!m_ActiveEdges) + return true; + try { + BuildIntersectList(topY); + size_t IlSize = m_IntersectList.size(); + if (IlSize == 0) + return true; + if (IlSize == 1 || FixupIntersectionOrder()) + ProcessIntersectList(); + else + return false; + } catch (...) { + m_SortedEdges = 0; + DisposeIntersectNodes(); + throw clipperException("ProcessIntersections error"); + } + m_SortedEdges = 0; + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeIntersectNodes() { + for (size_t i = 0; i < m_IntersectList.size(); ++i) + delete m_IntersectList[i]; + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +void Clipper::BuildIntersectList(const cInt topY) { + if (!m_ActiveEdges) + return; + + // prepare for sorting ... + TEdge *e = m_ActiveEdges; + m_SortedEdges = e; + while (e) { + e->PrevInSEL = e->PrevInAEL; + e->NextInSEL = e->NextInAEL; + e->Curr.X = TopX(*e, topY); + e = e->NextInAEL; + } + + // bubblesort ... + bool isModified; + do { + isModified = false; + e = m_SortedEdges; + while (e->NextInSEL) { + TEdge *eNext = e->NextInSEL; + IntPoint Pt; + if (e->Curr.X > eNext->Curr.X) { + IntersectPoint(*e, *eNext, Pt); + if (Pt.Y < topY) + Pt = IntPoint(TopX(*e, topY), topY); + IntersectNode *newNode = new IntersectNode; + newNode->Edge1 = e; + newNode->Edge2 = eNext; + newNode->Pt = Pt; + m_IntersectList.push_back(newNode); + + SwapPositionsInSEL(e, eNext); + isModified = true; + } else + e = eNext; + } + if (e->PrevInSEL) + e->PrevInSEL->NextInSEL = 0; + else + break; + } while (isModified); + m_SortedEdges = 0; // important +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessIntersectList() { + for (size_t i = 0; i < m_IntersectList.size(); ++i) { + IntersectNode *iNode = m_IntersectList[i]; + { + IntersectEdges(iNode->Edge1, iNode->Edge2, iNode->Pt); + SwapPositionsInAEL(iNode->Edge1, iNode->Edge2); + } + delete iNode; + } + m_IntersectList.clear(); +} +//------------------------------------------------------------------------------ + +bool IntersectListSort(IntersectNode *node1, IntersectNode *node2) { + return node2->Pt.Y < node1->Pt.Y; +} +//------------------------------------------------------------------------------ + +inline bool EdgesAdjacent(const IntersectNode &inode) { + return (inode.Edge1->NextInSEL == inode.Edge2) || + (inode.Edge1->PrevInSEL == inode.Edge2); +} +//------------------------------------------------------------------------------ + +bool Clipper::FixupIntersectionOrder() { + // pre-condition: intersections are sorted Bottom-most first. + // Now it's crucial that intersections are made only between adjacent edges, + // so to ensure this the order of intersections may need adjusting ... + CopyAELToSEL(); + std::sort(m_IntersectList.begin(), m_IntersectList.end(), IntersectListSort); + size_t cnt = m_IntersectList.size(); + for (size_t i = 0; i < cnt; ++i) { + if (!EdgesAdjacent(*m_IntersectList[i])) { + size_t j = i + 1; + while (j < cnt && !EdgesAdjacent(*m_IntersectList[j])) + j++; + if (j == cnt) + return false; + std::swap(m_IntersectList[i], m_IntersectList[j]); + } + SwapPositionsInSEL(m_IntersectList[i]->Edge1, m_IntersectList[i]->Edge2); + } + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DoMaxima(TEdge *e) { + TEdge *eMaxPair = GetMaximaPairEx(e); + if (!eMaxPair) { + if (e->OutIdx >= 0) + AddOutPt(e, e->Top); + DeleteFromAEL(e); + return; + } + + TEdge *eNext = e->NextInAEL; + while (eNext && eNext != eMaxPair) { + IntersectEdges(e, eNext, e->Top); + SwapPositionsInAEL(e, eNext); + eNext = e->NextInAEL; + } + + if (e->OutIdx == Unassigned && eMaxPair->OutIdx == Unassigned) { + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } else if (e->OutIdx >= 0 && eMaxPair->OutIdx >= 0) { + if (e->OutIdx >= 0) + AddLocalMaxPoly(e, eMaxPair, e->Top); + DeleteFromAEL(e); + DeleteFromAEL(eMaxPair); + } +#ifdef use_lines + else if (e->WindDelta == 0) { + if (e->OutIdx >= 0) { + AddOutPt(e, e->Top); + e->OutIdx = Unassigned; + } + DeleteFromAEL(e); + + if (eMaxPair->OutIdx >= 0) { + AddOutPt(eMaxPair, e->Top); + eMaxPair->OutIdx = Unassigned; + } + DeleteFromAEL(eMaxPair); + } +#endif + else + throw clipperException("DoMaxima error"); +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) { + TEdge *e = m_ActiveEdges; + while (e) { + // 1. process maxima, treating them as if they're 'bent' horizontal edges, + // but exclude maxima with horizontal edges. nb: e can't be a horizontal. + bool IsMaximaEdge = IsMaxima(e, topY); + + if (IsMaximaEdge) { + TEdge *eMaxPair = GetMaximaPairEx(e); + IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair)); + } + + if (IsMaximaEdge) { + if (m_StrictSimple) + m_Maxima.push_back(e->Top.X); + TEdge *ePrev = e->PrevInAEL; + DoMaxima(e); + if (!ePrev) + e = m_ActiveEdges; + else + e = ePrev->NextInAEL; + } else { + // 2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... + if (IsIntermediate(e, topY) && IsHorizontal(*e->NextInLML)) { + UpdateEdgeIntoAEL(e); + if (e->OutIdx >= 0) + AddOutPt(e, e->Bot); + AddEdgeToSEL(e); + } else { + e->Curr.X = TopX(*e, topY); + e->Curr.Y = topY; +#ifdef use_xyz + e->Curr.Z = + topY == e->Top.Y ? e->Top.Z : (topY == e->Bot.Y ? e->Bot.Z : 0); +#endif + } + + // When StrictlySimple and 'e' is being touched by another edge, then + // make sure both edges have a vertex here ... + if (m_StrictSimple) { + TEdge *ePrev = e->PrevInAEL; + if ((e->OutIdx >= 0) && (e->WindDelta != 0) && ePrev && + (ePrev->OutIdx >= 0) && (ePrev->Curr.X == e->Curr.X) && + (ePrev->WindDelta != 0)) { + IntPoint pt = e->Curr; +#ifdef use_xyz + SetZ(pt, *ePrev, *e); +#endif + OutPt *op = AddOutPt(ePrev, pt); + OutPt *op2 = AddOutPt(e, pt); + AddJoin(op, op2, pt); // StrictlySimple (type-3) join + } + } + + e = e->NextInAEL; + } + } + + // 3. Process horizontals at the Top of the scanbeam ... + m_Maxima.sort(); + ProcessHorizontals(); + m_Maxima.clear(); + + // 4. Promote intermediate vertices ... + e = m_ActiveEdges; + while (e) { + if (IsIntermediate(e, topY)) { + OutPt *op = 0; + if (e->OutIdx >= 0) + op = AddOutPt(e, e->Top); + UpdateEdgeIntoAEL(e); + + // if output polygons share an edge, they'll need joining later ... + TEdge *ePrev = e->PrevInAEL; + TEdge *eNext = e->NextInAEL; + if (ePrev && ePrev->Curr.X == e->Bot.X && ePrev->Curr.Y == e->Bot.Y && + op && ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && + SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top, + m_UseFullRange) && + (e->WindDelta != 0) && (ePrev->WindDelta != 0)) { + OutPt *op2 = AddOutPt(ePrev, e->Bot); + AddJoin(op, op2, e->Top); + } else if (eNext && eNext->Curr.X == e->Bot.X && + eNext->Curr.Y == e->Bot.Y && op && eNext->OutIdx >= 0 && + eNext->Curr.Y > eNext->Top.Y && + SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top, + m_UseFullRange) && + (e->WindDelta != 0) && (eNext->WindDelta != 0)) { + OutPt *op2 = AddOutPt(eNext, e->Bot); + AddJoin(op, op2, e->Top); + } + } + e = e->NextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolyline(OutRec &outrec) { + OutPt *pp = outrec.Pts; + OutPt *lastPP = pp->Prev; + while (pp != lastPP) { + pp = pp->Next; + if (pp->Pt == pp->Prev->Pt) { + if (pp == lastPP) + lastPP = pp->Prev; + OutPt *tmpPP = pp->Prev; + tmpPP->Next = pp->Next; + pp->Next->Prev = tmpPP; + delete pp; + pp = tmpPP; + } + } + + if (pp == pp->Prev) { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolygon(OutRec &outrec) { + // FixupOutPolygon() - removes duplicate points and simplifies consecutive + // parallel edges by removing the middle vertex. + OutPt *lastOK = 0; + outrec.BottomPt = 0; + OutPt *pp = outrec.Pts; + bool preserveCol = m_PreserveCollinear || m_StrictSimple; + + for (;;) { + if (pp->Prev == pp || pp->Prev == pp->Next) { + DisposeOutPts(pp); + outrec.Pts = 0; + return; + } + + // test for duplicate points and collinear edges ... + if ((pp->Pt == pp->Next->Pt) || (pp->Pt == pp->Prev->Pt) || + (SlopesEqual(pp->Prev->Pt, pp->Pt, pp->Next->Pt, m_UseFullRange) && + (!preserveCol || + !Pt2IsBetweenPt1AndPt3(pp->Prev->Pt, pp->Pt, pp->Next->Pt)))) { + lastOK = 0; + OutPt *tmp = pp; + pp->Prev->Next = pp->Next; + pp->Next->Prev = pp->Prev; + pp = pp->Prev; + delete tmp; + } else if (pp == lastOK) + break; + else { + if (!lastOK) + lastOK = pp; + pp = pp->Next; + } + } + outrec.Pts = pp; +} +//------------------------------------------------------------------------------ + +int PointCount(OutPt *Pts) { + if (!Pts) + return 0; + int result = 0; + OutPt *p = Pts; + do { + result++; + p = p->Next; + } while (p != Pts); + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult(Paths &polys) { + polys.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + if (!m_PolyOuts[i]->Pts) + continue; + Path pg; + OutPt *p = m_PolyOuts[i]->Pts->Prev; + int cnt = PointCount(p); + if (cnt < 2) + continue; + pg.reserve(cnt); + for (int i = 0; i < cnt; ++i) { + pg.push_back(p->Pt); + p = p->Prev; + } + polys.push_back(pg); + } +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult2(PolyTree &polytree) { + polytree.Clear(); + polytree.AllNodes.reserve(m_PolyOuts.size()); + // add each output polygon/contour to polytree ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { + OutRec *outRec = m_PolyOuts[i]; + int cnt = PointCount(outRec->Pts); + if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3)) + continue; + FixHoleLinkage(*outRec); + PolyNode *pn = new PolyNode(); + // nb: polytree takes ownership of all the PolyNodes + polytree.AllNodes.push_back(pn); + outRec->PolyNd = pn; + pn->Parent = 0; + pn->Index = 0; + pn->Contour.reserve(cnt); + OutPt *op = outRec->Pts->Prev; + for (int j = 0; j < cnt; j++) { + pn->Contour.push_back(op->Pt); + op = op->Prev; + } + } + + // fixup PolyNode links etc ... + polytree.Childs.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->PolyNd) + continue; + if (outRec->IsOpen) { + outRec->PolyNd->m_IsOpen = true; + polytree.AddChild(*outRec->PolyNd); + } else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd) + outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd); + else + polytree.AddChild(*outRec->PolyNd); + } +} +//------------------------------------------------------------------------------ + +void SwapIntersectNodes(IntersectNode &int1, IntersectNode &int2) { + // just swap the contents (because fIntersectNodes is a single-linked-list) + IntersectNode inode = int1; // gets a copy of Int1 + int1.Edge1 = int2.Edge1; + int1.Edge2 = int2.Edge2; + int1.Pt = int2.Pt; + int2.Edge1 = inode.Edge1; + int2.Edge2 = inode.Edge2; + int2.Pt = inode.Pt; +} +//------------------------------------------------------------------------------ + +inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) { + if (e2.Curr.X == e1.Curr.X) { + if (e2.Top.Y > e1.Top.Y) + return e2.Top.X < TopX(e1, e2.Top.Y); + else + return e1.Top.X > TopX(e2, e1.Top.Y); + } else + return e2.Curr.X < e1.Curr.X; +} +//------------------------------------------------------------------------------ + +bool GetOverlap(const cInt a1, const cInt a2, const cInt b1, const cInt b2, + cInt &Left, cInt &Right) { + if (a1 < a2) { + if (b1 < b2) { + Left = std::max(a1, b1); + Right = std::min(a2, b2); + } else { + Left = std::max(a1, b2); + Right = std::min(a2, b1); + } + } else { + if (b1 < b2) { + Left = std::max(a2, b1); + Right = std::min(a1, b2); + } else { + Left = std::max(a2, b2); + Right = std::min(a1, b1); + } + } + return Left < Right; +} +//------------------------------------------------------------------------------ + +inline void UpdateOutPtIdxs(OutRec &outrec) { + OutPt *op = outrec.Pts; + do { + op->Idx = outrec.Idx; + op = op->Prev; + } while (op != outrec.Pts); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge) { + if (!m_ActiveEdges) { + edge->PrevInAEL = 0; + edge->NextInAEL = 0; + m_ActiveEdges = edge; + } else if (!startEdge && E2InsertsBeforeE1(*m_ActiveEdges, *edge)) { + edge->PrevInAEL = 0; + edge->NextInAEL = m_ActiveEdges; + m_ActiveEdges->PrevInAEL = edge; + m_ActiveEdges = edge; + } else { + if (!startEdge) + startEdge = m_ActiveEdges; + while (startEdge->NextInAEL && + !E2InsertsBeforeE1(*startEdge->NextInAEL, *edge)) + startEdge = startEdge->NextInAEL; + edge->NextInAEL = startEdge->NextInAEL; + if (startEdge->NextInAEL) + startEdge->NextInAEL->PrevInAEL = edge; + edge->PrevInAEL = startEdge; + startEdge->NextInAEL = edge; + } +} +//---------------------------------------------------------------------- + +OutPt *DupOutPt(OutPt *outPt, bool InsertAfter) { + OutPt *result = new OutPt; + result->Pt = outPt->Pt; + result->Idx = outPt->Idx; + if (InsertAfter) { + result->Next = outPt->Next; + result->Prev = outPt; + outPt->Next->Prev = result; + outPt->Next = result; + } else { + result->Prev = outPt->Prev; + result->Next = outPt; + outPt->Prev->Next = result; + outPt->Prev = result; + } + return result; +} +//------------------------------------------------------------------------------ + +bool JoinHorz(OutPt *op1, OutPt *op1b, OutPt *op2, OutPt *op2b, + const IntPoint Pt, bool DiscardLeft) { + Direction Dir1 = (op1->Pt.X > op1b->Pt.X ? dRightToLeft : dLeftToRight); + Direction Dir2 = (op2->Pt.X > op2b->Pt.X ? dRightToLeft : dLeftToRight); + if (Dir1 == Dir2) + return false; + + // When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we + // want Op1b to be on the Right. (And likewise with Op2 and Op2b.) + // So, to facilitate this while inserting Op1b and Op2b ... + // when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, + // otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) + if (Dir1 == dLeftToRight) { + while (op1->Next->Pt.X <= Pt.X && op1->Next->Pt.X >= op1->Pt.X && + op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (DiscardLeft && (op1->Pt.X != Pt.X)) + op1 = op1->Next; + op1b = DupOutPt(op1, !DiscardLeft); + if (op1b->Pt != Pt) { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, !DiscardLeft); + } + } else { + while (op1->Next->Pt.X >= Pt.X && op1->Next->Pt.X <= op1->Pt.X && + op1->Next->Pt.Y == Pt.Y) + op1 = op1->Next; + if (!DiscardLeft && (op1->Pt.X != Pt.X)) + op1 = op1->Next; + op1b = DupOutPt(op1, DiscardLeft); + if (op1b->Pt != Pt) { + op1 = op1b; + op1->Pt = Pt; + op1b = DupOutPt(op1, DiscardLeft); + } + } + + if (Dir2 == dLeftToRight) { + while (op2->Next->Pt.X <= Pt.X && op2->Next->Pt.X >= op2->Pt.X && + op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (DiscardLeft && (op2->Pt.X != Pt.X)) + op2 = op2->Next; + op2b = DupOutPt(op2, !DiscardLeft); + if (op2b->Pt != Pt) { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, !DiscardLeft); + }; + } else { + while (op2->Next->Pt.X >= Pt.X && op2->Next->Pt.X <= op2->Pt.X && + op2->Next->Pt.Y == Pt.Y) + op2 = op2->Next; + if (!DiscardLeft && (op2->Pt.X != Pt.X)) + op2 = op2->Next; + op2b = DupOutPt(op2, DiscardLeft); + if (op2b->Pt != Pt) { + op2 = op2b; + op2->Pt = Pt; + op2b = DupOutPt(op2, DiscardLeft); + }; + }; + + if ((Dir1 == dLeftToRight) == DiscardLeft) { + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + } else { + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + } + return true; +} +//------------------------------------------------------------------------------ + +bool Clipper::JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2) { + OutPt *op1 = j->OutPt1, *op1b; + OutPt *op2 = j->OutPt2, *op2b; + + // There are 3 kinds of joins for output polygons ... + // 1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere + // along (horizontal) collinear edges (& Join.OffPt is on the same + // horizontal). + // 2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same + // location at the Bottom of the overlapping segment (& Join.OffPt is above). + // 3. StrictSimple joins where edges touch but are not collinear and where + // Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. + bool isHorizontal = (j->OutPt1->Pt.Y == j->OffPt.Y); + + if (isHorizontal && (j->OffPt == j->OutPt1->Pt) && + (j->OffPt == j->OutPt2->Pt)) { + // Strictly Simple join ... + if (outRec1 != outRec2) + return false; + op1b = j->OutPt1->Next; + while (op1b != op1 && (op1b->Pt == j->OffPt)) + op1b = op1b->Next; + bool reverse1 = (op1b->Pt.Y > j->OffPt.Y); + op2b = j->OutPt2->Next; + while (op2b != op2 && (op2b->Pt == j->OffPt)) + op2b = op2b->Next; + bool reverse2 = (op2b->Pt.Y > j->OffPt.Y); + if (reverse1 == reverse2) + return false; + if (reverse1) { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } else if (isHorizontal) { + // treat horizontal joins differently to non-horizontal joins since with + // them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt + // may be anywhere along the horizontal edge. + op1b = op1; + while (op1->Prev->Pt.Y == op1->Pt.Y && op1->Prev != op1b && + op1->Prev != op2) + op1 = op1->Prev; + while (op1b->Next->Pt.Y == op1b->Pt.Y && op1b->Next != op1 && + op1b->Next != op2) + op1b = op1b->Next; + if (op1b->Next == op1 || op1b->Next == op2) + return false; // a flat 'polygon' + + op2b = op2; + while (op2->Prev->Pt.Y == op2->Pt.Y && op2->Prev != op2b && + op2->Prev != op1b) + op2 = op2->Prev; + while (op2b->Next->Pt.Y == op2b->Pt.Y && op2b->Next != op2 && + op2b->Next != op1) + op2b = op2b->Next; + if (op2b->Next == op2 || op2b->Next == op1) + return false; // a flat 'polygon' + + cInt Left, Right; + // Op1 --> Op1b & Op2 --> Op2b are the extremites of the horizontal edges + if (!GetOverlap(op1->Pt.X, op1b->Pt.X, op2->Pt.X, op2b->Pt.X, Left, Right)) + return false; + + // DiscardLeftSide: when overlapping edges are joined, a spike will created + // which needs to be cleaned up. However, we don't want Op1 or Op2 caught up + // on the discard Side as either may still be needed for other joins ... + IntPoint Pt; + bool DiscardLeftSide; + if (op1->Pt.X >= Left && op1->Pt.X <= Right) { + Pt = op1->Pt; + DiscardLeftSide = (op1->Pt.X > op1b->Pt.X); + } else if (op2->Pt.X >= Left && op2->Pt.X <= Right) { + Pt = op2->Pt; + DiscardLeftSide = (op2->Pt.X > op2b->Pt.X); + } else if (op1b->Pt.X >= Left && op1b->Pt.X <= Right) { + Pt = op1b->Pt; + DiscardLeftSide = op1b->Pt.X > op1->Pt.X; + } else { + Pt = op2b->Pt; + DiscardLeftSide = (op2b->Pt.X > op2->Pt.X); + } + j->OutPt1 = op1; + j->OutPt2 = op2; + return JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); + } else { + // nb: For non-horizontal joins ... + // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y + // 2. Jr.OutPt1.Pt > Jr.OffPt.Y + + // make sure the polygons are correctly oriented ... + op1b = op1->Next; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) + op1b = op1b->Next; + bool Reverse1 = ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse1) { + op1b = op1->Prev; + while ((op1b->Pt == op1->Pt) && (op1b != op1)) + op1b = op1b->Prev; + if ((op1b->Pt.Y > op1->Pt.Y) || + !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)) + return false; + }; + op2b = op2->Next; + while ((op2b->Pt == op2->Pt) && (op2b != op2)) + op2b = op2b->Next; + bool Reverse2 = ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)); + if (Reverse2) { + op2b = op2->Prev; + while ((op2b->Pt == op2->Pt) && (op2b != op2)) + op2b = op2b->Prev; + if ((op2b->Pt.Y > op2->Pt.Y) || + !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)) + return false; + } + + if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || + ((outRec1 == outRec2) && (Reverse1 == Reverse2))) + return false; + + if (Reverse1) { + op1b = DupOutPt(op1, false); + op2b = DupOutPt(op2, true); + op1->Prev = op2; + op2->Next = op1; + op1b->Next = op2b; + op2b->Prev = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } else { + op1b = DupOutPt(op1, true); + op2b = DupOutPt(op2, false); + op1->Next = op2; + op2->Prev = op1; + op1b->Prev = op2b; + op2b->Next = op1b; + j->OutPt1 = op1; + j->OutPt2 = op1b; + return true; + } + } +} +//---------------------------------------------------------------------- + +static OutRec *ParseFirstLeft(OutRec *FirstLeft) { + while (FirstLeft && !FirstLeft->Pts) + FirstLeft = FirstLeft->FirstLeft; + return FirstLeft; +} +//------------------------------------------------------------------------------ + +void Clipper::FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec) { + // tests if NewOutRec contains the polygon before reassigning FirstLeft + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) { + if (Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts)) + outRec->FirstLeft = NewOutRec; + } + } +} +//---------------------------------------------------------------------- + +void Clipper::FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec) { + // A polygon has split into two such that one is now the inner of the other. + // It's possible that these polygons now wrap around other polygons, so check + // every polygon that's also contained by OuterOutRec's FirstLeft container + //(including 0) to see if they've become inner to the new inner polygon ... + OutRec *orfl = OuterOutRec->FirstLeft; + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + + if (!outRec->Pts || outRec == OuterOutRec || outRec == InnerOutRec) + continue; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (firstLeft != orfl && firstLeft != InnerOutRec && + firstLeft != OuterOutRec) + continue; + if (Poly2ContainsPoly1(outRec->Pts, InnerOutRec->Pts)) + outRec->FirstLeft = InnerOutRec; + else if (Poly2ContainsPoly1(outRec->Pts, OuterOutRec->Pts)) + outRec->FirstLeft = OuterOutRec; + else if (outRec->FirstLeft == InnerOutRec || + outRec->FirstLeft == OuterOutRec) + outRec->FirstLeft = orfl; + } +} +//---------------------------------------------------------------------- +void Clipper::FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec) { + // reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { + OutRec *outRec = m_PolyOuts[i]; + OutRec *firstLeft = ParseFirstLeft(outRec->FirstLeft); + if (outRec->Pts && firstLeft == OldOutRec) + outRec->FirstLeft = NewOutRec; + } +} +//---------------------------------------------------------------------- + +void Clipper::JoinCommonEdges() { + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) { + Join *join = m_Joins[i]; + + OutRec *outRec1 = GetOutRec(join->OutPt1->Idx); + OutRec *outRec2 = GetOutRec(join->OutPt2->Idx); + + if (!outRec1->Pts || !outRec2->Pts) + continue; + if (outRec1->IsOpen || outRec2->IsOpen) + continue; + + // get the polygon fragment with the correct hole state (FirstLeft) + // before calling JoinPoints() ... + OutRec *holeStateRec; + if (outRec1 == outRec2) + holeStateRec = outRec1; + else if (OutRec1RightOfOutRec2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (OutRec1RightOfOutRec2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + if (!JoinPoints(join, outRec1, outRec2)) + continue; + + if (outRec1 == outRec2) { + // instead of joining two polygons, we've just created a new one by + // splitting one polygon into two. + outRec1->Pts = join->OutPt1; + outRec1->BottomPt = 0; + outRec2 = CreateOutRec(); + outRec2->Pts = join->OutPt2; + + // update all OutRec2.Pts Idx's ... + UpdateOutPtIdxs(*outRec2); + + if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) { + // outRec1 contains outRec2 ... + outRec2->IsHole = !outRec1->IsHole; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) + FixupFirstLefts2(outRec2, outRec1); + + if ((outRec2->IsHole ^ m_ReverseOutput) == (Area(*outRec2) > 0)) + ReversePolyPtLinks(outRec2->Pts); + + } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) { + // outRec2 contains outRec1 ... + outRec2->IsHole = outRec1->IsHole; + outRec1->IsHole = !outRec2->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + outRec1->FirstLeft = outRec2; + + if (m_UsingPolyTree) + FixupFirstLefts2(outRec1, outRec2); + + if ((outRec1->IsHole ^ m_ReverseOutput) == (Area(*outRec1) > 0)) + ReversePolyPtLinks(outRec1->Pts); + } else { + // the 2 polygons are completely separate ... + outRec2->IsHole = outRec1->IsHole; + outRec2->FirstLeft = outRec1->FirstLeft; + + // fixup FirstLeft pointers that may need reassigning to OutRec2 + if (m_UsingPolyTree) + FixupFirstLefts1(outRec1, outRec2); + } + + } else { + // joined 2 polygons together ... + + outRec2->Pts = 0; + outRec2->BottomPt = 0; + outRec2->Idx = outRec1->Idx; + + outRec1->IsHole = holeStateRec->IsHole; + if (holeStateRec == outRec2) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec2->FirstLeft = outRec1; + + if (m_UsingPolyTree) + FixupFirstLefts3(outRec2, outRec1); + } + } +} + +//------------------------------------------------------------------------------ +// ClipperOffset support functions ... +//------------------------------------------------------------------------------ + +DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) { + if (pt2.X == pt1.X && pt2.Y == pt1.Y) + return DoublePoint(0, 0); + + double Dx = (double)(pt2.X - pt1.X); + double dy = (double)(pt2.Y - pt1.Y); + double f = 1 * 1.0 / std::sqrt(Dx * Dx + dy * dy); + Dx *= f; + dy *= f; + return DoublePoint(dy, -Dx); +} + +//------------------------------------------------------------------------------ +// ClipperOffset class +//------------------------------------------------------------------------------ + +ClipperOffset::ClipperOffset(double miterLimit, double arcTolerance) { + this->MiterLimit = miterLimit; + this->ArcTolerance = arcTolerance; + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +ClipperOffset::~ClipperOffset() { Clear(); } +//------------------------------------------------------------------------------ + +void ClipperOffset::Clear() { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) + delete m_polyNodes.Childs[i]; + m_polyNodes.Childs.clear(); + m_lowest.X = -1; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPath(const Path &path, JoinType joinType, + EndType endType) { + int highI = (int)path.size() - 1; + if (highI < 0) + return; + PolyNode *newNode = new PolyNode(); + newNode->m_jointype = joinType; + newNode->m_endtype = endType; + + // strip duplicate points from path and also get index to the lowest point ... + if (endType == etClosedLine || endType == etClosedPolygon) + while (highI > 0 && path[0] == path[highI]) + highI--; + newNode->Contour.reserve(highI + 1); + newNode->Contour.push_back(path[0]); + int j = 0, k = 0; + for (int i = 1; i <= highI; i++) + if (newNode->Contour[j] != path[i]) { + j++; + newNode->Contour.push_back(path[i]); + if (path[i].Y > newNode->Contour[k].Y || + (path[i].Y == newNode->Contour[k].Y && + path[i].X < newNode->Contour[k].X)) + k = j; + } + if (endType == etClosedPolygon && j < 2) { + delete newNode; + return; + } + m_polyNodes.AddChild(*newNode); + + // if this path's lowest pt is lower than all the others then update m_lowest + if (endType != etClosedPolygon) + return; + if (m_lowest.X < 0) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + else { + IntPoint ip = m_polyNodes.Childs[(int)m_lowest.X]->Contour[(int)m_lowest.Y]; + if (newNode->Contour[k].Y > ip.Y || + (newNode->Contour[k].Y == ip.Y && newNode->Contour[k].X < ip.X)) + m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::AddPaths(const Paths &paths, JoinType joinType, + EndType endType) { + for (Paths::size_type i = 0; i < paths.size(); ++i) + AddPath(paths[i], joinType, endType); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::FixOrientations() { + // fixup orientations of all closed paths if the orientation of the + // closed path with the lowermost vertex is wrong ... + if (m_lowest.X >= 0 && + !Orientation(m_polyNodes.Childs[(int)m_lowest.X]->Contour)) { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon || + (node.m_endtype == etClosedLine && Orientation(node.Contour))) + ReversePath(node.Contour); + } + } else { + for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedLine && !Orientation(node.Contour)) + ReversePath(node.Contour); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(Paths &solution, double delta) { + solution.clear(); + FixOrientations(); + DoOffset(delta); + + // now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } else { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + if (solution.size() > 0) + solution.erase(solution.begin()); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::Execute(PolyTree &solution, double delta) { + solution.Clear(); + FixOrientations(); + DoOffset(delta); + + // now clean up 'corners' ... + Clipper clpr; + clpr.AddPaths(m_destPolys, ptSubject, true); + if (delta > 0) { + clpr.Execute(ctUnion, solution, pftPositive, pftPositive); + } else { + IntRect r = clpr.GetBounds(); + Path outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPath(outer, ptSubject, true); + clpr.ReverseSolution(true); + clpr.Execute(ctUnion, solution, pftNegative, pftNegative); + // remove the outer PolyNode rectangle ... + if (solution.ChildCount() == 1 && solution.Childs[0]->ChildCount() > 0) { + PolyNode *outerNode = solution.Childs[0]; + solution.Childs.reserve(outerNode->ChildCount()); + solution.Childs[0] = outerNode->Childs[0]; + solution.Childs[0]->Parent = outerNode->Parent; + for (int i = 1; i < outerNode->ChildCount(); ++i) + solution.AddChild(*outerNode->Childs[i]); + } else + solution.Clear(); + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoOffset(double delta) { + m_destPolys.clear(); + m_delta = delta; + + // if Zero offset, just copy any CLOSED polygons to m_p and return ... + if (NEAR_ZERO(delta)) { + m_destPolys.reserve(m_polyNodes.ChildCount()); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) { + PolyNode &node = *m_polyNodes.Childs[i]; + if (node.m_endtype == etClosedPolygon) + m_destPolys.push_back(node.Contour); + } + return; + } + + // see offset_triginometry3.svg in the documentation folder ... + if (MiterLimit > 2) + m_miterLim = 2 / (MiterLimit * MiterLimit); + else + m_miterLim = 0.5; + + double y; + if (ArcTolerance <= 0.0) + y = def_arc_tolerance; + else if (ArcTolerance > std::fabs(delta) * def_arc_tolerance) + y = std::fabs(delta) * def_arc_tolerance; + else + y = ArcTolerance; + // see offset_triginometry2.svg in the documentation folder ... + double steps = pi / std::acos(1 - y / std::fabs(delta)); + if (steps > std::fabs(delta) * pi) + steps = std::fabs(delta) * pi; // ie excessive precision check + m_sin = std::sin(two_pi / steps); + m_cos = std::cos(two_pi / steps); + m_StepsPerRad = steps / two_pi; + if (delta < 0.0) + m_sin = -m_sin; + + m_destPolys.reserve(m_polyNodes.ChildCount() * 2); + for (int i = 0; i < m_polyNodes.ChildCount(); i++) { + PolyNode &node = *m_polyNodes.Childs[i]; + m_srcPoly = node.Contour; + + int len = (int)m_srcPoly.size(); + if (len == 0 || + (delta <= 0 && (len < 3 || node.m_endtype != etClosedPolygon))) + continue; + + m_destPoly.clear(); + if (len == 1) { + if (node.m_jointype == jtRound) { + double X = 1.0, Y = 0.0; + for (cInt j = 1; j <= steps; j++) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + double X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + } else { + double X = -1.0, Y = -1.0; + for (int j = 0; j < 4; ++j) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[0].X + X * delta), + Round(m_srcPoly[0].Y + Y * delta))); + if (X < 0) + X = 1; + else if (Y < 0) + Y = 1; + else + X = -1; + } + } + m_destPolys.push_back(m_destPoly); + continue; + } + // build m_normals ... + m_normals.clear(); + m_normals.reserve(len); + for (int j = 0; j < len - 1; ++j) + m_normals.push_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); + if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon) + m_normals.push_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); + else + m_normals.push_back(DoublePoint(m_normals[len - 2])); + + if (node.m_endtype == etClosedPolygon) { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } else if (node.m_endtype == etClosedLine) { + int k = len - 1; + for (int j = 0; j < len; ++j) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + m_destPoly.clear(); + // re-build m_normals ... + DoublePoint n = m_normals[len - 1]; + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-n.X, -n.Y); + k = 0; + for (int j = len - 1; j >= 0; j--) + OffsetPoint(j, k, node.m_jointype); + m_destPolys.push_back(m_destPoly); + } else { + int k = 0; + for (int j = 1; j < len - 1; ++j) + OffsetPoint(j, k, node.m_jointype); + + IntPoint pt1; + if (node.m_endtype == etOpenButt) { + int j = len - 1; + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X + m_normals[j].X * delta), + (cInt)Round(m_srcPoly[j].Y + m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[j].X - m_normals[j].X * delta), + (cInt)Round(m_srcPoly[j].Y - m_normals[j].Y * delta)); + m_destPoly.push_back(pt1); + } else { + int j = len - 1; + k = len - 2; + m_sinA = 0; + m_normals[j] = DoublePoint(-m_normals[j].X, -m_normals[j].Y); + if (node.m_endtype == etOpenSquare) + DoSquare(j, k); + else + DoRound(j, k); + } + + // re-build m_normals ... + for (int j = len - 1; j > 0; j--) + m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); + m_normals[0] = DoublePoint(-m_normals[1].X, -m_normals[1].Y); + + k = len - 1; + for (int j = k - 1; j > 0; --j) + OffsetPoint(j, k, node.m_jointype); + + if (node.m_endtype == etOpenButt) { + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X - m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y - m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + pt1 = IntPoint((cInt)Round(m_srcPoly[0].X + m_normals[0].X * delta), + (cInt)Round(m_srcPoly[0].Y + m_normals[0].Y * delta)); + m_destPoly.push_back(pt1); + } else { + k = 1; + m_sinA = 0; + if (node.m_endtype == etOpenSquare) + DoSquare(0, 1); + else + DoRound(0, 1); + } + m_destPolys.push_back(m_destPoly); + } + } +} +//------------------------------------------------------------------------------ + +void ClipperOffset::OffsetPoint(int j, int &k, JoinType jointype) { + // cross product ... + m_sinA = (m_normals[k].X * m_normals[j].Y - m_normals[j].X * m_normals[k].Y); + if (std::fabs(m_sinA * m_delta) < 1.0) { + // dot product ... + double cosA = + (m_normals[k].X * m_normals[j].X + m_normals[j].Y * m_normals[k].Y); + if (cosA > 0) // angle => 0 degrees + { + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + return; + } + // else angle => 180 degrees + } else if (m_sinA > 1.0) + m_sinA = 1.0; + else if (m_sinA < -1.0) + m_sinA = -1.0; + + if (m_sinA * m_delta < 0) { + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); + m_destPoly.push_back(m_srcPoly[j]); + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); + } else + switch (jointype) { + case jtMiter: { + double r = 1 + (m_normals[j].X * m_normals[k].X + + m_normals[j].Y * m_normals[k].Y); + if (r >= m_miterLim) + DoMiter(j, k, r); + else + DoSquare(j, k); + break; + } + case jtSquare: + DoSquare(j, k); + break; + case jtRound: + DoRound(j, k); + break; + } + k = j; +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoSquare(int j, int k) { + double dx = std::tan(std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + + m_normals[k].Y * m_normals[j].Y) / + 4); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[k].X - m_normals[k].Y * dx)), + Round(m_srcPoly[j].Y + + m_delta * (m_normals[k].Y + m_normals[k].X * dx)))); + m_destPoly.push_back(IntPoint( + Round(m_srcPoly[j].X + m_delta * (m_normals[j].X + m_normals[j].Y * dx)), + Round(m_srcPoly[j].Y + + m_delta * (m_normals[j].Y - m_normals[j].X * dx)))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoMiter(int j, int k, double r) { + double q = m_delta / r; + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + (m_normals[k].X + m_normals[j].X) * q), + Round(m_srcPoly[j].Y + (m_normals[k].Y + m_normals[j].Y) * q))); +} +//------------------------------------------------------------------------------ + +void ClipperOffset::DoRound(int j, int k) { + double a = std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + + m_normals[k].Y * m_normals[j].Y); + int steps = std::max((int)Round(m_StepsPerRad * std::fabs(a)), 1); + + double X = m_normals[k].X, Y = m_normals[k].Y, X2; + for (int i = 0; i < steps; ++i) { + m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + X * m_delta), + Round(m_srcPoly[j].Y + Y * m_delta))); + X2 = X; + X = X * m_cos - m_sin * Y; + Y = X2 * m_sin + Y * m_cos; + } + m_destPoly.push_back( + IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), + Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); +} + +//------------------------------------------------------------------------------ +// Miscellaneous public functions +//------------------------------------------------------------------------------ + +void Clipper::DoSimplePolygons() { + PolyOutList::size_type i = 0; + while (i < m_PolyOuts.size()) { + OutRec *outrec = m_PolyOuts[i++]; + OutPt *op = outrec->Pts; + if (!op || outrec->IsOpen) + continue; + do // for each Pt in Polygon until duplicate found do ... + { + OutPt *op2 = op->Next; + while (op2 != outrec->Pts) { + if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) { + // split the polygon into two ... + OutPt *op3 = op->Prev; + OutPt *op4 = op2->Prev; + op->Prev = op4; + op4->Next = op; + op2->Prev = op3; + op3->Next = op2; + + outrec->Pts = op; + OutRec *outrec2 = CreateOutRec(); + outrec2->Pts = op2; + UpdateOutPtIdxs(*outrec2); + if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) { + // OutRec2 is contained by OutRec1 ... + outrec2->IsHole = !outrec->IsHole; + outrec2->FirstLeft = outrec; + if (m_UsingPolyTree) + FixupFirstLefts2(outrec2, outrec); + } else if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) { + // OutRec1 is contained by OutRec2 ... + outrec2->IsHole = outrec->IsHole; + outrec->IsHole = !outrec2->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + outrec->FirstLeft = outrec2; + if (m_UsingPolyTree) + FixupFirstLefts2(outrec, outrec2); + } else { + // the 2 polygons are separate ... + outrec2->IsHole = outrec->IsHole; + outrec2->FirstLeft = outrec->FirstLeft; + if (m_UsingPolyTree) + FixupFirstLefts1(outrec, outrec2); + } + op2 = op; // ie get ready for the Next iteration + } + op2 = op2->Next; + } + op = op->Next; + } while (op != outrec->Pts); + } +} +//------------------------------------------------------------------------------ + +void ReversePath(Path &p) { std::reverse(p.begin(), p.end()); } +//------------------------------------------------------------------------------ + +void ReversePaths(Paths &p) { + for (Paths::size_type i = 0; i < p.size(); ++i) + ReversePath(p[i]); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, + PolyFillType fillType) { + Clipper c; + c.StrictlySimple(true); + c.AddPath(in_poly, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, + PolyFillType fillType) { + Clipper c; + c.StrictlySimple(true); + c.AddPaths(in_polys, ptSubject, true); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(Paths &polys, PolyFillType fillType) { + SimplifyPolygons(polys, polys, fillType); +} +//------------------------------------------------------------------------------ + +inline double DistanceSqrd(const IntPoint &pt1, const IntPoint &pt2) { + double Dx = ((double)pt1.X - pt2.X); + double dy = ((double)pt1.Y - pt2.Y); + return (Dx * Dx + dy * dy); +} +//------------------------------------------------------------------------------ + +double DistanceFromLineSqrd(const IntPoint &pt, const IntPoint &ln1, + const IntPoint &ln2) { + // The equation of a line in general form (Ax + By + C = 0) + // given 2 points (x�,y�) & (x�,y�) is ... + //(y� - y�)x + (x� - x�)y + (y� - y�)x� - (x� - x�)y� = 0 + // A = (y� - y�); B = (x� - x�); C = (y� - y�)x� - (x� - x�)y� + // perpendicular distance of point (x�,y�) = (Ax� + By� + C)/Sqrt(A� + B�) + // see http://en.wikipedia.org/wiki/Perpendicular_distance + double A = double(ln1.Y - ln2.Y); + double B = double(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); +} +//--------------------------------------------------------------------------- + +bool SlopesNearCollinear(const IntPoint &pt1, const IntPoint &pt2, + const IntPoint &pt3, double distSqrd) { + // this function is more accurate when the point that's geometrically + // between the other 2 points is the one that's tested for distance. + // ie makes it more likely to pick up 'spikes' ... + if (Abs(pt1.X - pt2.X) > Abs(pt1.Y - pt2.Y)) { + if ((pt1.X > pt2.X) == (pt1.X < pt3.X)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.X > pt1.X) == (pt2.X < pt3.X)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } else { + if ((pt1.Y > pt2.Y) == (pt1.Y < pt3.Y)) + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + else if ((pt2.Y > pt1.Y) == (pt2.Y < pt3.Y)) + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + else + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } +} +//------------------------------------------------------------------------------ + +bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) { + double Dx = (double)pt1.X - pt2.X; + double dy = (double)pt1.Y - pt2.Y; + return ((Dx * Dx) + (dy * dy) <= distSqrd); +} +//------------------------------------------------------------------------------ + +OutPt *ExcludeOp(OutPt *op) { + OutPt *result = op->Prev; + result->Next = op->Next; + op->Next->Prev = result; + result->Idx = 0; + return result; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(const Path &in_poly, Path &out_poly, double distance) { + // distance = proximity in units/pixels below which vertices + // will be stripped. Default ~= sqrt(2). + + size_t size = in_poly.size(); + + if (size == 0) { + out_poly.clear(); + return; + } + + OutPt *outPts = new OutPt[size]; + for (size_t i = 0; i < size; ++i) { + outPts[i].Pt = in_poly[i]; + outPts[i].Next = &outPts[(i + 1) % size]; + outPts[i].Next->Prev = &outPts[i]; + outPts[i].Idx = 0; + } + + double distSqrd = distance * distance; + OutPt *op = &outPts[0]; + while (op->Idx == 0 && op->Next != op->Prev) { + if (PointsAreClose(op->Pt, op->Prev->Pt, distSqrd)) { + op = ExcludeOp(op); + size--; + } else if (PointsAreClose(op->Prev->Pt, op->Next->Pt, distSqrd)) { + ExcludeOp(op->Next); + op = ExcludeOp(op); + size -= 2; + } else if (SlopesNearCollinear(op->Prev->Pt, op->Pt, op->Next->Pt, + distSqrd)) { + op = ExcludeOp(op); + size--; + } else { + op->Idx = 1; + op = op->Next; + } + } + + if (size < 3) + size = 0; + out_poly.resize(size); + for (size_t i = 0; i < size; ++i) { + out_poly[i] = op->Pt; + op = op->Next; + } + delete[] outPts; +} +//------------------------------------------------------------------------------ + +void CleanPolygon(Path &poly, double distance) { + CleanPolygon(poly, poly, distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(const Paths &in_polys, Paths &out_polys, double distance) { + out_polys.resize(in_polys.size()); + for (Paths::size_type i = 0; i < in_polys.size(); ++i) + CleanPolygon(in_polys[i], out_polys[i], distance); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(Paths &polys, double distance) { + CleanPolygons(polys, polys, distance); +} +//------------------------------------------------------------------------------ + +void Minkowski(const Path &poly, const Path &path, Paths &solution, bool isSum, + bool isClosed) { + int delta = (isClosed ? 1 : 0); + size_t polyCnt = poly.size(); + size_t pathCnt = path.size(); + Paths pp; + pp.reserve(pathCnt); + if (isSum) + for (size_t i = 0; i < pathCnt; ++i) { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X + poly[j].X, path[i].Y + poly[j].Y)); + pp.push_back(p); + } + else + for (size_t i = 0; i < pathCnt; ++i) { + Path p; + p.reserve(polyCnt); + for (size_t j = 0; j < poly.size(); ++j) + p.push_back(IntPoint(path[i].X - poly[j].X, path[i].Y - poly[j].Y)); + pp.push_back(p); + } + + solution.clear(); + solution.reserve((pathCnt + delta) * (polyCnt + 1)); + for (size_t i = 0; i < pathCnt - 1 + delta; ++i) + for (size_t j = 0; j < polyCnt; ++j) { + Path quad; + quad.reserve(4); + quad.push_back(pp[i % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][j % polyCnt]); + quad.push_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); + quad.push_back(pp[i % pathCnt][(j + 1) % polyCnt]); + if (!Orientation(quad)) + ReversePath(quad); + solution.push_back(quad); + } +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution, + bool pathIsClosed) { + Minkowski(pattern, path, solution, true, pathIsClosed); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void TranslatePath(const Path &input, Path &output, const IntPoint delta) { + // precondition: input != output + output.resize(input.size()); + for (size_t i = 0; i < input.size(); ++i) + output[i] = IntPoint(input[i].X + delta.X, input[i].Y + delta.Y); +} +//------------------------------------------------------------------------------ + +void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution, + bool pathIsClosed) { + Clipper c; + for (size_t i = 0; i < paths.size(); ++i) { + Paths tmp; + Minkowski(pattern, paths[i], tmp, true, pathIsClosed); + c.AddPaths(tmp, ptSubject, true); + if (pathIsClosed) { + Path tmp2; + TranslatePath(paths[i], tmp2, pattern[0]); + c.AddPath(tmp2, ptClip, true); + } + } + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution) { + Minkowski(poly1, poly2, solution, false, true); + Clipper c; + c.AddPaths(solution, ptSubject, true); + c.Execute(ctUnion, solution, pftNonZero, pftNonZero); +} +//------------------------------------------------------------------------------ + +enum NodeType { ntAny, ntOpen, ntClosed }; + +void AddPolyNodeToPaths(const PolyNode &polynode, NodeType nodetype, + Paths &paths) { + bool match = true; + if (nodetype == ntClosed) + match = !polynode.IsOpen(); + else if (nodetype == ntOpen) + return; + + if (!polynode.Contour.empty() && match) + paths.push_back(polynode.Contour); + for (int i = 0; i < polynode.ChildCount(); ++i) + AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); +} +//------------------------------------------------------------------------------ + +void PolyTreeToPaths(const PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntAny, paths); +} +//------------------------------------------------------------------------------ + +void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(polytree, ntClosed, paths); +} +//------------------------------------------------------------------------------ + +void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths) { + paths.resize(0); + paths.reserve(polytree.Total()); + // Open paths are top level only, so ... + for (int i = 0; i < polytree.ChildCount(); ++i) + if (polytree.Childs[i]->IsOpen()) + paths.push_back(polytree.Childs[i]->Contour); +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const IntPoint &p) { + s << "(" << p.X << "," << p.Y << ")"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const Path &p) { + if (p.empty()) + return s; + Path::size_type last = p.size() - 1; + for (Path::size_type i = 0; i < last; i++) + s << "(" << p[i].X << "," << p[i].Y << "), "; + s << "(" << p[last].X << "," << p[last].Y << ")\n"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream &operator<<(std::ostream &s, const Paths &p) { + for (Paths::size_type i = 0; i < p.size(); i++) + s << p[i]; + s << "\n"; + return s; +} +//------------------------------------------------------------------------------ + +} // ClipperLib namespace diff --git a/deploy/lite/clipper.hpp b/deploy/lite/clipper.hpp new file mode 100644 index 00000000..384a6cf4 --- /dev/null +++ b/deploy/lite/clipper.hpp @@ -0,0 +1,423 @@ +/******************************************************************************* +* * +* Author : Angus Johnson * +* Version : 6.4.2 * +* Date : 27 February 2017 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2017 * +* * +* License: * +* Use, modification & distribution is subject to Boost Software License Ver 1. * +* http://www.boost.org/LICENSE_1_0.txt * +* * +* Attributions: * +* The code in this library is an extension of Bala Vatti's clipping algorithm: * +* "A generic solution to polygon clipping" * +* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * +* http://portal.acm.org/citation.cfm?id=129906 * +* * +* Computer graphics and geometric modeling: implementation and algorithms * +* By Max K. Agoston * +* Springer; 1 edition (January 4, 2005) * +* http://books.google.com/books?q=vatti+clipping+agoston * +* * +* See also: * +* "Polygon Offsetting by Computing Winding Numbers" * +* Paper no. DETC2005-85513 pp. 565-575 * +* ASME 2005 International Design Engineering Technical Conferences * +* and Computers and Information in Engineering Conference (IDETC/CIE2005) * +* September 24-28, 2005 , Long Beach, California, USA * +* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * +* * +*******************************************************************************/ + +#ifndef clipper_hpp +#define clipper_hpp + +#define CLIPPER_VERSION "6.4.2" + +// use_int32: When enabled 32bit ints are used instead of 64bit ints. This +// improve performance but coordinate values are limited to the range +/- 46340 +//#define use_int32 + +// use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance. +//#define use_xyz + +// use_lines: Enables line clipping. Adds a very minor cost to performance. +#define use_lines + +// use_deprecated: Enables temporary support for the obsolete functions +//#define use_deprecated + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }; +enum PolyType { ptSubject, ptClip }; +// By far the most widely used winding rules for polygon filling are +// EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) +// Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) +// see http://glprogramming.com/red/chapter11.html +enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }; + +#ifdef use_int32 +typedef int cInt; +static cInt const loRange = 0x7FFF; +static cInt const hiRange = 0x7FFF; +#else +typedef signed long long cInt; +static cInt const loRange = 0x3FFFFFFF; +static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL; +typedef signed long long long64; // used by Int128 class +typedef unsigned long long ulong64; + +#endif + +struct IntPoint { + cInt X; + cInt Y; +#ifdef use_xyz + cInt Z; + IntPoint(cInt x = 0, cInt y = 0, cInt z = 0) : X(x), Y(y), Z(z){}; +#else + IntPoint(cInt x = 0, cInt y = 0) : X(x), Y(y){}; +#endif + + friend inline bool operator==(const IntPoint &a, const IntPoint &b) { + return a.X == b.X && a.Y == b.Y; + } + friend inline bool operator!=(const IntPoint &a, const IntPoint &b) { + return a.X != b.X || a.Y != b.Y; + } +}; +//------------------------------------------------------------------------------ + +typedef std::vector Path; +typedef std::vector Paths; + +inline Path &operator<<(Path &poly, const IntPoint &p) { + poly.push_back(p); + return poly; +} +inline Paths &operator<<(Paths &polys, const Path &p) { + polys.push_back(p); + return polys; +} + +std::ostream &operator<<(std::ostream &s, const IntPoint &p); +std::ostream &operator<<(std::ostream &s, const Path &p); +std::ostream &operator<<(std::ostream &s, const Paths &p); + +struct DoublePoint { + double X; + double Y; + DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {} + DoublePoint(IntPoint ip) : X((double)ip.X), Y((double)ip.Y) {} +}; +//------------------------------------------------------------------------------ + +#ifdef use_xyz +typedef void (*ZFillCallback)(IntPoint &e1bot, IntPoint &e1top, IntPoint &e2bot, + IntPoint &e2top, IntPoint &pt); +#endif + +enum InitOptions { + ioReverseSolution = 1, + ioStrictlySimple = 2, + ioPreserveCollinear = 4 +}; +enum JoinType { jtSquare, jtRound, jtMiter }; +enum EndType { + etClosedPolygon, + etClosedLine, + etOpenButt, + etOpenSquare, + etOpenRound +}; + +class PolyNode; +typedef std::vector PolyNodes; + +class PolyNode { +public: + PolyNode(); + virtual ~PolyNode(){}; + Path Contour; + PolyNodes Childs; + PolyNode *Parent; + PolyNode *GetNext() const; + bool IsHole() const; + bool IsOpen() const; + int ChildCount() const; + +private: + // PolyNode& operator =(PolyNode& other); + unsigned Index; // node index in Parent.Childs + bool m_IsOpen; + JoinType m_jointype; + EndType m_endtype; + PolyNode *GetNextSiblingUp() const; + void AddChild(PolyNode &child); + friend class Clipper; // to access Index + friend class ClipperOffset; +}; + +class PolyTree : public PolyNode { +public: + ~PolyTree() { Clear(); }; + PolyNode *GetFirst() const; + void Clear(); + int Total() const; + +private: + // PolyTree& operator =(PolyTree& other); + PolyNodes AllNodes; + friend class Clipper; // to access AllNodes +}; + +bool Orientation(const Path &poly); +double Area(const Path &poly); +int PointInPolygon(const IntPoint &pt, const Path &path); + +void SimplifyPolygon(const Path &in_poly, Paths &out_polys, + PolyFillType fillType = pftEvenOdd); +void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, + PolyFillType fillType = pftEvenOdd); +void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd); + +void CleanPolygon(const Path &in_poly, Path &out_poly, double distance = 1.415); +void CleanPolygon(Path &poly, double distance = 1.415); +void CleanPolygons(const Paths &in_polys, Paths &out_polys, + double distance = 1.415); +void CleanPolygons(Paths &polys, double distance = 1.415); + +void MinkowskiSum(const Path &pattern, const Path &path, Paths &solution, + bool pathIsClosed); +void MinkowskiSum(const Path &pattern, const Paths &paths, Paths &solution, + bool pathIsClosed); +void MinkowskiDiff(const Path &poly1, const Path &poly2, Paths &solution); + +void PolyTreeToPaths(const PolyTree &polytree, Paths &paths); +void ClosedPathsFromPolyTree(const PolyTree &polytree, Paths &paths); +void OpenPathsFromPolyTree(PolyTree &polytree, Paths &paths); + +void ReversePath(Path &p); +void ReversePaths(Paths &p); + +struct IntRect { + cInt left; + cInt top; + cInt right; + cInt bottom; +}; + +// enums that are used internally ... +enum EdgeSide { esLeft = 1, esRight = 2 }; + +// forward declarations (for stuff used internally) ... +struct TEdge; +struct IntersectNode; +struct LocalMinimum; +struct OutPt; +struct OutRec; +struct Join; + +typedef std::vector PolyOutList; +typedef std::vector EdgeList; +typedef std::vector JoinList; +typedef std::vector IntersectList; + +//------------------------------------------------------------------------------ + +// ClipperBase is the ancestor to the Clipper class. It should not be +// instantiated directly. This class simply abstracts the conversion of sets of +// polygon coordinates into edge objects that are stored in a LocalMinima list. +class ClipperBase { +public: + ClipperBase(); + virtual ~ClipperBase(); + virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed); + bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed); + virtual void Clear(); + IntRect GetBounds(); + bool PreserveCollinear() { return m_PreserveCollinear; }; + void PreserveCollinear(bool value) { m_PreserveCollinear = value; }; + +protected: + void DisposeLocalMinimaList(); + TEdge *AddBoundsToLML(TEdge *e, bool IsClosed); + virtual void Reset(); + TEdge *ProcessBound(TEdge *E, bool IsClockwise); + void InsertScanbeam(const cInt Y); + bool PopScanbeam(cInt &Y); + bool LocalMinimaPending(); + bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin); + OutRec *CreateOutRec(); + void DisposeAllOutRecs(); + void DisposeOutRec(PolyOutList::size_type index); + void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2); + void DeleteFromAEL(TEdge *e); + void UpdateEdgeIntoAEL(TEdge *&e); + + typedef std::vector MinimaList; + MinimaList::iterator m_CurrentLM; + MinimaList m_MinimaList; + + bool m_UseFullRange; + EdgeList m_edges; + bool m_PreserveCollinear; + bool m_HasOpenPaths; + PolyOutList m_PolyOuts; + TEdge *m_ActiveEdges; + + typedef std::priority_queue ScanbeamList; + ScanbeamList m_Scanbeam; +}; +//------------------------------------------------------------------------------ + +class Clipper : public virtual ClipperBase { +public: + Clipper(int initOptions = 0); + bool Execute(ClipType clipType, Paths &solution, + PolyFillType fillType = pftEvenOdd); + bool Execute(ClipType clipType, Paths &solution, PolyFillType subjFillType, + PolyFillType clipFillType); + bool Execute(ClipType clipType, PolyTree &polytree, + PolyFillType fillType = pftEvenOdd); + bool Execute(ClipType clipType, PolyTree &polytree, PolyFillType subjFillType, + PolyFillType clipFillType); + bool ReverseSolution() { return m_ReverseOutput; }; + void ReverseSolution(bool value) { m_ReverseOutput = value; }; + bool StrictlySimple() { return m_StrictSimple; }; + void StrictlySimple(bool value) { m_StrictSimple = value; }; +// set the callback function for z value filling on intersections (otherwise Z +// is 0) +#ifdef use_xyz + void ZFillFunction(ZFillCallback zFillFunc); +#endif +protected: + virtual bool ExecuteInternal(); + +private: + JoinList m_Joins; + JoinList m_GhostJoins; + IntersectList m_IntersectList; + ClipType m_ClipType; + typedef std::list MaximaList; + MaximaList m_Maxima; + TEdge *m_SortedEdges; + bool m_ExecuteLocked; + PolyFillType m_ClipFillType; + PolyFillType m_SubjFillType; + bool m_ReverseOutput; + bool m_UsingPolyTree; + bool m_StrictSimple; +#ifdef use_xyz + ZFillCallback m_ZFill; // custom callback +#endif + void SetWindingCount(TEdge &edge); + bool IsEvenOddFillType(const TEdge &edge) const; + bool IsEvenOddAltFillType(const TEdge &edge) const; + void InsertLocalMinimaIntoAEL(const cInt botY); + void InsertEdgeIntoAEL(TEdge *edge, TEdge *startEdge); + void AddEdgeToSEL(TEdge *edge); + bool PopEdgeFromSEL(TEdge *&edge); + void CopyAELToSEL(); + void DeleteFromSEL(TEdge *e); + void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2); + bool IsContributing(const TEdge &edge) const; + bool IsTopHorz(const cInt XPos); + void DoMaxima(TEdge *e); + void ProcessHorizontals(); + void ProcessHorizontal(TEdge *horzEdge); + void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + OutPt *AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); + OutRec *GetOutRec(int idx); + void AppendPolygon(TEdge *e1, TEdge *e2); + void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); + OutPt *AddOutPt(TEdge *e, const IntPoint &pt); + OutPt *GetLastOutPt(TEdge *e); + bool ProcessIntersections(const cInt topY); + void BuildIntersectList(const cInt topY); + void ProcessIntersectList(); + void ProcessEdgesAtTopOfScanbeam(const cInt topY); + void BuildResult(Paths &polys); + void BuildResult2(PolyTree &polytree); + void SetHoleState(TEdge *e, OutRec *outrec); + void DisposeIntersectNodes(); + bool FixupIntersectionOrder(); + void FixupOutPolygon(OutRec &outrec); + void FixupOutPolyline(OutRec &outrec); + bool IsHole(TEdge *e); + bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl); + void FixHoleLinkage(OutRec &outrec); + void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt); + void ClearJoins(); + void ClearGhostJoins(); + void AddGhostJoin(OutPt *op, const IntPoint offPt); + bool JoinPoints(Join *j, OutRec *outRec1, OutRec *outRec2); + void JoinCommonEdges(); + void DoSimplePolygons(); + void FixupFirstLefts1(OutRec *OldOutRec, OutRec *NewOutRec); + void FixupFirstLefts2(OutRec *InnerOutRec, OutRec *OuterOutRec); + void FixupFirstLefts3(OutRec *OldOutRec, OutRec *NewOutRec); +#ifdef use_xyz + void SetZ(IntPoint &pt, TEdge &e1, TEdge &e2); +#endif +}; +//------------------------------------------------------------------------------ + +class ClipperOffset { +public: + ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25); + ~ClipperOffset(); + void AddPath(const Path &path, JoinType joinType, EndType endType); + void AddPaths(const Paths &paths, JoinType joinType, EndType endType); + void Execute(Paths &solution, double delta); + void Execute(PolyTree &solution, double delta); + void Clear(); + double MiterLimit; + double ArcTolerance; + +private: + Paths m_destPolys; + Path m_srcPoly; + Path m_destPoly; + std::vector m_normals; + double m_delta, m_sinA, m_sin, m_cos; + double m_miterLim, m_StepsPerRad; + IntPoint m_lowest; + PolyNode m_polyNodes; + + void FixOrientations(); + void DoOffset(double delta); + void OffsetPoint(int j, int &k, JoinType jointype); + void DoSquare(int j, int k); + void DoMiter(int j, int k, double r); + void DoRound(int j, int k); +}; +//------------------------------------------------------------------------------ + +class clipperException : public std::exception { +public: + clipperException(const char *description) : m_descr(description) {} + virtual ~clipperException() throw() {} + virtual const char *what() const throw() { return m_descr.c_str(); } + +private: + std::string m_descr; +}; +//------------------------------------------------------------------------------ + +} // ClipperLib namespace + +#endif // clipper_hpp diff --git a/deploy/lite/cls_process.cc b/deploy/lite/cls_process.cc new file mode 100644 index 00000000..9f5c3e94 --- /dev/null +++ b/deploy/lite/cls_process.cc @@ -0,0 +1,43 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "cls_process.h" //NOLINT +#include +#include +#include + +const std::vector rec_image_shape{3, 48, 192}; + +cv::Mat ClsResizeImg(cv::Mat img) { + int imgC, imgH, imgW; + imgC = rec_image_shape[0]; + imgH = rec_image_shape[1]; + imgW = rec_image_shape[2]; + + float ratio = static_cast(img.cols) / static_cast(img.rows); + + int resize_w, resize_h; + if (ceilf(imgH * ratio) > imgW) + resize_w = imgW; + else + resize_w = int(ceilf(imgH * ratio)); + cv::Mat resize_img; + cv::resize(img, resize_img, cv::Size(resize_w, imgH), 0.f, 0.f, + cv::INTER_LINEAR); + if (resize_w < imgW) { + cv::copyMakeBorder(resize_img, resize_img, 0, 0, 0, imgW - resize_w, + cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0)); + } + return resize_img; +} \ No newline at end of file diff --git a/deploy/lite/cls_process.h b/deploy/lite/cls_process.h new file mode 100644 index 00000000..eedeeb9b --- /dev/null +++ b/deploy/lite/cls_process.h @@ -0,0 +1,29 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "math.h" //NOLINT +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" + +cv::Mat ClsResizeImg(cv::Mat img); \ No newline at end of file diff --git a/deploy/lite/config.txt b/deploy/lite/config.txt new file mode 100644 index 00000000..670b2ff0 --- /dev/null +++ b/deploy/lite/config.txt @@ -0,0 +1,5 @@ +max_side_len 960 +det_db_thresh 0.3 +det_db_box_thresh 0.5 +det_db_unclip_ratio 1.6 +use_direction_classify 0 \ No newline at end of file diff --git a/deploy/lite/crnn_process.cc b/deploy/lite/crnn_process.cc new file mode 100644 index 00000000..7528f36f --- /dev/null +++ b/deploy/lite/crnn_process.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "crnn_process.h" //NOLINT +#include +#include +#include + +const std::vector rec_image_shape{3, 32, 320}; + +cv::Mat CrnnResizeImg(cv::Mat img, float wh_ratio) { + int imgC, imgH, imgW; + imgC = rec_image_shape[0]; + imgW = rec_image_shape[2]; + imgH = rec_image_shape[1]; + + imgW = int(32 * wh_ratio); + + float ratio = static_cast(img.cols) / static_cast(img.rows); + int resize_w, resize_h; + if (ceilf(imgH * ratio) > imgW) + resize_w = imgW; + else + resize_w = static_cast(ceilf(imgH * ratio)); + cv::Mat resize_img; + cv::resize(img, resize_img, cv::Size(resize_w, imgH), 0.f, 0.f, + cv::INTER_LINEAR); + + return resize_img; +} + +std::vector ReadDict(std::string path) { + std::ifstream in(path); + std::string filename; + std::string line; + std::vector m_vec; + if (in) { + while (getline(in, line)) { + m_vec.push_back(line); + } + } else { + std::cout << "no such file" << std::endl; + } + return m_vec; +} + +cv::Mat GetRotateCropImage(cv::Mat srcimage, + std::vector> box) { + cv::Mat image; + srcimage.copyTo(image); + std::vector> points = box; + + int x_collect[4] = {box[0][0], box[1][0], box[2][0], box[3][0]}; + int y_collect[4] = {box[0][1], box[1][1], box[2][1], box[3][1]}; + int left = int(*std::min_element(x_collect, x_collect + 4)); + int right = int(*std::max_element(x_collect, x_collect + 4)); + int top = int(*std::min_element(y_collect, y_collect + 4)); + int bottom = int(*std::max_element(y_collect, y_collect + 4)); + + cv::Mat img_crop; + image(cv::Rect(left, top, right - left, bottom - top)).copyTo(img_crop); + + for (int i = 0; i < points.size(); i++) { + points[i][0] -= left; + points[i][1] -= top; + } + + int img_crop_width = + static_cast(sqrt(pow(points[0][0] - points[1][0], 2) + + pow(points[0][1] - points[1][1], 2))); + int img_crop_height = + static_cast(sqrt(pow(points[0][0] - points[3][0], 2) + + pow(points[0][1] - points[3][1], 2))); + + cv::Point2f pts_std[4]; + pts_std[0] = cv::Point2f(0., 0.); + pts_std[1] = cv::Point2f(img_crop_width, 0.); + pts_std[2] = cv::Point2f(img_crop_width, img_crop_height); + pts_std[3] = cv::Point2f(0.f, img_crop_height); + + cv::Point2f pointsf[4]; + pointsf[0] = cv::Point2f(points[0][0], points[0][1]); + pointsf[1] = cv::Point2f(points[1][0], points[1][1]); + pointsf[2] = cv::Point2f(points[2][0], points[2][1]); + pointsf[3] = cv::Point2f(points[3][0], points[3][1]); + + cv::Mat M = cv::getPerspectiveTransform(pointsf, pts_std); + + cv::Mat dst_img; + cv::warpPerspective(img_crop, dst_img, M, + cv::Size(img_crop_width, img_crop_height), + cv::BORDER_REPLICATE); + + const float ratio = 1.5; + if (static_cast(dst_img.rows) >= + static_cast(dst_img.cols) * ratio) { + cv::Mat srcCopy = cv::Mat(dst_img.rows, dst_img.cols, dst_img.depth()); + cv::transpose(dst_img, srcCopy); + cv::flip(srcCopy, srcCopy, 0); + return srcCopy; + } else { + return dst_img; + } +} diff --git a/deploy/lite/crnn_process.h b/deploy/lite/crnn_process.h new file mode 100644 index 00000000..29e67906 --- /dev/null +++ b/deploy/lite/crnn_process.h @@ -0,0 +1,38 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "math.h" //NOLINT +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" + +cv::Mat CrnnResizeImg(cv::Mat img, float wh_ratio); + +std::vector ReadDict(std::string path); + +cv::Mat GetRotateCropImage(cv::Mat srcimage, std::vector> box); + +template +inline size_t Argmax(ForwardIterator first, ForwardIterator last) { + return std::distance(first, std::max_element(first, last)); +} diff --git a/deploy/lite/db_post_process.cc b/deploy/lite/db_post_process.cc new file mode 100644 index 00000000..0c1c8b92 --- /dev/null +++ b/deploy/lite/db_post_process.cc @@ -0,0 +1,301 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "db_post_process.h" // NOLINT +#include +#include + +void GetContourArea(std::vector> box, float unclip_ratio, + float &distance) { + int pts_num = 4; + float area = 0.0f; + float dist = 0.0f; + for (int i = 0; i < pts_num; i++) { + area += box[i][0] * box[(i + 1) % pts_num][1] - + box[i][1] * box[(i + 1) % pts_num][0]; + dist += sqrtf((box[i][0] - box[(i + 1) % pts_num][0]) * + (box[i][0] - box[(i + 1) % pts_num][0]) + + (box[i][1] - box[(i + 1) % pts_num][1]) * + (box[i][1] - box[(i + 1) % pts_num][1])); + } + area = fabs(float(area / 2.0)); + + distance = area * unclip_ratio / dist; +} + +cv::RotatedRect Unclip(std::vector> box, + float unclip_ratio) { + float distance = 1.0; + + GetContourArea(box, unclip_ratio, distance); + + ClipperLib::ClipperOffset offset; + ClipperLib::Path p; + p << ClipperLib::IntPoint(static_cast(box[0][0]), + static_cast(box[0][1])) + << ClipperLib::IntPoint(static_cast(box[1][0]), + static_cast(box[1][1])) + << ClipperLib::IntPoint(static_cast(box[2][0]), + static_cast(box[2][1])) + << ClipperLib::IntPoint(static_cast(box[3][0]), + static_cast(box[3][1])); + offset.AddPath(p, ClipperLib::jtRound, ClipperLib::etClosedPolygon); + + ClipperLib::Paths soln; + offset.Execute(soln, distance); + std::vector points; + + for (int j = 0; j < soln.size(); j++) { + for (int i = 0; i < soln[soln.size() - 1].size(); i++) { + points.emplace_back(soln[j][i].X, soln[j][i].Y); + } + } + cv::RotatedRect res = cv::minAreaRect(points); + + return res; +} + +std::vector> Mat2Vector(cv::Mat mat) { + std::vector> img_vec; + std::vector tmp; + + for (int i = 0; i < mat.rows; ++i) { + tmp.clear(); + for (int j = 0; j < mat.cols; ++j) { + tmp.push_back(mat.at(i, j)); + } + img_vec.push_back(tmp); + } + return img_vec; +} + +bool XsortFp32(std::vector a, std::vector b) { + if (a[0] != b[0]) + return a[0] < b[0]; + return false; +} + +bool XsortInt(std::vector a, std::vector b) { + if (a[0] != b[0]) + return a[0] < b[0]; + return false; +} + +std::vector> +OrderPointsClockwise(std::vector> pts) { + std::vector> box = pts; + std::sort(box.begin(), box.end(), XsortInt); + + std::vector> leftmost = {box[0], box[1]}; + std::vector> rightmost = {box[2], box[3]}; + + if (leftmost[0][1] > leftmost[1][1]) + std::swap(leftmost[0], leftmost[1]); + + if (rightmost[0][1] > rightmost[1][1]) + std::swap(rightmost[0], rightmost[1]); + + std::vector> rect = {leftmost[0], rightmost[0], rightmost[1], + leftmost[1]}; + return rect; +} + +std::vector> GetMiniBoxes(cv::RotatedRect box, float &ssid) { + ssid = std::min(box.size.width, box.size.height); + + cv::Mat points; + cv::boxPoints(box, points); + + auto array = Mat2Vector(points); + std::sort(array.begin(), array.end(), XsortFp32); + + std::vector idx1 = array[0], idx2 = array[1], idx3 = array[2], + idx4 = array[3]; + if (array[3][1] <= array[2][1]) { + idx2 = array[3]; + idx3 = array[2]; + } else { + idx2 = array[2]; + idx3 = array[3]; + } + if (array[1][1] <= array[0][1]) { + idx1 = array[1]; + idx4 = array[0]; + } else { + idx1 = array[0]; + idx4 = array[1]; + } + + array[0] = idx1; + array[1] = idx2; + array[2] = idx3; + array[3] = idx4; + + return array; +} + +float BoxScoreFast(std::vector> box_array, cv::Mat pred) { + auto array = box_array; + int width = pred.cols; + int height = pred.rows; + + float box_x[4] = {array[0][0], array[1][0], array[2][0], array[3][0]}; + float box_y[4] = {array[0][1], array[1][1], array[2][1], array[3][1]}; + + int xmin = clamp( + static_cast(std::floorf(*(std::min_element(box_x, box_x + 4)))), 0, + width - 1); + int xmax = + clamp(static_cast(std::ceilf(*(std::max_element(box_x, box_x + 4)))), + 0, width - 1); + int ymin = clamp( + static_cast(std::floorf(*(std::min_element(box_y, box_y + 4)))), 0, + height - 1); + int ymax = + clamp(static_cast(std::ceilf(*(std::max_element(box_y, box_y + 4)))), + 0, height - 1); + + cv::Mat mask; + mask = cv::Mat::zeros(ymax - ymin + 1, xmax - xmin + 1, CV_8UC1); + + cv::Point root_point[4]; + root_point[0] = cv::Point(static_cast(array[0][0]) - xmin, + static_cast(array[0][1]) - ymin); + root_point[1] = cv::Point(static_cast(array[1][0]) - xmin, + static_cast(array[1][1]) - ymin); + root_point[2] = cv::Point(static_cast(array[2][0]) - xmin, + static_cast(array[2][1]) - ymin); + root_point[3] = cv::Point(static_cast(array[3][0]) - xmin, + static_cast(array[3][1]) - ymin); + const cv::Point *ppt[1] = {root_point}; + int npt[] = {4}; + cv::fillPoly(mask, ppt, npt, 1, cv::Scalar(1)); + + cv::Mat croppedImg; + pred(cv::Rect(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1)) + .copyTo(croppedImg); + + auto score = cv::mean(croppedImg, mask)[0]; + return score; +} + +std::vector>> +BoxesFromBitmap(const cv::Mat pred, const cv::Mat bitmap, + std::map Config) { + const int min_size = 3; + const int max_candidates = 1000; + const float box_thresh = static_cast(Config["det_db_box_thresh"]); + const float unclip_ratio = static_cast(Config["det_db_unclip_ratio"]); + + int width = bitmap.cols; + int height = bitmap.rows; + + std::vector> contours; + std::vector hierarchy; + + cv::findContours(bitmap, contours, hierarchy, cv::RETR_LIST, + cv::CHAIN_APPROX_SIMPLE); + + int num_contours = + contours.size() >= max_candidates ? max_candidates : contours.size(); + + std::vector>> boxes; + + for (int i = 0; i < num_contours; i++) { + float ssid; + if (contours[i].size() <= 2) + continue; + + cv::RotatedRect box = cv::minAreaRect(contours[i]); + auto array = GetMiniBoxes(box, ssid); + + auto box_for_unclip = array; + // end get_mini_box + + if (ssid < min_size) { + continue; + } + + float score; + score = BoxScoreFast(array, pred); + // end box_score_fast + if (score < box_thresh) + continue; + + // start for unclip + cv::RotatedRect points = Unclip(box_for_unclip, unclip_ratio); + if (points.size.height < 1.001 && points.size.width < 1.001) + continue; + // end for unclip + + cv::RotatedRect clipbox = points; + auto cliparray = GetMiniBoxes(clipbox, ssid); + + if (ssid < min_size + 2) + continue; + + int dest_width = pred.cols; + int dest_height = pred.rows; + std::vector> intcliparray; + + for (int num_pt = 0; num_pt < 4; num_pt++) { + std::vector a{ + static_cast(clamp( + roundf(cliparray[num_pt][0] / float(width) * float(dest_width)), + float(0), float(dest_width))), + static_cast(clamp( + roundf(cliparray[num_pt][1] / float(height) * float(dest_height)), + float(0), float(dest_height)))}; + intcliparray.push_back(a); + } + boxes.push_back(intcliparray); + + } // end for + return boxes; +} + +std::vector>> +FilterTagDetRes(std::vector>> boxes, float ratio_h, + float ratio_w, cv::Mat srcimg) { + int oriimg_h = srcimg.rows; + int oriimg_w = srcimg.cols; + + std::vector>> root_points; + for (int n = 0; n < static_cast(boxes.size()); n++) { + boxes[n] = OrderPointsClockwise(boxes[n]); + for (int m = 0; m < static_cast(boxes[0].size()); m++) { + boxes[n][m][0] /= ratio_w; + boxes[n][m][1] /= ratio_h; + + boxes[n][m][0] = + static_cast(std::min(std::max(boxes[n][m][0], 0), oriimg_w - 1)); + boxes[n][m][1] = + static_cast(std::min(std::max(boxes[n][m][1], 0), oriimg_h - 1)); + } + } + + for (int n = 0; n < boxes.size(); n++) { + int rect_width, rect_height; + rect_width = + static_cast(sqrt(pow(boxes[n][0][0] - boxes[n][1][0], 2) + + pow(boxes[n][0][1] - boxes[n][1][1], 2))); + rect_height = + static_cast(sqrt(pow(boxes[n][0][0] - boxes[n][3][0], 2) + + pow(boxes[n][0][1] - boxes[n][3][1], 2))); + if (rect_width <= 4 || rect_height <= 4) + continue; + root_points.push_back(boxes[n]); + } + return root_points; +} diff --git a/deploy/lite/db_post_process.h b/deploy/lite/db_post_process.h new file mode 100644 index 00000000..06dbcb2c --- /dev/null +++ b/deploy/lite/db_post_process.h @@ -0,0 +1,62 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include +#include +#include + +#include "clipper.hpp" +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" + +template T clamp(T x, T min, T max) { + if (x > max) + return max; + if (x < min) + return min; + return x; +} + +std::vector> Mat2Vector(cv::Mat mat); + +void GetContourArea(std::vector> box, float unclip_ratio, + float &distance); + +cv::RotatedRect Unclip(std::vector> box, float unclip_ratio); + +std::vector> Mat2Vector(cv::Mat mat); + +bool XsortFp32(std::vector a, std::vector b); + +bool XsortInt(std::vector a, std::vector b); + +std::vector> +OrderPointsClockwise(std::vector> pts); + +std::vector> GetMiniBoxes(cv::RotatedRect box, float &ssid); + +float BoxScoreFast(std::vector> box_array, cv::Mat pred); + +std::vector>> +BoxesFromBitmap(const cv::Mat pred, const cv::Mat bitmap, + std::map Config); + +std::vector>> +FilterTagDetRes(std::vector>> boxes, float ratio_h, + float ratio_w, cv::Mat srcimg); diff --git a/deploy/lite/ocr_db_crnn.cc b/deploy/lite/ocr_db_crnn.cc new file mode 100644 index 00000000..200d3464 --- /dev/null +++ b/deploy/lite/ocr_db_crnn.cc @@ -0,0 +1,409 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "paddle_api.h" // NOLINT +#include + +#include "cls_process.h" +#include "crnn_process.h" +#include "db_post_process.h" + +using namespace paddle::lite_api; // NOLINT +using namespace std; + +// fill tensor with mean and scale and trans layout: nhwc -> nchw, neon speed up +void NeonMeanScale(const float *din, float *dout, int size, + const std::vector mean, + const std::vector scale) { + if (mean.size() != 3 || scale.size() != 3) { + std::cerr << "[ERROR] mean or scale size must equal to 3\n"; + exit(1); + } + float32x4_t vmean0 = vdupq_n_f32(mean[0]); + float32x4_t vmean1 = vdupq_n_f32(mean[1]); + float32x4_t vmean2 = vdupq_n_f32(mean[2]); + float32x4_t vscale0 = vdupq_n_f32(scale[0]); + float32x4_t vscale1 = vdupq_n_f32(scale[1]); + float32x4_t vscale2 = vdupq_n_f32(scale[2]); + + float *dout_c0 = dout; + float *dout_c1 = dout + size; + float *dout_c2 = dout + size * 2; + + int i = 0; + for (; i < size - 3; i += 4) { + float32x4x3_t vin3 = vld3q_f32(din); + float32x4_t vsub0 = vsubq_f32(vin3.val[0], vmean0); + float32x4_t vsub1 = vsubq_f32(vin3.val[1], vmean1); + float32x4_t vsub2 = vsubq_f32(vin3.val[2], vmean2); + float32x4_t vs0 = vmulq_f32(vsub0, vscale0); + float32x4_t vs1 = vmulq_f32(vsub1, vscale1); + float32x4_t vs2 = vmulq_f32(vsub2, vscale2); + vst1q_f32(dout_c0, vs0); + vst1q_f32(dout_c1, vs1); + vst1q_f32(dout_c2, vs2); + + din += 12; + dout_c0 += 4; + dout_c1 += 4; + dout_c2 += 4; + } + for (; i < size; i++) { + *(dout_c0++) = (*(din++) - mean[0]) * scale[0]; + *(dout_c1++) = (*(din++) - mean[1]) * scale[1]; + *(dout_c2++) = (*(din++) - mean[2]) * scale[2]; + } +} + +// resize image to a size multiple of 32 which is required by the network +cv::Mat DetResizeImg(const cv::Mat img, int max_size_len, + std::vector &ratio_hw) { + int w = img.cols; + int h = img.rows; + + float ratio = 1.f; + int max_wh = w >= h ? w : h; + if (max_wh > max_size_len) { + if (h > w) { + ratio = static_cast(max_size_len) / static_cast(h); + } else { + ratio = static_cast(max_size_len) / static_cast(w); + } + } + + int resize_h = static_cast(float(h) * ratio); + int resize_w = static_cast(float(w) * ratio); + if (resize_h % 32 == 0) + resize_h = resize_h; + else if (resize_h / 32 < 1 + 1e-5) + resize_h = 32; + else + resize_h = (resize_h / 32 - 1) * 32; + + if (resize_w % 32 == 0) + resize_w = resize_w; + else if (resize_w / 32 < 1 + 1e-5) + resize_w = 32; + else + resize_w = (resize_w / 32 - 1) * 32; + + cv::Mat resize_img; + cv::resize(img, resize_img, cv::Size(resize_w, resize_h)); + + ratio_hw.push_back(static_cast(resize_h) / static_cast(h)); + ratio_hw.push_back(static_cast(resize_w) / static_cast(w)); + return resize_img; +} + +cv::Mat RunClsModel(cv::Mat img, std::shared_ptr predictor_cls, + const float thresh = 0.9) { + std::vector mean = {0.5f, 0.5f, 0.5f}; + std::vector scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + + cv::Mat srcimg; + img.copyTo(srcimg); + cv::Mat crop_img; + img.copyTo(crop_img); + cv::Mat resize_img; + + int index = 0; + float wh_ratio = + static_cast(crop_img.cols) / static_cast(crop_img.rows); + + resize_img = ClsResizeImg(crop_img); + resize_img.convertTo(resize_img, CV_32FC3, 1 / 255.f); + + const float *dimg = reinterpret_cast(resize_img.data); + + std::unique_ptr input_tensor0(std::move(predictor_cls->GetInput(0))); + input_tensor0->Resize({1, 3, resize_img.rows, resize_img.cols}); + auto *data0 = input_tensor0->mutable_data(); + + NeonMeanScale(dimg, data0, resize_img.rows * resize_img.cols, mean, scale); + // Run CLS predictor + predictor_cls->Run(); + + // Get output and run postprocess + std::unique_ptr softmax_out( + std::move(predictor_cls->GetOutput(0))); + auto *softmax_scores = softmax_out->mutable_data(); + auto softmax_out_shape = softmax_out->shape(); + float score = 0; + int label = 0; + for (int i = 0; i < softmax_out_shape[1]; i++) { + if (softmax_scores[i] > score) { + score = softmax_scores[i]; + label = i; + } + } + if (label % 2 == 1 && score > thresh) { + cv::rotate(srcimg, srcimg, 1); + } + return srcimg; +} + +void RunRecModel(std::vector>> boxes, cv::Mat img, + std::shared_ptr predictor_crnn, + std::vector &rec_text, + std::vector &rec_text_score, + std::vector charactor_dict, + std::shared_ptr predictor_cls, + int use_direction_classify) { + std::vector mean = {0.5f, 0.5f, 0.5f}; + std::vector scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f}; + + cv::Mat srcimg; + img.copyTo(srcimg); + cv::Mat crop_img; + cv::Mat resize_img; + + int index = 0; + for (int i = boxes.size() - 1; i >= 0; i--) { + crop_img = GetRotateCropImage(srcimg, boxes[i]); + if (use_direction_classify >= 1) { + crop_img = RunClsModel(crop_img, predictor_cls); + } + float wh_ratio = + static_cast(crop_img.cols) / static_cast(crop_img.rows); + + resize_img = CrnnResizeImg(crop_img, wh_ratio); + resize_img.convertTo(resize_img, CV_32FC3, 1 / 255.f); + + const float *dimg = reinterpret_cast(resize_img.data); + + std::unique_ptr input_tensor0( + std::move(predictor_crnn->GetInput(0))); + input_tensor0->Resize({1, 3, resize_img.rows, resize_img.cols}); + auto *data0 = input_tensor0->mutable_data(); + + NeonMeanScale(dimg, data0, resize_img.rows * resize_img.cols, mean, scale); + //// Run CRNN predictor + predictor_crnn->Run(); + + // Get output and run postprocess + std::unique_ptr output_tensor0( + std::move(predictor_crnn->GetOutput(0))); + auto *predict_batch = output_tensor0->data(); + auto predict_shape = output_tensor0->shape(); + + // ctc decode + std::string str_res; + int argmax_idx; + int last_index = 0; + float score = 0.f; + int count = 0; + float max_value = 0.0f; + + for (int n = 0; n < predict_shape[1]; n++) { + argmax_idx = int(Argmax(&predict_batch[n * predict_shape[2]], + &predict_batch[(n + 1) * predict_shape[2]])); + max_value = + float(*std::max_element(&predict_batch[n * predict_shape[2]], + &predict_batch[(n + 1) * predict_shape[2]])); + if (argmax_idx > 0 && (!(i > 0 && argmax_idx == last_index))) { + score += max_value; + count += 1; + str_res += charactor_dict[argmax_idx]; + } + last_index = argmax_idx; + } + score /= count; + rec_text.push_back(str_res); + rec_text_score.push_back(score); + } +} + +std::vector>> +RunDetModel(std::shared_ptr predictor, cv::Mat img, + std::map Config) { + // Read img + int max_side_len = int(Config["max_side_len"]); + + cv::Mat srcimg; + img.copyTo(srcimg); + + std::vector ratio_hw; + img = DetResizeImg(img, max_side_len, ratio_hw); + cv::Mat img_fp; + img.convertTo(img_fp, CV_32FC3, 1.0 / 255.f); + + // Prepare input data from image + std::unique_ptr input_tensor0(std::move(predictor->GetInput(0))); + input_tensor0->Resize({1, 3, img_fp.rows, img_fp.cols}); + auto *data0 = input_tensor0->mutable_data(); + + std::vector mean = {0.485f, 0.456f, 0.406f}; + std::vector scale = {1 / 0.229f, 1 / 0.224f, 1 / 0.225f}; + const float *dimg = reinterpret_cast(img_fp.data); + NeonMeanScale(dimg, data0, img_fp.rows * img_fp.cols, mean, scale); + + // Run predictor + predictor->Run(); + + // Get output and post process + std::unique_ptr output_tensor( + std::move(predictor->GetOutput(0))); + auto *outptr = output_tensor->data(); + auto shape_out = output_tensor->shape(); + + // Save output + float pred[shape_out[2] * shape_out[3]]; + unsigned char cbuf[shape_out[2] * shape_out[3]]; + + for (int i = 0; i < int(shape_out[2] * shape_out[3]); i++) { + pred[i] = static_cast(outptr[i]); + cbuf[i] = static_cast((outptr[i]) * 255); + } + + cv::Mat cbuf_map(shape_out[2], shape_out[3], CV_8UC1, + reinterpret_cast(cbuf)); + cv::Mat pred_map(shape_out[2], shape_out[3], CV_32F, + reinterpret_cast(pred)); + + const double threshold = double(Config["det_db_thresh"]) * 255; + const double maxvalue = 255; + cv::Mat bit_map; + cv::threshold(cbuf_map, bit_map, threshold, maxvalue, cv::THRESH_BINARY); + cv::Mat dilation_map; + cv::Mat dila_ele = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2)); + cv::dilate(bit_map, dilation_map, dila_ele); + auto boxes = BoxesFromBitmap(pred_map, dilation_map, Config); + + std::vector>> filter_boxes = + FilterTagDetRes(boxes, ratio_hw[0], ratio_hw[1], srcimg); + + return filter_boxes; +} + +std::shared_ptr loadModel(std::string model_file) { + MobileConfig config; + config.set_model_from_file(model_file); + + std::shared_ptr predictor = + CreatePaddlePredictor(config); + return predictor; +} + +cv::Mat Visualization(cv::Mat srcimg, + std::vector>> boxes) { + cv::Point rook_points[boxes.size()][4]; + for (int n = 0; n < boxes.size(); n++) { + for (int m = 0; m < boxes[0].size(); m++) { + rook_points[n][m] = cv::Point(static_cast(boxes[n][m][0]), + static_cast(boxes[n][m][1])); + } + } + cv::Mat img_vis; + srcimg.copyTo(img_vis); + for (int n = 0; n < boxes.size(); n++) { + const cv::Point *ppt[1] = {rook_points[n]}; + int npt[] = {4}; + cv::polylines(img_vis, ppt, npt, 1, 1, CV_RGB(0, 255, 0), 2, 8, 0); + } + + cv::imwrite("./vis.jpg", img_vis); + std::cout << "The detection visualized image saved in ./vis.jpg" << std::endl; + return img_vis; +} + +std::vector split(const std::string &str, + const std::string &delim) { + std::vector res; + if ("" == str) + return res; + char *strs = new char[str.length() + 1]; + std::strcpy(strs, str.c_str()); + + char *d = new char[delim.length() + 1]; + std::strcpy(d, delim.c_str()); + + char *p = std::strtok(strs, d); + while (p) { + string s = p; + res.push_back(s); + p = std::strtok(NULL, d); + } + + return res; +} + +std::map LoadConfigTxt(std::string config_path) { + auto config = ReadDict(config_path); + + std::map dict; + for (int i = 0; i < config.size(); i++) { + std::vector res = split(config[i], " "); + dict[res[0]] = stod(res[1]); + } + return dict; +} + +int main(int argc, char **argv) { + if (argc < 5) { + std::cerr << "[ERROR] usage: " << argv[0] + << " det_model_file cls_model_file rec_model_file image_path " + "charactor_dict\n"; + exit(1); + } + std::string det_model_file = argv[1]; + std::string rec_model_file = argv[2]; + std::string cls_model_file = argv[3]; + std::string img_path = argv[4]; + std::string dict_path = argv[5]; + + //// load config from txt file + auto Config = LoadConfigTxt("./config.txt"); + int use_direction_classify = int(Config["use_direction_classify"]); + + auto start = std::chrono::system_clock::now(); + + auto det_predictor = loadModel(det_model_file); + auto rec_predictor = loadModel(rec_model_file); + auto cls_predictor = loadModel(cls_model_file); + + auto charactor_dict = ReadDict(dict_path); + charactor_dict.insert(charactor_dict.begin(), "#"); // blank char for ctc + charactor_dict.push_back(" "); +std: + cout << charactor_dict[0] << " " << charactor_dict[1] << std::endl; + cv::Mat srcimg = cv::imread(img_path, cv::IMREAD_COLOR); + auto boxes = RunDetModel(det_predictor, srcimg, Config); + + std::vector rec_text; + std::vector rec_text_score; + + RunRecModel(boxes, srcimg, rec_predictor, rec_text, rec_text_score, + charactor_dict, cls_predictor, use_direction_classify); + + auto end = std::chrono::system_clock::now(); + auto duration = + std::chrono::duration_cast(end - start); + + //// visualization + auto img_vis = Visualization(srcimg, boxes); + + //// print recognized text + for (int i = 0; i < rec_text.size(); i++) { + std::cout << i << "\t" << rec_text[i] << "\t" << rec_text_score[i] + << std::endl; + } + + std::cout << "花费了" + << double(duration.count()) * + std::chrono::microseconds::period::num / + std::chrono::microseconds::period::den + << "秒" << std::endl; + + return 0; +} \ No newline at end of file diff --git a/deploy/lite/prepare.sh b/deploy/lite/prepare.sh new file mode 100644 index 00000000..daaa30c4 --- /dev/null +++ b/deploy/lite/prepare.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +mkdir -p $1/demo/cxx/ocr/debug/ +cp ../../ppocr/utils/ppocr_keys_v1.txt $1/demo/cxx/ocr/debug/ +cp -r ./* $1/demo/cxx/ocr/ +cp ./config.txt $1/demo/cxx/ocr/debug/ +cp ../../doc/imgs/11.jpg $1/demo/cxx/ocr/debug/ + +echo "Prepare Done" diff --git a/deploy/lite/readme.md b/deploy/lite/readme.md new file mode 100644 index 00000000..4775c19c --- /dev/null +++ b/deploy/lite/readme.md @@ -0,0 +1,269 @@ +# 端侧部署 + +本教程将介绍基于[Paddle Lite](https://github.com/PaddlePaddle/Paddle-Lite) 在移动端部署PaddleOCR超轻量中文检测、识别模型的详细步骤。 + +Paddle Lite是飞桨轻量化推理引擎,为手机、IOT端提供高效推理能力,并广泛整合跨平台硬件,为端侧部署及应用落地问题提供轻量化的部署方案。 + + +## 1. 准备环境 + +### 运行准备 +- 电脑(编译Paddle Lite) +- 安卓手机(armv7或armv8) + +***注意: PaddleOCR 移动端部署当前不支持动态图模型,只支持静态图保存的模型。当前PaddleOCR静态图的分支是`develop`。*** + +### 1.1 准备交叉编译环境 +交叉编译环境用于编译 Paddle Lite 和 PaddleOCR 的C++ demo。 +支持多种开发环境,不同开发环境的编译流程请参考对应文档。 + +1. [Docker](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#docker) +2. [Linux](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#linux) +3. [MAC OS](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#mac-os) + +### 1.2 准备预测库 + +预测库有两种获取方式: +- 1. 直接下载,预测库下载链接如下: + + | 平台 | 预测库下载链接 | + |---|---| + |Android|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv7.gcc.c++_shared.with_extra.with_cv.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv8.gcc.c++_shared.with_extra.with_cv.tar.gz)| + |IOS|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv7.with_cv.with_extra.with_log.tiny_publish.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv8.with_cv.with_extra.with_log.tiny_publish.tar.gz)| + + 注:1. 上述预测库为PaddleLite 2.8分支编译得到,有关PaddleLite 2.8 详细信息可参考[链接](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8)。 + +- 2. [推荐]编译Paddle-Lite得到预测库,Paddle-Lite的编译方式如下: +``` +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +# 切换到Paddle-Lite release/v2.8 稳定分支 +git checkout release/v2.8 +./lite/tools/build_android.sh --arch=armv8 --with_cv=ON --with_extra=ON +``` + +注意:编译Paddle-Lite获得预测库时,需要打开`--with_cv=ON --with_extra=ON`两个选项,`--arch`表示`arm`版本,这里指定为armv8, +更多编译命令 +介绍请参考[链接](https://paddle-lite.readthedocs.io/zh/latest/user_guides/Compile/Android.html#id2)。 + +直接下载预测库并解压后,可以得到`inference_lite_lib.android.armv8/`文件夹,通过编译Paddle-Lite得到的预测库位于 +`Paddle-Lite/build.lite.android.armv8.gcc/inference_lite_lib.android.armv8/`文件夹下。 +预测库的文件目录如下: +``` +inference_lite_lib.android.armv8/ +|-- cxx C++ 预测库和头文件 +| |-- include C++ 头文件 +| | |-- paddle_api.h +| | |-- paddle_image_preprocess.h +| | |-- paddle_lite_factory_helper.h +| | |-- paddle_place.h +| | |-- paddle_use_kernels.h +| | |-- paddle_use_ops.h +| | `-- paddle_use_passes.h +| `-- lib C++预测库 +| |-- libpaddle_api_light_bundled.a C++静态库 +| `-- libpaddle_light_api_shared.so C++动态库 +|-- java Java预测库 +| |-- jar +| | `-- PaddlePredictor.jar +| |-- so +| | `-- libpaddle_lite_jni.so +| `-- src +|-- demo C++和Java示例代码 +| |-- cxx C++ 预测库demo +| `-- java Java 预测库demo +``` + +## 2 开始运行 + +### 2.1 模型优化 + +Paddle-Lite 提供了多种策略来自动优化原始的模型,其中包括量化、子图融合、混合调度、Kernel优选等方法,使用Paddle-lite的opt工具可以自动 +对inference模型进行优化,优化后的模型更轻量,模型运行速度更快。 + +如果已经准备好了 `.nb` 结尾的模型文件,可以跳过此步骤。 + +下述表格中也提供了一系列中文移动端模型: + +|模型版本|模型简介|模型大小|检测模型|文本方向分类模型|识别模型|Paddle-Lite版本| +|---|---|---|---|---|---|---| +|V2.0|超轻量中文OCR 移动端模型|8.1M|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_opt.nb)|v2.8| +|V2.0(slim)|超轻量中文OCR 移动端模型|3.5M|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_prune_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_quant_opt.nb)|[下载地址](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_quant_opt.nb)|v2.8| + +注意:V2.0 3.0M 轻量模型是使用PaddleSlim优化后的,需要配合Paddle-Lite最新预测库使用。 + +如果直接使用上述表格中的模型进行部署,可略过下述步骤,直接阅读 [2.2节](#2.2与手机联调)。 + +如果要部署的模型不在上述表格中,则需要按照如下步骤获得优化后的模型。 + +模型优化需要Paddle-Lite的opt可执行文件,可以通过编译Paddle-Lite源码获得,编译步骤如下: +``` +# 如果准备环境时已经clone了Paddle-Lite,则不用重新clone Paddle-Lite +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +git checkout release/v2.8 +# 启动编译 +./lite/tools/build.sh build_optimize_tool +``` + +编译完成后,opt文件位于`build.opt/lite/api/`下,可通过如下方式查看opt的运行选项和使用方式; +``` +cd build.opt/lite/api/ +./opt +``` + +|选项|说明| +|---|---| +|--model_dir|待优化的PaddlePaddle模型(非combined形式)的路径| +|--model_file|待优化的PaddlePaddle模型(combined形式)的网络结构文件路径| +|--param_file|待优化的PaddlePaddle模型(combined形式)的权重文件路径| +|--optimize_out_type|输出模型类型,目前支持两种类型:protobuf和naive_buffer,其中naive_buffer是一种更轻量级的序列化/反序列化实现。若您需要在mobile端执行模型预测,请将此选项设置为naive_buffer。默认为protobuf| +|--optimize_out|优化模型的输出路径| +|--valid_targets|指定模型可执行的backend,默认为arm。目前可支持x86、arm、opencl、npu、xpu,可以同时指定多个backend(以空格分隔),Model Optimize Tool将会自动选择最佳方式。如果需要支持华为NPU(Kirin 810/990 Soc搭载的达芬奇架构NPU),应当设置为npu, arm| +|--record_tailoring_info|当使用 根据模型裁剪库文件 功能时,则设置该选项为true,以记录优化后模型含有的kernel和OP信息,默认为false| + +`--model_dir`适用于待优化的模型是非combined方式,PaddleOCR的inference模型是combined方式,即模型结构和模型参数使用单独一个文件存储。 + +下面以PaddleOCR的超轻量中文模型为例,介绍使用编译好的opt文件完成inference模型到Paddle-Lite优化模型的转换。 + +``` +# 【推荐】 下载PaddleOCR V2.0版本的中英文 inference模型 +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_det_slim_infer.tar && tar xf ch_ppocr_mobile_v1.1_det_prune_infer.tar +wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_slim_infer.tar && tar xf ch_ppocr_mobile_v1.1_rec_quant_infer.tar +# 转换V2.0检测模型 +./opt --model_file=./ch_ppocr_mobile_v1.1_det_prune_infer/model --param_file=./ch_ppocr_mobile_v1.1_det_prune_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_det_prune_opt --valid_targets=arm --optimize_out_type=naive_buffer +# 转换V2.0识别模型 +./opt --model_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/model --param_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_rec_quant_opt --valid_targets=arm --optimize_out_type=naive_buffer +``` + +转换成功后,当前目录下会多出`.nb`结尾的文件,即是转换成功的模型文件。 + +注意:使用paddle-lite部署时,需要使用opt工具优化后的模型。 opt 工具的输入模型是paddle保存的inference模型 + + +### 2.2 与手机联调 + +首先需要进行一些准备工作。 + 1. 准备一台arm8的安卓手机,如果编译的预测库和opt文件是armv7,则需要arm7的手机,并修改Makefile中`ARM_ABI = arm7`。 + 2. 打开手机的USB调试选项,选择文件传输模式,连接电脑。 + 3. 电脑上安装adb工具,用于调试。 adb安装方式如下: + + 3.1. MAC电脑安装ADB: + ``` + brew cask install android-platform-tools + ``` + 3.2. Linux安装ADB + ``` + sudo apt update + sudo apt install -y wget adb + ``` + 3.3. Window安装ADB + + win上安装需要去谷歌的安卓平台下载adb软件包进行安装:[链接](https://developer.android.com/studio) + + 打开终端,手机连接电脑,在终端中输入 + ``` + adb devices + ``` + 如果有device输出,则表示安装成功。 + ``` + List of devices attached + 744be294 device + ``` + + 4. 准备优化后的模型、预测库文件、测试图像和使用的字典文件。 + ``` + git clone https://github.com/PaddlePaddle/PaddleOCR.git + cd PaddleOCR/deploy/lite/ + # 运行prepare.sh,准备预测库文件、测试图像和使用的字典文件,并放置在预测库中的demo/cxx/ocr文件夹下 + sh prepare.sh /{lite prediction library path}/inference_lite_lib.android.armv8 + + # 进入OCR demo的工作目录 + cd /{lite prediction library path}/inference_lite_lib.android.armv8/ + cd demo/cxx/ocr/ + # 将C++预测动态库so文件复制到debug文件夹中 + cp ../../../cxx/lib/libpaddle_light_api_shared.so ./debug/ + ``` + + 准备测试图像,以`PaddleOCR/doc/imgs/11.jpg`为例,将测试的图像复制到`demo/cxx/ocr/debug/`文件夹下。 + 准备lite opt工具优化后的模型文件,比如使用`ch_ppocr_mobile_v1.1_det_prune_opt.nb,ch_ppocr_mobile_v1.1_rec_quant_opt.nb, ch_ppocr_mobile_cls_quant_opt.nb`,模型文件放置在`demo/cxx/ocr/debug/`文件夹下。 + + 执行完成后,ocr文件夹下将有如下文件格式: + +``` +demo/cxx/ocr/ +|-- debug/ +| |--ch_ppocr_mobile_v1.1_det_prune_opt.nb 优化后的检测模型文件 +| |--ch_ppocr_mobile_v1.1_rec_quant_opt.nb 优化后的识别模型文件 +| |--ch_ppocr_mobile_cls_quant_opt.nb 优化后的文字方向分类器模型文件 +| |--11.jpg 待测试图像 +| |--ppocr_keys_v1.txt 中文字典文件 +| |--libpaddle_light_api_shared.so C++预测库文件 +| |--config.txt DB-CRNN超参数配置 +|-- config.txt DB-CRNN超参数配置 +|-- crnn_process.cc 识别模型CRNN的预处理和后处理文件 +|-- crnn_process.h +|-- db_post_process.cc 检测模型DB的后处理文件 +|-- db_post_process.h +|-- Makefile 编译文件 +|-- ocr_db_crnn.cc C++预测源文件 +``` + +#### 注意: +1. ppocr_keys_v1.txt是中文字典文件,如果使用的 nb 模型是英文数字或其他语言的模型,需要更换为对应语言的字典。 +PaddleOCR 在ppocr/utils/下存放了多种字典,包括: +``` +dict/french_dict.txt # 法语字典 +dict/german_dict.txt # 德语字典 +ic15_dict.txt # 英文字典 +dict/japan_dict.txt # 日语字典 +dict/korean_dict.txt # 韩语字典 +ppocr_keys_v1.txt # 中文字典 +``` + +2. `config.txt` 包含了检测器、分类器的超参数,如下: +``` +max_side_len 960 # 输入图像长宽大于960时,等比例缩放图像,使得图像最长边为960 +det_db_thresh 0.3 # 用于过滤DB预测的二值化图像,设置为0.-0.3对结果影响不明显 +det_db_box_thresh 0.5 # DB后处理过滤box的阈值,如果检测存在漏框情况,可酌情减小 +det_db_unclip_ratio 1.6 # 表示文本框的紧致程度,越小则文本框更靠近文本 +use_direction_classify 0 # 是否使用方向分类器,0表示不使用,1表示使用 +``` + + 5. 启动调试 + + 上述步骤完成后就可以使用adb将文件push到手机上运行,步骤如下: + + ``` + # 执行编译,得到可执行文件ocr_db_crnn + # ocr_db_crnn可执行文件的使用方式为: + # ./ocr_db_crnn 检测模型文件 方向分类器模型文件 识别模型文件 测试图像路径 字典文件路径 + make -j + # 将编译的可执行文件移动到debug文件夹中 + mv ocr_db_crnn ./debug/ + # 将debug文件夹push到手机上 + adb push debug /data/local/tmp/ + adb shell + cd /data/local/tmp/debug + export LD_LIBRARY_PATH=${PWD}:$LD_LIBRARY_PATH + ./ocr_db_crnn ch_ppocr_mobile_v1.1_det_prune_opt.nb ch_ppocr_mobile_v1.1_rec_quant_opt.nb ch_ppocr_mobile_cls_quant_opt.nb ./11.jpg ppocr_keys_v1.txt + ``` + + 如果对代码做了修改,则需要重新编译并push到手机上。 + + 运行效果如下: + +
+ +
+ + +## FAQ +Q1:如果想更换模型怎么办,需要重新按照流程走一遍吗? +A1:如果已经走通了上述步骤,更换模型只需要替换 .nb 模型文件即可,同时要注意字典更新 + +Q2:换一个图测试怎么做? +A2:替换debug下的.jpg测试图像为你想要测试的图像,adb push 到手机上即可 + +Q3:如何封装到手机APP中? +A3:此demo旨在提供能在手机上运行OCR的核心算法部分,PaddleOCR/deploy/android_demo是将这个demo封装到手机app的示例,供参考 diff --git a/deploy/lite/readme_en.md b/deploy/lite/readme_en.md new file mode 100644 index 00000000..58f6a574 --- /dev/null +++ b/deploy/lite/readme_en.md @@ -0,0 +1,246 @@ + +# Tutorial of PaddleOCR Mobile deployment + +This tutorial will introduce how to use [paddle-lite](https://github.com/PaddlePaddle/Paddle-Lite) to deploy paddleOCR ultra-lightweight Chinese and English detection models on mobile phones. + +paddle-lite is a lightweight inference engine for PaddlePaddle. +It provides efficient inference capabilities for mobile phones and IoTs, +and extensively integrates cross-platform hardware to provide lightweight +deployment solutions for end-side deployment issues. + +## 1. Preparation + +- Computer (for Compiling Paddle Lite) +- Mobile phone (arm7 or arm8) + +***Note: PaddleOCR lite deployment currently does not support dynamic graph models, only models saved with static graph. The static branch of PaddleOCR is `develop`.*** + +## 2. Build PaddleLite library +1. [Docker](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#docker) +2. [Linux](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#linux) +3. [MAC OS](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#mac-os) + +## 3. Prepare prebuild library for android and ios + +### 3.1 Download prebuild library +|Platform|Prebuild library Download Link| +|---|---| +|Android|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv7.gcc.c++_shared.with_extra.with_cv.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.android.armv8.gcc.c++_shared.with_extra.with_cv.tar.gz)| +|IOS|[arm7](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv7.with_cv.with_extra.with_log.tiny_publish.tar.gz) / [arm8](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.8/inference_lite_lib.ios.armv8.with_cv.with_extra.with_log.tiny_publish.tar.gz)| + +note: The above pre-build inference library is compiled from the PaddleLite `release/v2.8` branch. For more information about PaddleLite 2.8, please refer to [link](https://github.com/PaddlePaddle/Paddle-Lite/releases/tag/v2.8). + +### 3.2 Compile prebuild library (Recommended) +``` +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +# checkout to Paddle-Lite release/v2.8 branch +git checkout release/v2.8 +./lite/tools/build_android.sh --arch=armv8 --with_cv=ON --with_extra=ON +``` + +The structure of the prediction library is as follows: + +``` +inference_lite_lib.android.armv8/ +|-- cxx C++ prebuild library +| |-- include C++ +| | |-- paddle_api.h +| | |-- paddle_image_preprocess.h +| | |-- paddle_lite_factory_helper.h +| | |-- paddle_place.h +| | |-- paddle_use_kernels.h +| | |-- paddle_use_ops.h +| | `-- paddle_use_passes.h +| `-- lib +| |-- libpaddle_api_light_bundled.a C++ static library +| `-- libpaddle_light_api_shared.so C++ dynamic library +|-- java Java predict library +| |-- jar +| | `-- PaddlePredictor.jar +| |-- so +| | `-- libpaddle_lite_jni.so +| `-- src +|-- demo C++ and java demo +| |-- cxx +| `-- java +``` + + +## 4. Inference Model Optimization + +Paddle Lite provides a variety of strategies to automatically optimize the original training model, including quantization, sub-graph fusion, hybrid scheduling, Kernel optimization and so on. In order to make the optimization process more convenient and easy to use, Paddle Lite provide opt tools to automatically complete the optimization steps and output a lightweight, optimal executable model. + +If you have prepared the model file ending in `.nb`, you can skip this step. + +The following table also provides a series of models that can be deployed on mobile phones to recognize Chinese. +You can directly download the optimized model. + +| Version | Introduction | Model size | Detection model | Text Direction model | Recognition model | Paddle Lite branch | +| --- | --- | --- | --- | --- | --- | --- | +| V1.1 | extra-lightweight chinese OCR optimized model | 8.1M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_opt.nb) | develop | +| [slim] V1.1 | extra-lightweight chinese OCR optimized model | 3.5M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_det_prune_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_cls_quant_opt.nb) | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.1_rec_quant_opt.nb) | develop | +| V1.0 | lightweight Chinese OCR optimized model | 8.6M | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.0_det_opt.nb) | - | [Download](https://paddleocr.bj.bcebos.com/20-09-22/mobile/lite/ch_ppocr_mobile_v1.0_rec_opt.nb) | develop | + +If the model to be deployed is not in the above table, you need to follow the steps below to obtain the optimized model. + +``` +git clone https://github.com/PaddlePaddle/Paddle-Lite.git +cd Paddle-Lite +git checkout release/v2.7 +./lite/tools/build.sh build_optimize_tool +``` + +The `opt` tool can be obtained by compiling Paddle Lite. + +After the compilation is complete, the opt file is located under `build.opt/lite/api/`. + +The `opt` can optimize the inference model saved by paddle.io.save_inference_model to get the model that the paddlelite API can use. + +The usage of opt is as follows: +``` +# 【Recommend】V1.1 is better than V1.0. steps for convert V1.1 model to nb file are as follows +wget https://paddleocr.bj.bcebos.com/20-09-22/mobile-slim/det/ch_ppocr_mobile_v1.1_det_prune_infer.tar && tar xf ch_ppocr_mobile_v1.1_det_prune_infer.tar +wget https://paddleocr.bj.bcebos.com/20-09-22/mobile-slim/rec/ch_ppocr_mobile_v1.1_rec_quant_infer.tar && tar xf ch_ppocr_mobile_v1.1_rec_quant_infer.tar + +./opt --model_file=./ch_ppocr_mobile_v1.1_det_prune_infer/model --param_file=./ch_ppocr_mobile_v1.1_det_prune_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_det_prune_opt --valid_targets=arm +./opt --model_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/model --param_file=./ch_ppocr_mobile_v1.1_rec_quant_infer/params --optimize_out=./ch_ppocr_mobile_v1.1_rec_quant_opt --valid_targets=arm + +# or use V1.0 model +wget https://paddleocr.bj.bcebos.com/ch_models/ch_det_mv3_db_infer.tar && tar xf ch_det_mv3_db_infer.tar +wget https://paddleocr.bj.bcebos.com/ch_models/ch_rec_mv3_crnn_infer.tar && tar xf ch_rec_mv3_crnn_infer.tar + +./opt --model_file=./ch_det_mv3_db/model --param_file=./ch_det_mv3_db/params --optimize_out_type=naive_buffer --optimize_out=./ch_det_mv3_db_opt --valid_targets=arm +./opt --model_file=./ch_rec_mv3_crnn/model --param_file=./ch_rec_mv3_crnn/params --optimize_out_type=naive_buffer --optimize_out=./ch_rec_mv3_crnn_opt --valid_targets=arm + +``` + +When the above code command is completed, there will be two more files `.nb` in the current directory, which is the converted model file. + +## 5. Run optimized model on Phone + +1. Prepare an Android phone with arm8. If the compiled prediction library and opt file are armv7, you need an arm7 phone and modify ARM_ABI = arm7 in the Makefile. + +2. Make sure the phone is connected to the computer, open the USB debugging option of the phone, and select the file transfer mode. + +3. Install the adb tool on the computer. + 3.1 Install ADB for MAC + ``` + brew cask install android-platform-tools + ``` + 3.2 Install ADB for Linux + ``` + sudo apt update + sudo apt install -y wget adb + ``` + 3.3 Install ADB for windows + [Download Link](https://developer.android.com/studio) + + Verify whether adb is installed successfully + ``` + $ adb devices + + List of devices attached + 744be294 device + ``` + + If there is `device` output, it means the installation was successful. + +4. Prepare optimized models, prediction library files, test images and dictionary files used. + +``` + git clone https://github.com/PaddlePaddle/PaddleOCR.git + cd PaddleOCR/deploy/lite/ + # run prepare.sh + sh prepare.sh /{lite prediction library path}/inference_lite_lib.android.armv8 + + # + cd /{lite prediction library path}/inference_lite_lib.android.armv8/ + cd demo/cxx/ocr/ + # copy paddle-lite C++ .so file to debug/ directory + cp ../../../cxx/lib/libpaddle_light_api_shared.so ./debug/ + + cd inference_lite_lib.android.armv8/demo/cxx/ocr/ + cp ../../../cxx/lib/libpaddle_light_api_shared.so ./debug/ + +``` + +Prepare the test image, taking `PaddleOCR/doc/imgs/11.jpg` as an example, copy the image file to the `demo/cxx/ocr/debug/` folder. +Prepare the model files optimized by the lite opt tool, `ch_det_mv3_db_opt.nb, ch_rec_mv3_crnn_opt.nb`, +and place them under the `demo/cxx/ocr/debug/` folder. + + +The structure of the OCR demo is as follows after the above command is executed: +``` +demo/cxx/ocr/ +|-- debug/ +| |--ch_ppocr_mobile_v1.1_det_prune_opt.nb Detection model +| |--ch_ppocr_mobile_v1.1_rec_quant_opt.nb Recognition model +| |--ch_ppocr_mobile_cls_quant_opt.nb Text direction classification model +| |--11.jpg Image for OCR +| |--ppocr_keys_v1.txt Dictionary file +| |--libpaddle_light_api_shared.so C++ .so file +| |--config.txt Config file +|-- config.txt +|-- crnn_process.cc +|-- crnn_process.h +|-- db_post_process.cc +|-- db_post_process.h +|-- Makefile +|-- ocr_db_crnn.cc + +``` + +#### Note: +1. ppocr_keys_v1.txt is a Chinese dictionary file. +If the nb model is used for English recognition or other language recognition, dictionary file should be replaced with a dictionary of the corresponding language. +PaddleOCR provides a variety of dictionaries under ppocr/utils/, including: +``` +dict/french_dict.txt # french +dict/german_dict.txt # german +ic15_dict.txt # english +dict/japan_dict.txt # japan +dict/korean_dict.txt # korean +ppocr_keys_v1.txt # chinese +``` + +2. `config.txt` of the detector and classifier, as shown below: +``` +max_side_len 960 # Limit the maximum image height and width to 960 +det_db_thresh 0.3 # Used to filter the binarized image of DB prediction, setting 0.-0.3 has no obvious effect on the result +det_db_box_thresh 0.5 # DDB post-processing filter box threshold, if there is a missing box detected, it can be reduced as appropriate +det_db_unclip_ratio 1.6 # Indicates the compactness of the text box, the smaller the value, the closer the text box to the text +use_direction_classify 0 # Whether to use the direction classifier, 0 means not to use, 1 means to use +``` + +5. Run Model on phone + +``` +cd inference_lite_lib.android.armv8/demo/cxx/ocr/ +make -j +mv ocr_db_crnn ./debug/ +adb push debug /data/local/tmp/ +adb shell +cd /data/local/tmp/debug +export LD_LIBRARY_PATH=/data/local/tmp/debug:$LD_LIBRARY_PATH +# run model + ./ocr_db_crnn ch_ppocr_mobile_v1.1_det_prune_opt.nb ch_ppocr_mobile_v1.1_rec_quant_opt.nb ch_ppocr_mobile_cls_quant_opt.nb ./11.jpg ppocr_keys_v1.txt +``` + +The outputs are as follows: + +
+ +
+ +## FAQ + +Q1: What if I want to change the model, do I need to run it again according to the process? +A1: If you have performed the above steps, you only need to replace the .nb model file to complete the model replacement. + +Q2: How to test with another picture? +A2: Replace the .jpg test image under `./debug` with the image you want to test, and run `adb push` to push new image to the phone. + +Q3: How to package it into the mobile APP? +A3: This demo aims to provide the core algorithm part that can run OCR on mobile phones. Further, +PaddleOCR/deploy/android_demo is an example of encapsulating this demo into a mobile app for reference. diff --git a/doc/imgs_results/lite_demo.png b/doc/imgs_results/lite_demo.png new file mode 100644 index 0000000000000000000000000000000000000000..c9daf1b2e6d8fa65da84b4ed36013db3970d52e3 GIT binary patch literal 96358 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%0A6t+wnAOfL6~YU;GqxH1*gyXM&ENXf58W4V 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