/// <summary> /// Generates a solid outline of the path. /// </summary> /// <param name="path">the path to outline</param> /// <param name="width">The final width outline</param> /// <param name="jointStyle">The style to render the joints.</param> /// <param name="endCapStyle">The style to render the end caps of open paths (ignored on closed paths).</param> /// <returns>A new path representing the outline.</returns> public static IPath GenerateOutline(this IPath path, float width, JointStyle jointStyle = JointStyle.Square, EndCapStyle endCapStyle = EndCapStyle.Square) { var offset = new ClipperOffset() { MiterLimit = MiterOffsetDelta }; JoinType style = Convert(jointStyle); EndType openEndCapStyle = Convert(endCapStyle); // Pattern can be applied to the path by cutting it into segments IEnumerable <ISimplePath> paths = path.Flatten(); foreach (ISimplePath p in paths) { IReadOnlyList <PointF> vectors = p.Points; var points = new List <IntPoint>(vectors.Count); foreach (Vector2 v in vectors) { points.Add(new IntPoint(v.X * ScalingFactor, v.Y * ScalingFactor)); } EndType type = p.IsClosed ? EndType.etClosedLine : openEndCapStyle; offset.AddPath(points, style, type); } return(ExecuteOutliner(width, offset)); }
private void VerifyPoints(PointF[] expectedPoints, IPath path) { var simplePath = Assert.Single(path.Flatten()); Assert.True(simplePath.IsClosed); Assert.Equal(expectedPoints, simplePath.Points.ToArray()); }
public override void DrawLine(IImageProcessingContext context, float scale, IPath path) { if (Marker == null) { base.DrawLine(context, scale, path); } else { var simplePaths = path.Flatten(); PointF[] points = new PointF[2]; GraphicsOptions graphicsOptions = GraphicsOptions.Default; foreach (var simplePath in simplePaths) { for (int i = 0; i < simplePath.Points.Count - 1; i++) { PointF point0 = simplePath.Points[i]; PointF point1 = simplePath.Points[i + 1]; IPen pen = ToPen(scale, point0, point1); points[0] = point0; points[1] = point1; context.Draw(graphicsOptions, pen, points.ToPath()); } } } }
/// <summary> /// Calculates the approximate length of the path as though each segment were unrolled into a line. /// </summary> /// <param name="path">The path to compute the length for.</param> /// <returns> /// The <see cref="float"/> representing the unrolled length. /// For closed paths, the length includes an implicit closing segment. /// </returns> public static float ComputeLength(this IPath path) { float dist = 0; foreach (ISimplePath s in path.Flatten()) { ReadOnlySpan <PointF> points = s.Points.Span; if (points.Length < 2) { // Only a single point continue; } for (int i = 1; i < points.Length; i++) { dist += Vector2.Distance(points[i - 1], points[i]); } if (s.IsClosed) { dist += Vector2.Distance(points[0], points[points.Length - 1]); } } return(dist); }
/// <summary> /// Adds the path. /// </summary> /// <param name="path">The path.</param> /// <param name="clippingType">The clipping type.</param> public void AddPath(IPath path, ClippingType clippingType) { Guard.NotNull(path, nameof(path)); foreach (ISimplePath p in path.Flatten()) { this.AddPath(p, clippingType); } }
private void VerifyPoints(IPath expectedPath, IPath path) { var simplePathExpected = Assert.Single(expectedPath.Flatten()); var expectedPoints = simplePathExpected.Points.ToArray(); var simplePath = Assert.Single(path.Flatten()); Assert.True(simplePath.IsClosed); Assert.Equal(expectedPoints, simplePath.Points.ToArray()); }
private void VerifyPoints(PointF[] expectedPoints, IPath path) { Path innerPath = Assert.IsType <Path>(path); ILineSegment segment = Assert.Single(innerPath.LineSegments); CubicBezierLineSegment bezierSegment = Assert.IsType <CubicBezierLineSegment>(segment); Assert.Equal(expectedPoints, bezierSegment.ControlPoints.ToArray()); ISimplePath simplePath = Assert.Single(path.Flatten()); Assert.False(simplePath.IsClosed); }
/// <summary> /// Create a path with the segment order reversed. /// </summary> /// <param name="path">The path to reverse.</param> /// <returns>The reversed <see cref="IPath"/>.</returns> internal static IPath Reverse(this IPath path) { IEnumerable <LinearLineSegment> segments = path.Flatten().Select(p => new LinearLineSegment(p.Points.ToArray().Reverse().ToArray())); bool closed = false; if (path is ISimplePath sp) { closed = sp.IsClosed; } return(closed ? new Polygon(segments) : new Path(segments)); }
/// <summary> /// Adds the path. /// </summary> /// <param name="path">The path.</param> /// <param name="clippingType">The clipping type.</param> public void AddPath(IPath path, ClippingType clippingType) { if (path is null) { throw new ArgumentNullException(nameof(path)); } foreach (ISimplePath p in path.Flatten()) { this.AddPath(p, clippingType); } }
public static TessellatedMultipolygon Create(IPath path, MemoryAllocator memoryAllocator) { if (path is IInternalPathOwner ipo) { IReadOnlyList <InternalPath> internalPaths = ipo.GetRingsAsInternalPath(); // If we have only one ring, we can change it's orientation without negative side-effects. // Since the algorithm works best with positively-oriented polygons, // we enforce the orientation for best output quality. bool enforcePositiveOrientationOnFirstRing = internalPaths.Count == 1; var rings = new Ring[internalPaths.Count]; IMemoryOwner <PointF> pointBuffer = internalPaths[0].ExtractVertices(memoryAllocator); RepeateFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, enforcePositiveOrientationOnFirstRing); rings[0] = new Ring(pointBuffer); for (int i = 1; i < internalPaths.Count; i++) { pointBuffer = internalPaths[i].ExtractVertices(memoryAllocator); RepeateFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, false); rings[i] = new Ring(pointBuffer); } return(new TessellatedMultipolygon(rings)); } else { ReadOnlyMemory <PointF>[] points = path.Flatten().Select(sp => sp.Points).ToArray(); // If we have only one ring, we can change it's orientation without negative side-effects. // Since the algorithm works best with positively-oriented polygons, // we enforce the orientation for best output quality. bool enforcePositiveOrientationOnFirstRing = points.Length == 1; var rings = new Ring[points.Length]; rings[0] = MakeRing(points[0], enforcePositiveOrientationOnFirstRing, memoryAllocator); for (int i = 1; i < points.Length; i++) { rings[i] = MakeRing(points[i], false, memoryAllocator); } return(new TessellatedMultipolygon(rings)); }
private static void CompareToSkiaResultsImpl(TestImageProvider <Rgba32> provider, IPath shape) { using Image <Rgba32> image = provider.GetImage(); image.Mutate(c => c.Fill(Color.White, shape)); image.DebugSave(provider, "ImageSharp", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); using var bitmap = new SKBitmap(new SKImageInfo(image.Width, image.Height)); using var skPath = new SKPath(); foreach (ISimplePath loop in shape.Flatten()) { ReadOnlySpan <SKPoint> points = MemoryMarshal.Cast <PointF, SKPoint>(loop.Points.Span); skPath.AddPoly(points.ToArray()); } using var paint = new SKPaint { Style = SKPaintStyle.Fill, Color = SKColors.White, IsAntialias = true, }; using var canvas = new SKCanvas(bitmap); canvas.Clear(new SKColor(0, 0, 0)); canvas.DrawPath(skPath, paint); using var skResultImage = Image.LoadPixelData <Rgba32>(bitmap.GetPixelSpan(), image.Width, image.Height); skResultImage.DebugSave( provider, "SkiaSharp", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); ImageSimilarityReport <Rgba32, Rgba32> result = ImageComparer.Exact.CompareImagesOrFrames(image, skResultImage); throw new Exception(result.DifferencePercentageString); }
/// <summary> /// Generates a solid outline of the path. /// </summary> /// <param name="path">the path to outline</param> /// <param name="width">The final width outline</param> /// <returns>A new path representing the outline.</returns> 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 IEnumerable <ISimplePath> paths = path.Flatten(); foreach (ISimplePath p in paths) { IReadOnlyList <PointF> vectors = p.Points; List <IntPoint> points = new List <ClipperLib.IntPoint>(vectors.Count); 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.jtSquare, type); } return(ExecuteOutliner(width, offset)); }
/// <summary> /// Generates a outline of the path with alternating on and off segments based on the pattern. /// </summary> /// <param name="path">the path to outline</param> /// <param name="width">The final width outline</param> /// <param name="pattern">The pattern made of multiples of the width.</param> /// <param name="startOff">Weather the first item in the pattern is on or off.</param> /// <returns>A new path representing the outline.</returns> public static IPath GenerateOutline(this IPath path, float width, float[] pattern, bool startOff) { if (pattern == null || pattern.Length < 2) { return(path.GenerateOutline(width)); } IEnumerable <ISimplePath> paths = path.Flatten(); ClipperOffset offset = new ClipperOffset(); List <IntPoint> buffer = new List <IntPoint>(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.Count; if (!p.IsClosed) { pCount--; } int i = 0; Vector2 currentPoint = p.Points[0]; while (i < pCount) { int next = (i + 1) % p.Points.Count; 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)); }
/// <summary> /// Generates a outline of the path with alternating on and off segments based on the pattern. /// </summary> /// <param name="path">the path to outline</param> /// <param name="width">The final width outline</param> /// <param name="pattern">The pattern made of multiples of the width.</param> /// <param name="startOff">Weather the first item in the pattern is on or off.</param> /// <param name="jointStyle">The style to render the joints.</param> /// <param name="patternSectionCapStyle">The style to render between sections of the specified pattern.</param> /// <returns>A new path representing the outline.</returns> public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan <float> pattern, bool startOff, JointStyle jointStyle = JointStyle.Square, EndCapStyle patternSectionCapStyle = EndCapStyle.Butt) { if (pattern.Length < 2) { return(path.GenerateOutline(width, jointStyle: jointStyle)); } JoinType style = Convert(jointStyle); EndType patternSectionCap = Convert(patternSectionCapStyle); IEnumerable <ISimplePath> paths = path.Flatten(); var offset = new ClipperOffset() { MiterLimit = MiterOffsetDelta }; var buffer = new List <IntPoint>(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.Count; if (!p.IsClosed) { pCount--; } int i = 0; Vector2 currentPoint = p.Points[0]; while (i < pCount) { int next = (i + 1) % p.Points.Count; 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, style, patternSectionCap); } 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, style, patternSectionCap); } online = !online; buffer.Clear(); patternPos = (patternPos + 1) % pattern.Length; targetLength = pattern[patternPos] * width; } } return(ExecuteOutliner(width, offset)); }
/// <summary> /// Generates an outline of the path with alternating on and off segments based on the pattern. /// </summary> /// <param name="path">The path to outline</param> /// <param name="width">The outline width.</param> /// <param name="pattern">The pattern made of multiples of the width.</param> /// <param name="startOff">Whether the first item in the pattern is on or off.</param> /// <param name="jointStyle">The style to apply to the joints.</param> /// <param name="endCapStyle">The style to apply to the end caps.</param> /// <returns>A new <see cref="IPath"/> representing the outline.</returns> /// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception> public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan <float> pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle) { if (pattern.Length < 2) { return(path.GenerateOutline(width, jointStyle, endCapStyle)); } IEnumerable <ISimplePath> paths = path.Flatten(); ClipperOffset offset = new(MiterOffsetDelta); List <PointF> buffer = new(); foreach (ISimplePath p in paths) { bool online = !startOff; float targetLength = pattern[0] * width; int patternPos = 0; ReadOnlySpan <PointF> points = p.Points.Span; // Create a new list of points representing the new outline int pCount = points.Length; if (!p.IsClosed) { pCount--; } int i = 0; Vector2 currentPoint = points[0]; while (i < pCount) { int next = (i + 1) % points.Length; Vector2 targetPoint = 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); buffer.Add(point); // we now inset a line joining if (online) { offset.AddPath(new ReadOnlySpan <PointF>(buffer.ToArray()), jointStyle, endCapStyle); } online = !online; buffer.Clear(); currentPoint = point; // next length patternPos = (patternPos + 1) % pattern.Length; targetLength = pattern[patternPos] * width; } else if (distToNext <= targetLength) { buffer.Add(currentPoint); currentPoint = targetPoint; i++; targetLength -= distToNext; } } if (buffer.Count > 0) { if (p.IsClosed) { buffer.Add(points[0]); } else { buffer.Add(points[points.Length - 1]); } if (online) { offset.AddPath(new ReadOnlySpan <PointF>(buffer.ToArray()), jointStyle, endCapStyle); } online = !online; buffer.Clear(); patternPos = (patternPos + 1) % pattern.Length; targetLength = pattern[patternPos] * width; } } return(offset.Execute(width)); }