diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index 8835fb0f..e48ae9dc 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 15 May 2023 * +* Date : 24 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -15,7 +15,9 @@ namespace Clipper2Lib { -enum class JoinType { Square, Round, Miter }; +enum class JoinType { Square, Bevel, Round, Miter }; +//Square : Joins are 'squared' at exactly the offset distance (more complex code) +//Bevel : Similar to Square, but the offset distance varies with angle (simple code & faster) enum class EndType {Polygon, Joined, Butt, Square, Round}; //Butt : offsets both sides of a path, with square blunt ends @@ -51,7 +53,7 @@ class ClipperOffset { PathD norms; Paths64 solution; std::vector groups_; - JoinType join_type_ = JoinType::Square; + JoinType join_type_ = JoinType::Bevel; EndType end_type_ = EndType::Polygon; double miter_limit_ = 0.0; @@ -64,6 +66,7 @@ class ClipperOffset { #endif DeltaCallback64 deltaCallback64_ = nullptr; + void DoBevel(Group& group, const Path64& path, size_t j, size_t k); void DoSquare(Group& group, const Path64& path, size_t j, size_t k); void DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a); void DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle); diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index 2c63284a..0439ec52 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 September 2023 * +* Date : 24 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -200,6 +200,24 @@ PointD IntersectPoint(const PointD& pt1a, const PointD& pt1b, } } +void ClipperOffset::DoBevel(Group& group, const Path64& path, size_t j, size_t k) +{ + PointD pt1, pt2; + if (j == k) + { + double abs_delta = std::abs(group_delta_); + pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y); + pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y); + } + else + { + pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y); + pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y); + } + group.path.push_back(Point64(pt1)); + group.path.push_back(Point64(pt2)); +} + void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t k) { PointD vec; @@ -342,10 +360,13 @@ void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t k) if (cos_a > temp_lim_ - 1) DoMiter(group, path, j, k, cos_a); else DoSquare(group, path, j, k); } - else if (cos_a > 0.99 || join_type_ == JoinType::Square) // 0.99 ~= 8.1 deg. - DoSquare(group, path, j, k); - else + else if (cos_a > 0.99 || join_type_ == JoinType::Bevel) + // ie > 2.5 deg (see above) but less than ~8 deg ( acos(0.99) ) + DoBevel(group, path, j, k); + else if (join_type_ == JoinType::Round) DoRound(group, path, j, k, std::atan2(sin_a, cos_a)); + else + DoSquare(group, path, j, k); } void ClipperOffset::OffsetPolygon(Group& group, Path64& path) @@ -394,17 +415,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) switch (end_type_) { case EndType::Butt: -#ifdef USINGZ - group.path.push_back(Point64( - path[0].x - norms[0].x * group_delta_, - path[0].y - norms[0].y * group_delta_, - path[0].z)); -#else - group.path.push_back(Point64( - path[0].x - norms[0].x * group_delta_, - path[0].y - norms[0].y * group_delta_)); -#endif - group.path.push_back(GetPerpendic(path[0], norms[0], group_delta_)); + DoBevel(group, path, 0, 0); break; case EndType::Round: DoRound(group, path, 0, 0, PI); @@ -436,17 +447,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) switch (end_type_) { case EndType::Butt: -#ifdef USINGZ - group.path.push_back(Point64( - path[highI].x - norms[highI].x * group_delta_, - path[highI].y - norms[highI].y * group_delta_, - path[highI].z)); -#else - group.path.push_back(Point64( - path[highI].x - norms[highI].x * group_delta_, - path[highI].y - norms[highI].y * group_delta_)); -#endif - group.path.push_back(GetPerpendic(path[highI], norms[highI], group_delta_)); + DoBevel(group, path, highI, highI); break; case EndType::Round: DoRound(group, path, highI, highI, PI); diff --git a/CPP/Examples/Inflate/Inflate.cpp b/CPP/Examples/Inflate/Inflate.cpp index 32e4f21e..cc181169 100644 --- a/CPP/Examples/Inflate/Inflate.cpp +++ b/CPP/Examples/Inflate/Inflate.cpp @@ -13,10 +13,10 @@ void System(const std::string& filename); int main(int argc, char* argv[]) { - //DoSimpleShapes(); + DoSimpleShapes(); DoRabbit(); - std::getchar(); + //std::getchar(); } void DoSimpleShapes() @@ -26,23 +26,29 @@ void DoSimpleShapes() FillRule fr2 = FillRule::EvenOdd; SvgWriter svg2; - op1.push_back(MakePath({ 80,60, 20,20, 180,20, 180,80, 20,180, 180,180 })); - op2 = InflatePaths(op1, 20, JoinType::Square, EndType::Butt); + op1.push_back(MakePath({ 80,60, 20,20, 180,20, 180,70, 25,150, 20,180, 180,180 })); + op2 = InflatePaths(op1, 20, JoinType::Miter, EndType::Square, 3); SvgAddOpenSubject(svg2, op1, fr2, false); SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); - SvgAddCaption(svg2, "Square Joins; Butt Ends", 20, 220); + SvgAddCaption(svg2, "Miter Joins; Square Ends", 20, 210); - op1 = TranslatePaths(op1, 250, 0); - op2 = InflatePaths(op1, 20, JoinType::Miter, EndType::Square, 3); + op1 = TranslatePaths(op1, 210, 0); + op2 = InflatePaths(op1, 20, JoinType::Square, EndType::Square); + SvgAddOpenSubject(svg2, op1, fr2, false); + SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); + SvgAddCaption(svg2, "Square Joins; Square Ends", 230, 210); + + op1 = TranslatePaths(op1, 210, 0); + op2 = InflatePaths(op1, 20, JoinType::Bevel, EndType::Butt, 3); SvgAddOpenSubject(svg2, op1, fr2, false); SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); - SvgAddCaption(svg2, "Miter Joins; Square Ends", 300, 220); + SvgAddCaption(svg2, "Bevel Joins; Butt Ends", 440, 210); - op1 = TranslatePaths(op1, 250, 0); + op1 = TranslatePaths(op1, 210, 0); op2 = InflatePaths(op1, 20, JoinType::Round, EndType::Round); SvgAddOpenSubject(svg2, op1, fr2, false); SvgAddSolution(svg2, Paths64ToPathsD(op2), fr2, false); - SvgAddCaption(svg2, "Round Joins; Round Ends", 580, 220); + SvgAddCaption(svg2, "Round Joins; Round Ends", 650, 210); SvgSaveToFile(svg2, "open_paths.svg", 800, 600, 20); System("open_paths.svg"); @@ -68,7 +74,7 @@ void DoSimpleShapes() //different join types within the same offset operation ClipperOffset co; co.AddPaths(p, JoinType::Miter, EndType::Joined); - p = TranslatePaths(p, 120, 100); + p = TranslatePaths(p, 120, 100); pp.insert(pp.end(), p.begin(), p.end()); co.AddPaths(p, JoinType::Round, EndType::Joined); co.Execute(20, p); diff --git a/CPP/Utils/clipper.svg.cpp b/CPP/Utils/clipper.svg.cpp index c7b1894e..337a07ab 100644 --- a/CPP/Utils/clipper.svg.cpp +++ b/CPP/Utils/clipper.svg.cpp @@ -260,7 +260,7 @@ namespace Clipper2Lib { file << " font_name << "\" font-size=\"" << ti->font_size << "\" fill=\"" << ColorToHtml(ti->font_color) << "\" fill-opacity=\"" << GetAlphaAsFrac(ti->font_color) << "\">\n"; - file << " x + margin) << "\" y=\"" << (ti->y+margin) << "\">" << + file << " x * scale + offsetX) << "\" y=\"" << (ti->y * scale + offsetY) << "\">" << ti->text << "\n \n\n"; } diff --git a/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs b/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs index 31d8aa7a..f7542aa2 100644 --- a/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs +++ b/CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs @@ -1,8 +1,8 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 July 2023 * +* Date : 24 September 2023 * * Website : http://www.angusj.com * -* Copyright : Angus Johnson 2010-2022 * +* Copyright : Angus Johnson 2010-2023 * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ @@ -32,28 +32,39 @@ public static void DoSimpleShapes() for (int i = 0; i < 5; ++i) { - //nb: the following '10' parameter greatly increases miter limit + //nb: the last parameter here (10) greatly increases miter limit p = Clipper.InflatePaths(p, 5, JoinType.Miter, EndType.Polygon, 10); pp.AddRange(p); } //rectangle offset - both squared and rounded p.Clear(); - p.Add(Clipper.MakePath(new int[] { 100, 0, 340, 0, 340, 200, 100, 200 })); - pp.AddRange(p); + ClipperOffset co = new(); + //nb: using the ClipperOffest class directly here to control //different join types within the same offset operation - ClipperOffset co = new(); + p.Add(Clipper.MakePath(new int[] { 100, 0, 340, 0, 340, 200, 100, 200 })); + pp.AddRange(p); + co.AddPaths(p, JoinType.Bevel, EndType.Joined); + + p = Clipper.TranslatePaths(p, 60, 50); + pp.AddRange(p); co.AddPaths(p, JoinType.Square, EndType.Joined); - p = Clipper.TranslatePaths(p, 120, 100); + + p = Clipper.TranslatePaths(p, 60, 50); pp.AddRange(p); co.AddPaths(p, JoinType.Round, EndType.Joined); + + co.Execute(20, p); pp.AddRange(p); string filename = "../../../inflate.svg"; SvgWriter svg = new(); SvgUtils.AddSolution(svg, pp, false); + SvgUtils.AddCaption(svg, "Beveled join", 100, -27); + SvgUtils.AddCaption(svg, "Squared join", 160, 23); + SvgUtils.AddCaption(svg, "Rounded join", 220, 73); SvgUtils.SaveToFile(svg, filename, FillRule.EvenOdd, 800, 600, 20); ClipperFileIO.OpenFileWithDefaultApp(filename); } @@ -69,7 +80,7 @@ public static void DoRabbit() pd = Clipper.InflatePaths(pd, -2.5, JoinType.Round, EndType.Polygon); // SimplifyPaths - is not essential but it not only // speeds up the loop but it also tidies the result - pd = Clipper.SimplifyPaths(pd, 0.2); + pd = Clipper.SimplifyPaths(pd, 0.25); solution.AddRange(pd); } diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 8d4a9c90..eb9eba0b 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 September 2023 * +* Date : 24 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -15,9 +15,10 @@ namespace Clipper2Lib { public enum JoinType { + Miter, Square, - Round, - Miter + Bevel, + Round }; public enum EndType @@ -323,6 +324,25 @@ private PointD GetPerpendicD(Point64 pt, PointD norm) #endif } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoBevel(Group group, Path64 path, int j, int k) + { + Point64 pt1, pt2; + if (j == k) + { + double absDelta = Math.Abs(_groupDelta); + pt1 = new Point64(path[j].X - absDelta * _normals[j].x, path[j].Y - absDelta * _normals[j].y); + pt2 = new Point64(path[j].X + absDelta * _normals[j].x, path[j].Y + absDelta * _normals[j].y); + } + else + { + pt1 = new Point64(path[j].X + _groupDelta * _normals[k].x, path[j].Y + _groupDelta * _normals[k].y); + pt2 = new Point64(path[j].X + _groupDelta * _normals[j].x, path[j].Y + _groupDelta * _normals[j].y); + } + group.outPath.Add(pt1); + group.outPath.Add(pt2); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DoSquare(Group group, Path64 path, int j, int k) { @@ -483,12 +503,14 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) if (cosA > _mitLimSqr - 1) DoMiter(group, path, j, k, cosA); else DoSquare(group, path, j, k); } - else if (cosA > 0.99 || _joinType == JoinType.Square) + else if (cosA > 0.99 || _joinType == JoinType.Bevel) //angle less than 8 degrees or a squared join - DoSquare(group, path, j, k); - else + DoBevel(group, path, j, k); + else if (_joinType == JoinType.Round) DoRound(group, path, j, k, Math.Atan2(sinA, cosA)); - + else + DoSquare(group, path, j, k); + k = j; } @@ -537,17 +559,7 @@ private void OffsetOpenPath(Group group, Path64 path) switch (_endType) { case EndType.Butt: -#if USINGZ - group.outPath.Add(new Point64( - path[0].X - _normals[0].x * _groupDelta, - path[0].Y - _normals[0].y * _groupDelta, - path[0].Z)); -#else - group.outPath.Add(new Point64( - path[0].X - _normals[0].x * _groupDelta, - path[0].Y - _normals[0].y * _groupDelta)); -#endif - group.outPath.Add(GetPerpendic(path[0], _normals[0])); + DoBevel(group, path, 0, 0); break; case EndType.Round: DoRound(group, path, 0, 0, Math.PI); @@ -575,17 +587,7 @@ private void OffsetOpenPath(Group group, Path64 path) switch (_endType) { case EndType.Butt: -#if USINGZ - group.outPath.Add(new Point64( - path[highI].X - _normals[highI].x * _groupDelta, - path[highI].Y - _normals[highI].y * _groupDelta, - path[highI].Z)); -#else - group.outPath.Add(new Point64( - path[highI].X - _normals[highI].x * _groupDelta, - path[highI].Y - _normals[highI].y * _groupDelta)); -#endif - group.outPath.Add(GetPerpendic(path[highI], _normals[highI])); + DoBevel(group, path, highI, highI); break; case EndType.Round: DoRound(group, path, highI, highI, Math.PI); diff --git a/CSharp/Utils/SVG/Clipper.SVG.cs b/CSharp/Utils/SVG/Clipper.SVG.cs index 08f922fb..42ffdd5e 100644 --- a/CSharp/Utils/SVG/Clipper.SVG.cs +++ b/CSharp/Utils/SVG/Clipper.SVG.cs @@ -305,7 +305,7 @@ public bool SaveToFile(string filename, int maxWidth = 0, int maxHeight = 0, int { writer.Write("\n", captionInfo.fontSize, ColorToHtml(captionInfo.fontColor)); - writer.Write("{2}\n\n", captionInfo.posX + margin, captionInfo.posY + margin, captionInfo.text); + writer.Write("{2}\n\n", captionInfo.posX * scale + offsetX, captionInfo.posY * scale + offsetX, captionInfo.text); } writer.Write("\n"); diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 5ea37644..f3bf669a 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 19 September 2023 * +* Date : 24 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -18,7 +18,10 @@ interface type - TJoinType = (jtSquare, jtRound, jtMiter); + TJoinType = (jtMiter, jtSquare, jtBevel, jtRound); + //jtSquare: Joins are 'squared' at exactly the offset distance (complex code) + //jtBevel : offset distances vary depending on the angle (simple code, faster) + TEndType = (etPolygon, etJoined, etButt, etSquare, etRound); // etButt : offsets both sides of a path, with square blunt ends // etSquare : offsets both sides of a path, with square extended ends @@ -70,6 +73,7 @@ TClipperOffset = class procedure AddPoint(const pt: TPoint64); overload; {$IFDEF INLINING} inline; {$ENDIF} procedure DoSquare(j, k: Integer); + procedure DoBevel(j, k: Integer); procedure DoMiter(j, k: Integer; cosA: Double); procedure DoRound(j, k: integer; angle: double); procedure OffsetPoint(j: Integer; var k: integer); @@ -483,20 +487,7 @@ procedure TClipperOffset.OffsetOpenPath; AddPoint(fInPath[0]); end else case fEndType of - etButt: - begin -{$IFDEF USINGZ} - with fInPath[0] do AddPoint(Point64( - X - fNorms[0].X * fGroupDelta, - Y - fNorms[0].Y * fGroupDelta, - Z)); -{$ELSE} - with fInPath[0] do AddPoint(Point64( - X - fNorms[0].X * fGroupDelta, - Y - fNorms[0].Y * fGroupDelta)); -{$ENDIF} - AddPoint(GetPerpendic(fInPath[0], fNorms[0], fGroupDelta)); - end; + etButt: DoBevel(0, 0); etRound: DoRound(0,0, PI); else DoSquare(0, 0); end; @@ -523,20 +514,7 @@ procedure TClipperOffset.OffsetOpenPath; AddPoint(fInPath[highI]); end else case fEndType of - etButt: - begin -{$IFDEF USINGZ} - with fInPath[highI] do AddPoint(Point64( - X - fNorms[highI].X *fGroupDelta, - Y - fNorms[highI].Y *fGroupDelta, - Z)); -{$ELSE} - with fInPath[highI] do AddPoint(Point64( - X - fNorms[highI].X *fGroupDelta, - Y - fNorms[highI].Y *fGroupDelta)); -{$ENDIF} - AddPoint(GetPerpendic(fInPath[highI], fNorms[highI], fGroupDelta)); - end; + etButt: DoBevel(highI, highI); etRound: DoRound(highI,highI, PI); else DoSquare(highI, highI); end; @@ -733,6 +711,31 @@ function ReflectPoint(const pt, pivot: TPointD): TPointD; end; //------------------------------------------------------------------------------ +procedure TClipperOffset.DoBevel(j, k: Integer); +var + absDelta: double; +begin + if k = j then + begin + absDelta := abs(fGroupDelta); + AddPoint( + fInPath[j].x - absDelta * fNorms[j].x, + fInPath[j].y - absDelta * fNorms[j].y); + AddPoint( + fInPath[j].x + absDelta * fNorms[j].x, + fInPath[j].y + absDelta * fNorms[j].y); + end else + begin + AddPoint( + fInPath[j].x + fGroupDelta * fNorms[k].x, + fInPath[j].y + fGroupDelta * fNorms[k].y); + AddPoint( + fInPath[j].x + fGroupDelta * fNorms[j].x, + fInPath[j].y + fGroupDelta * fNorms[j].y); + end; +end; +//------------------------------------------------------------------------------ + procedure TClipperOffset.DoSquare(j, k: Integer); var vec, pt1,pt2,pt3,pt4, pt,ptQ : TPointD; @@ -912,11 +915,13 @@ procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer); if (cosA > fTmpLimit -1) then DoMiter(j, k, cosA) else DoSquare(j, k); end - else if (cosA > 0.99) or (fJoinType = jtSquare) then - //angle less than 8 degrees or squared joins - DoSquare(j, k) + else if (cosA > 0.99) or (fJoinType = jtBevel) then + // ie > 2.5 deg (see above) but less than ~8 deg ( acos(0.99) ) + DoBevel(j, k) + else if (fJoinType = jtRound) then + DoRound(j, k, ArcTan2(sinA, cosA)) else - DoRound(j, k, ArcTan2(sinA, cosA)); + DoSquare(j, k); k := j; end; diff --git a/Delphi/Examples/Example1/Example1.dpr b/Delphi/Examples/Example1/Example1.dpr index 068ec729..c3c067a8 100644 --- a/Delphi/Examples/Example1/Example1.dpr +++ b/Delphi/Examples/Example1/Example1.dpr @@ -48,4 +48,37 @@ begin end; ShellExecute(0, 'open','Sample1.svg', nil, nil, SW_SHOW); + setLength(subj, 1); + subj[0] := MakePath([40,60, 20,20, 180,20, 180,70, 25,150, 20,180, 180,180]); + solution := InflatePaths(subj, 20, jtMiter, etSquare, 3); + + with TSvgWriter.Create(fillRule) do + try + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Miter Joins; Square Ends', 10, 210); + + subj := Clipper.TranslatePaths(subj, 210, 0); + solution := InflatePaths(subj, 20, jtSquare, etSquare, 3); + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Square Joins; Square Ends', 220, 210); + + subj := Clipper.TranslatePaths(subj, 210, 0); + solution := InflatePaths(subj, 20, jtBevel, etButt, 3); + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Bevel Joins; Butt Ends', 430, 210); + + subj := Clipper.TranslatePaths(subj, 210, 0); + solution := InflatePaths(subj, 20, jtRound, etRound, 3); + AddPaths(subj, true, $2000BBFF, $800033FF, 0.8); + AddPaths( solution, false, $2000FF00, $FF006600, 1.2, false); + AddText('Round Joins; Round Ends', 640, 210); + SaveToFile('offsets.svg', 800,600, 40); + finally + free; + end; + ShellExecute(0, 'open','offsets.svg', nil, nil, SW_SHOW); + end.