Path: blob/master/thirdparty/clipper2/src/clipper.offset.cpp
9902 views
/*******************************************************************************1* Author : Angus Johnson *2* Date : 4 May 2025 *3* Website : https://www.angusj.com *4* Copyright : Angus Johnson 2010-2025 *5* Purpose : Path Offset (Inflate/Shrink) *6* License : https://www.boost.org/LICENSE_1_0.txt *7*******************************************************************************/89#include "clipper2/clipper.h"10#include "clipper2/clipper.offset.h"1112namespace Clipper2Lib {1314const double floating_point_tolerance = 1e-12;1516// Clipper2 approximates arcs by using series of relatively short straight17//line segments. And logically, shorter line segments will produce better arc18// approximations. But very short segments can degrade performance, usually19// with little or no discernable improvement in curve quality. Very short20// segments can even detract from curve quality, due to the effects of integer21// rounding. Since there isn't an optimal number of line segments for any given22// arc radius (that perfectly balances curve approximation with performance),23// arc tolerance is user defined. Nevertheless, when the user doesn't define24// an arc tolerance (ie leaves alone the 0 default value), the calculated25// default arc tolerance (offset_radius / 500) generally produces good (smooth)26// arc approximations without producing excessively small segment lengths.27// See also: https://www.angusj.com/clipper2/Docs/Trigonometry.htm28const double arc_const = 0.002; // <-- 1/500293031//------------------------------------------------------------------------------32// Miscellaneous methods33//------------------------------------------------------------------------------3435void GetLowestClosedPathInfo(const Paths64& paths, std::optional<size_t>& idx, bool& is_neg_area)36{37idx.reset();38Point64 botPt = Point64(INT64_MAX, INT64_MIN);39for (size_t i = 0; i < paths.size(); ++i)40{41double a = MAX_DBL;42for (const Point64& pt : paths[i])43{44if ((pt.y < botPt.y) ||45((pt.y == botPt.y) && (pt.x >= botPt.x))) continue;46if (a == MAX_DBL)47{48a = Area(paths[i]);49if (a == 0) break; // invalid closed path, so break from inner loop50is_neg_area = a < 0;51}52idx = i;53botPt.x = pt.x;54botPt.y = pt.y;55}56}57}5859inline double Hypot(double x, double y)60{61// given that this is an internal function, and given the x and y parameters62// will always be coordinate values (or the difference between coordinate values),63// x and y should always be within INT64_MIN to INT64_MAX. Consequently,64// there should be no risk that the following computation will overflow65// see https://stackoverflow.com/a/32436148/35953866return std::sqrt(x * x + y * y);67}6869static PointD GetUnitNormal(const Point64& pt1, const Point64& pt2)70{71if (pt1 == pt2) return PointD(0.0, 0.0);72double dx = static_cast<double>(pt2.x - pt1.x);73double dy = static_cast<double>(pt2.y - pt1.y);74double inverse_hypot = 1.0 / Hypot(dx, dy);75dx *= inverse_hypot;76dy *= inverse_hypot;77return PointD(dy, -dx);78}7980inline bool AlmostZero(double value, double epsilon = 0.001)81{82return std::fabs(value) < epsilon;83}8485inline PointD NormalizeVector(const PointD& vec)86{87double h = Hypot(vec.x, vec.y);88if (AlmostZero(h)) return PointD(0,0);89double inverseHypot = 1 / h;90return PointD(vec.x * inverseHypot, vec.y * inverseHypot);91}9293inline PointD GetAvgUnitVector(const PointD& vec1, const PointD& vec2)94{95return NormalizeVector(PointD(vec1.x + vec2.x, vec1.y + vec2.y));96}9798inline bool IsClosedPath(EndType et)99{100return et == EndType::Polygon || et == EndType::Joined;101}102103static inline Point64 GetPerpendic(const Point64& pt, const PointD& norm, double delta)104{105#ifdef USINGZ106return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta, pt.z);107#else108return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta);109#endif110}111112inline PointD GetPerpendicD(const Point64& pt, const PointD& norm, double delta)113{114#ifdef USINGZ115return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta, pt.z);116#else117return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta);118#endif119}120121inline void NegatePath(PathD& path)122{123for (PointD& pt : path)124{125pt.x = -pt.x;126pt.y = -pt.y;127#ifdef USINGZ128pt.z = pt.z;129#endif130}131}132133134//------------------------------------------------------------------------------135// ClipperOffset::Group methods136//------------------------------------------------------------------------------137138ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType _end_type):139paths_in(_paths), join_type(_join_type), end_type(_end_type)140{141bool is_joined =142(end_type == EndType::Polygon) ||143(end_type == EndType::Joined);144for (Path64& p: paths_in)145StripDuplicates(p, is_joined);146147if (end_type == EndType::Polygon)148{149bool is_neg_area;150GetLowestClosedPathInfo(paths_in, lowest_path_idx, is_neg_area);151// the lowermost path must be an outer path, so if its orientation is negative,152// then flag the whole group is 'reversed' (will negate delta etc.)153// as this is much more efficient than reversing every path.154is_reversed = lowest_path_idx.has_value() && is_neg_area;155}156else157{158lowest_path_idx.reset();159is_reversed = false;160}161}162163//------------------------------------------------------------------------------164// ClipperOffset methods165//------------------------------------------------------------------------------166167void ClipperOffset::AddPath(const Path64& path, JoinType jt_, EndType et_)168{169groups_.emplace_back(Paths64(1, path), jt_, et_);170}171172void ClipperOffset::AddPaths(const Paths64 &paths, JoinType jt_, EndType et_)173{174if (paths.size() == 0) return;175groups_.emplace_back(paths, jt_, et_);176}177178void ClipperOffset::BuildNormals(const Path64& path)179{180norms.clear();181norms.reserve(path.size());182if (path.size() == 0) return;183Path64::const_iterator path_iter, path_stop_iter = --path.cend();184for (path_iter = path.cbegin(); path_iter != path_stop_iter; ++path_iter)185norms.emplace_back(GetUnitNormal(*path_iter,*(path_iter +1)));186norms.emplace_back(GetUnitNormal(*path_stop_iter, *(path.cbegin())));187}188189void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k)190{191PointD pt1, pt2;192if (j == k)193{194double abs_delta = std::abs(group_delta_);195#ifdef USINGZ196pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y, path[j].z);197pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y, path[j].z);198#else199pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y);200pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y);201#endif202}203else204{205#ifdef USINGZ206pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y, path[j].z);207pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y, path[j].z);208#else209pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y);210pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y);211#endif212}213path_out.emplace_back(pt1);214path_out.emplace_back(pt2);215}216217void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k)218{219PointD vec;220if (j == k)221vec = PointD(norms[j].y, -norms[j].x);222else223vec = GetAvgUnitVector(224PointD(-norms[k].y, norms[k].x),225PointD(norms[j].y, -norms[j].x));226227double abs_delta = std::abs(group_delta_);228229// now offset the original vertex delta units along unit vector230PointD ptQ = PointD(path[j]);231ptQ = TranslatePoint(ptQ, abs_delta * vec.x, abs_delta * vec.y);232// get perpendicular vertices233PointD pt1 = TranslatePoint(ptQ, group_delta_ * vec.y, group_delta_ * -vec.x);234PointD pt2 = TranslatePoint(ptQ, group_delta_ * -vec.y, group_delta_ * vec.x);235// get 2 vertices along one edge offset236PointD pt3 = GetPerpendicD(path[k], norms[k], group_delta_);237if (j == k)238{239PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_);240PointD pt = ptQ;241GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt);242//get the second intersect point through reflecion243path_out.emplace_back(ReflectPoint(pt, ptQ));244path_out.emplace_back(pt);245}246else247{248PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_);249PointD pt = ptQ;250GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt);251path_out.emplace_back(pt);252//get the second intersect point through reflecion253path_out.emplace_back(ReflectPoint(pt, ptQ));254}255}256257void ClipperOffset::DoMiter(const Path64& path, size_t j, size_t k, double cos_a)258{259double q = group_delta_ / (cos_a + 1);260#ifdef USINGZ261path_out.emplace_back(262path[j].x + (norms[k].x + norms[j].x) * q,263path[j].y + (norms[k].y + norms[j].y) * q,264path[j].z);265#else266path_out.emplace_back(267path[j].x + (norms[k].x + norms[j].x) * q,268path[j].y + (norms[k].y + norms[j].y) * q);269#endif270}271272void ClipperOffset::DoRound(const Path64& path, size_t j, size_t k, double angle)273{274if (deltaCallback64_) {275// when deltaCallback64_ is assigned, group_delta_ won't be constant,276// so we'll need to do the following calculations for *every* vertex.277double abs_delta = std::fabs(group_delta_);278double arcTol = (arc_tolerance_ > floating_point_tolerance ?279std::min(abs_delta, arc_tolerance_) : abs_delta * arc_const);280double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI);281step_sin_ = std::sin(2 * PI / steps_per_360);282step_cos_ = std::cos(2 * PI / steps_per_360);283if (group_delta_ < 0.0) step_sin_ = -step_sin_;284steps_per_rad_ = steps_per_360 / (2 * PI);285}286287Point64 pt = path[j];288PointD offsetVec = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_);289290if (j == k) offsetVec.Negate();291#ifdef USINGZ292path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z);293#else294path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y);295#endif296int steps = static_cast<int>(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456297for (int i = 1; i < steps; ++i) // ie 1 less than steps298{299offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y,300offsetVec.x * step_sin_ + offsetVec.y * step_cos_);301#ifdef USINGZ302path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z);303#else304path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y);305#endif306}307path_out.emplace_back(GetPerpendic(path[j], norms[j], group_delta_));308}309310void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size_t k)311{312// Let A = change in angle where edges join313// A == 0: ie no change in angle (flat join)314// A == PI: edges 'spike'315// sin(A) < 0: right turning316// cos(A) < 0: change in angle is more than 90 degree317318if (path[j] == path[k]) return;319320double sin_a = CrossProduct(norms[j], norms[k]);321double cos_a = DotProduct(norms[j], norms[k]);322if (sin_a > 1.0) sin_a = 1.0;323else if (sin_a < -1.0) sin_a = -1.0;324325if (deltaCallback64_) {326group_delta_ = deltaCallback64_(path, norms, j, k);327if (group.is_reversed) group_delta_ = -group_delta_;328}329if (std::fabs(group_delta_) <= floating_point_tolerance)330{331path_out.emplace_back(path[j]);332return;333}334335if (cos_a > -0.999 && (sin_a * group_delta_ < 0)) // test for concavity first (#593)336{337// is concave338// by far the simplest way to construct concave joins, especially those joining very339// short segments, is to insert 3 points that produce negative regions. These regions340// will be removed later by the finishing union operation. This is also the best way341// to ensure that path reversals (ie over-shrunk paths) are removed.342#ifdef USINGZ343path_out.emplace_back(GetPerpendic(path[j], norms[k], group_delta_), path[j].z);344path_out.emplace_back(path[j]); // (#405, #873, #916)345path_out.emplace_back(GetPerpendic(path[j], norms[j], group_delta_), path[j].z);346#else347path_out.emplace_back(GetPerpendic(path[j], norms[k], group_delta_));348path_out.emplace_back(path[j]); // (#405, #873, #916)349path_out.emplace_back(GetPerpendic(path[j], norms[j], group_delta_));350#endif351}352else if (cos_a > 0.999 && join_type_ != JoinType::Round)353{354// almost straight - less than 2.5 degree (#424, #482, #526 & #724)355DoMiter(path, j, k, cos_a);356}357else if (join_type_ == JoinType::Miter)358{359// miter unless the angle is sufficiently acute to exceed ML360if (cos_a > temp_lim_ - 1) DoMiter(path, j, k, cos_a);361else DoSquare(path, j, k);362}363else if (join_type_ == JoinType::Round)364DoRound(path, j, k, std::atan2(sin_a, cos_a));365else if ( join_type_ == JoinType::Bevel)366DoBevel(path, j, k);367else368DoSquare(path, j, k);369}370371void ClipperOffset::OffsetPolygon(Group& group, const Path64& path)372{373path_out.clear();374for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j)375OffsetPoint(group, path, j, k);376solution->emplace_back(path_out);377}378379void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path)380{381OffsetPolygon(group, path);382Path64 reverse_path(path);383std::reverse(reverse_path.begin(), reverse_path.end());384385//rebuild normals386std::reverse(norms.begin(), norms.end());387norms.emplace_back(norms[0]);388norms.erase(norms.begin());389NegatePath(norms);390391OffsetPolygon(group, reverse_path);392}393394void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path)395{396// do the line start cap397if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, 0, 0);398399if (std::fabs(group_delta_) <= floating_point_tolerance)400path_out.emplace_back(path[0]);401else402{403switch (end_type_)404{405case EndType::Butt:406DoBevel(path, 0, 0);407break;408case EndType::Round:409DoRound(path, 0, 0, PI);410break;411default:412DoSquare(path, 0, 0);413break;414}415}416417size_t highI = path.size() - 1;418// offset the left side going forward419for (Path64::size_type j = 1, k = 0; j < highI; k = j, ++j)420OffsetPoint(group, path, j, k);421422// reverse normals423for (size_t i = highI; i > 0; --i)424norms[i] = PointD(-norms[i - 1].x, -norms[i - 1].y);425norms[0] = norms[highI];426427// do the line end cap428if (deltaCallback64_)429group_delta_ = deltaCallback64_(path, norms, highI, highI);430431if (std::fabs(group_delta_) <= floating_point_tolerance)432path_out.emplace_back(path[highI]);433else434{435switch (end_type_)436{437case EndType::Butt:438DoBevel(path, highI, highI);439break;440case EndType::Round:441DoRound(path, highI, highI, PI);442break;443default:444DoSquare(path, highI, highI);445break;446}447}448449for (size_t j = highI -1, k = highI; j > 0; k = j, --j)450OffsetPoint(group, path, j, k);451solution->emplace_back(path_out);452}453454void ClipperOffset::DoGroupOffset(Group& group)455{456if (group.end_type == EndType::Polygon)457{458// a straight path (2 points) can now also be 'polygon' offset459// where the ends will be treated as (180 deg.) joins460if (!group.lowest_path_idx.has_value()) delta_ = std::abs(delta_);461group_delta_ = (group.is_reversed) ? -delta_ : delta_;462}463else464group_delta_ = std::abs(delta_);// *0.5;465466double abs_delta = std::fabs(group_delta_);467join_type_ = group.join_type;468end_type_ = group.end_type;469470if (group.join_type == JoinType::Round || group.end_type == EndType::Round)471{472// calculate the number of steps required to approximate a circle473// (see https://www.angusj.com/clipper2/Docs/Trigonometry.htm)474// arcTol - when arc_tolerance_ is undefined (0) then curve imprecision475// will be relative to the size of the offset (delta). Obviously very476//large offsets will almost always require much less precision.477double arcTol = (arc_tolerance_ > floating_point_tolerance) ?478std::min(abs_delta, arc_tolerance_) : abs_delta * arc_const;479480double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI);481step_sin_ = std::sin(2 * PI / steps_per_360);482step_cos_ = std::cos(2 * PI / steps_per_360);483if (group_delta_ < 0.0) step_sin_ = -step_sin_;484steps_per_rad_ = steps_per_360 / (2 * PI);485}486487//double min_area = PI * Sqr(group_delta_);488Paths64::const_iterator path_in_it = group.paths_in.cbegin();489for ( ; path_in_it != group.paths_in.cend(); ++path_in_it)490{491Path64::size_type pathLen = path_in_it->size();492path_out.clear();493494if (pathLen == 1) // single point495{496if (deltaCallback64_)497{498group_delta_ = deltaCallback64_(*path_in_it, norms, 0, 0);499if (group.is_reversed) group_delta_ = -group_delta_;500abs_delta = std::fabs(group_delta_);501}502503if (group_delta_ < 1) continue;504const Point64& pt = (*path_in_it)[0];505//single vertex so build a circle or square ...506if (group.join_type == JoinType::Round)507{508double radius = abs_delta;509size_t steps = steps_per_rad_ > 0 ? static_cast<size_t>(std::ceil(steps_per_rad_ * 2 * PI)) : 0; //#617510path_out = Ellipse(pt, radius, radius, steps);511#ifdef USINGZ512for (auto& p : path_out) p.z = pt.z;513#endif514}515else516{517int d = (int)std::ceil(abs_delta);518Rect64 r = Rect64(pt.x - d, pt.y - d, pt.x + d, pt.y + d);519path_out = r.AsPath();520#ifdef USINGZ521for (auto& p : path_out) p.z = pt.z;522#endif523}524525solution->emplace_back(path_out);526continue;527} // end of offsetting a single point528529if ((pathLen == 2) && (group.end_type == EndType::Joined))530end_type_ = (group.join_type == JoinType::Round) ?531EndType::Round :532EndType::Square;533534BuildNormals(*path_in_it);535if (end_type_ == EndType::Polygon) OffsetPolygon(group, *path_in_it);536else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, *path_in_it);537else OffsetOpenPath(group, *path_in_it);538}539}540541#ifdef USINGZ542void ClipperOffset::ZCB(const Point64& bot1, const Point64& top1,543const Point64& bot2, const Point64& top2, Point64& ip)544{545if (bot1.z && ((bot1.z == bot2.z) || (bot1.z == top2.z))) ip.z = bot1.z;546else if (bot2.z && (bot2.z == top1.z)) ip.z = bot2.z;547else if (top1.z && (top1.z == top2.z)) ip.z = top1.z;548else if (zCallback64_) zCallback64_(bot1, top1, bot2, top2, ip);549}550#endif551552size_t ClipperOffset::CalcSolutionCapacity()553{554size_t result = 0;555for (const Group& g : groups_)556result += (g.end_type == EndType::Joined) ? g.paths_in.size() * 2 : g.paths_in.size();557return result;558}559560bool ClipperOffset::CheckReverseOrientation()561{562// nb: this assumes there's consistency in orientation between groups563bool is_reversed_orientation = false;564for (const Group& g : groups_)565if (g.end_type == EndType::Polygon)566{567is_reversed_orientation = g.is_reversed;568break;569}570return is_reversed_orientation;571}572573void ClipperOffset::ExecuteInternal(double delta)574{575error_code_ = 0;576if (groups_.size() == 0) return;577solution->reserve(CalcSolutionCapacity());578579if (std::abs(delta) < 0.5) // ie: offset is insignificant580{581Paths64::size_type sol_size = 0;582for (const Group& group : groups_) sol_size += group.paths_in.size();583solution->reserve(sol_size);584for (const Group& group : groups_)585copy(group.paths_in.begin(), group.paths_in.end(), back_inserter(*solution));586}587else588{589590temp_lim_ = (miter_limit_ <= 1) ?5912.0 :5922.0 / (miter_limit_ * miter_limit_);593594delta_ = delta;595std::vector<Group>::iterator git;596for (git = groups_.begin(); git != groups_.end(); ++git)597{598DoGroupOffset(*git);599if (!error_code_) continue; // all OK600solution->clear();601}602}603604if (!solution->size()) return;605606bool paths_reversed = CheckReverseOrientation();607//clean up self-intersections ...608Clipper64 c;609c.PreserveCollinear(preserve_collinear_);610//the solution should retain the orientation of the input611c.ReverseSolution(reverse_solution_ != paths_reversed);612#ifdef USINGZ613auto fp = std::bind(&ClipperOffset::ZCB, this, std::placeholders::_1,614std::placeholders::_2, std::placeholders::_3,615std::placeholders::_4, std::placeholders::_5);616c.SetZCallback(fp);617#endif618c.AddSubject(*solution);619if (solution_tree)620{621if (paths_reversed)622c.Execute(ClipType::Union, FillRule::Negative, *solution_tree);623else624c.Execute(ClipType::Union, FillRule::Positive, *solution_tree);625}626else627{628if (paths_reversed)629c.Execute(ClipType::Union, FillRule::Negative, *solution);630else631c.Execute(ClipType::Union, FillRule::Positive, *solution);632}633}634635void ClipperOffset::Execute(double delta, Paths64& paths64)636{637paths64.clear();638solution = &paths64;639solution_tree = nullptr;640ExecuteInternal(delta);641}642643644void ClipperOffset::Execute(double delta, PolyTree64& polytree)645{646polytree.Clear();647solution_tree = &polytree;648solution = new Paths64();649ExecuteInternal(delta);650delete solution;651solution = nullptr;652}653654void ClipperOffset::Execute(DeltaCallback64 delta_cb, Paths64& paths)655{656deltaCallback64_ = delta_cb;657Execute(1.0, paths);658}659660} // namespace661662663