From d8bb36408469b2032aa15ecd4fa9dc7122de5fc3 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Thu, 20 Apr 2017 14:57:18 +0100 Subject: [PATCH] add path outlining --- .../DrawShapesWithImageSharp.csproj | 4 +- samples/DrawShapesWithImageSharp/Program.cs | 23 ++- src/SixLabors.Shapes/InternalPath.cs | 7 +- src/SixLabors.Shapes/Outliner.cs | 176 ++++++++++++++++++ 4 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 src/SixLabors.Shapes/Outliner.cs diff --git a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj index af49a61..58b5958 100644 --- a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj +++ b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs index daa9663..2988ec6 100644 --- a/samples/DrawShapesWithImageSharp/Program.cs +++ b/samples/DrawShapesWithImageSharp/Program.cs @@ -33,9 +33,28 @@ namespace SixLabors.Shapes.DrawShapesWithImageSharp DrawSerializedOPenSansLetterShape_a(); DrawSerializedOPenSansLetterShape_o(); - DrawFatL(); + OutlineFatL(); + OutlineSquare(); } + private static void OutlineFatL() + { + var shape = new Path(new LinearLineSegment(new Vector2(8, 8), + new Vector2(64, 8), + new Vector2(64, 64), + new Vector2(120, 64), + new Vector2(120, 120), + new Vector2(8, 120))).GenerateOutline(3, new[] { 2f, 1f }); + shape.SaveImage("Outline", "ClippedCorner.png"); + } + + private static void OutlineSquare() + { + var p = new Rectangle(new Vector2(10, 10), new Size(30, 30)).GenerateOutline(3, new[] { 2f, 1f }); + p.SaveImage("Outline", "Square.png"); + } + + private static void DrawFatL() { var shape = new Polygon(new LinearLineSegment(new Vector2(8, 8), @@ -80,7 +99,7 @@ namespace SixLabors.Shapes.DrawShapesWithImageSharp .ToArray(); return new Polygon(new LinearLineSegment(points)); }).ToArray(); - var complex = new ComplexPolygon(polys); + var complex = new ComplexPolygon(polys).Scale(10).GenerateOutline(6, new float[] { 2, 1 }); ; complex.SaveImage("letter", "o.png"); } diff --git a/src/SixLabors.Shapes/InternalPath.cs b/src/SixLabors.Shapes/InternalPath.cs index c57c4c7..1e41bd7 100644 --- a/src/SixLabors.Shapes/InternalPath.cs +++ b/src/SixLabors.Shapes/InternalPath.cs @@ -282,8 +282,11 @@ namespace SixLabors.Shapes if (precaclulate[i].RemoveLastIntersectionAndSkip) { - position--; - count++; + if (position > 0) + { + position--; + count++; + } continue; } if (precaclulate[i].DoIntersect) diff --git a/src/SixLabors.Shapes/Outliner.cs b/src/SixLabors.Shapes/Outliner.cs new file mode 100644 index 0000000..34e4190 --- /dev/null +++ b/src/SixLabors.Shapes/Outliner.cs @@ -0,0 +1,176 @@ +using ClipperLib; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace SixLabors.Shapes +{ + /// + /// Path extensions to generate outlines of paths. + /// + public static class Outliner + { + private const float ScalingFactor = 1000.0f; + + /// + /// Generates a outline of the path with alternating on and off segments based on the pattern. + /// + /// the path to outline + /// The final width outline + /// The pattern made of multiples of the width. + /// A new path representing the outline. + public static IPath GenerateOutline(this IPath path, float width, float[] pattern) + { + return path.GenerateOutline(width, pattern, false); + } + + /// + /// Generates a outline of the path with alternating on and off segments based on the pattern. + /// + /// the path to outline + /// The final width outline + /// The pattern made of multiples of the width. + /// Weather the first item in the pattern is on or off. + /// A new path representing the outline. + public static IPath GenerateOutline(this IPath path, float width, float[] pattern, bool startOff) + { + if (pattern == null || pattern.Length < 2) + { + return path.GenerateOutline(width); + } + + ImmutableArray paths = path.Flatten(); + + ClipperOffset offset = new ClipperOffset(); + + List buffer = new List(3); + foreach (ISimplePath p in paths) + { + bool online = !startOff; + float targetLength = pattern[0] * width; + int patternPos = 0; + // create a new list of points representing the new outline + int pCount = p.Points.Length; + if (!p.IsClosed) + { + pCount--; + } + int i = 0; + Vector2 currentPoint = p.Points[0]; + + while (i < pCount) + { + int next = (i + 1) % p.Points.Length; + Vector2 targetPoint = p.Points[next]; + float distToNext = Vector2.Distance(currentPoint, targetPoint); + if (distToNext > targetLength) + { + // find a point between the 2 + float t = targetLength / distToNext; + + Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t); + buffer.Add(currentPoint.ToPoint()); + buffer.Add(point.ToPoint()); + // we now inset a line joining + + if (online) + { + offset.AddPath(buffer, JoinType.jtSquare, EndType.etOpenButt); + } + online = !online; + + buffer.Clear(); + + currentPoint = point; + + // next length + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * width; + + } + else if (distToNext <= targetLength) + { + buffer.Add(currentPoint.ToPoint()); + currentPoint = targetPoint; + i++; + targetLength -= distToNext; + } + } + if (buffer.Count > 0) + { + if (p.IsClosed) + { + buffer.Add(p.Points.First().ToPoint()); + } + else + { + buffer.Add(p.Points.Last().ToPoint()); + } + + if (online) + { + offset.AddPath(buffer, JoinType.jtSquare, EndType.etOpenButt); + } + online = !online; + + buffer.Clear(); + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * width; + } + } + + return ExecuteOutliner(width, offset); + } + /// + /// Generates a solid outline of the path. + /// + /// the path to outline + /// The final width outline + /// A new path representing the outline. + public static IPath GenerateOutline(this IPath path, float width) + { + ClipperOffset offset = new ClipperLib.ClipperOffset(); + + //pattern can be applied to the path by cutting it into segments + System.Collections.Immutable.ImmutableArray paths = path.Flatten(); + foreach (ISimplePath p in paths) + { + System.Collections.Immutable.ImmutableArray vectors = p.Points; + List points = new List(vectors.Length); + foreach (Vector2 v in vectors) + { + points.Add(new IntPoint(v.X * ScalingFactor, v.Y * ScalingFactor)); + } + + EndType type = p.IsClosed ? EndType.etClosedLine : EndType.etOpenButt; + + + offset.AddPath(points, JoinType.jtMiter, type); + } + + return ExecuteOutliner(width, offset); + } + + private static IPath ExecuteOutliner(float width, ClipperOffset offset) + { + List> tree = new List>(); + offset.Execute(ref tree, width * ScalingFactor / 2); + List polygons = new List(); + foreach (List pt in tree) + { + Vector2[] points = pt.Select(p => new Vector2(p.X / ScalingFactor, p.Y / ScalingFactor)).ToArray(); + polygons.Add(new Polygon(new LinearLineSegment(points))); + } + + return new ComplexPolygon(polygons.ToArray()); + } + + private static IntPoint ToPoint(this Vector2 vector) + { + return new IntPoint(vector.X * ScalingFactor, vector.Y * ScalingFactor); + } + } +}