From b7c7e684cc18958bb935336de4fd613a11cbb71f Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sat, 21 Jan 2017 16:15:29 +0000 Subject: [PATCH] add shape builder --- src/Shaper2D/BezierLineSegment.cs | 44 +- src/Shaper2D/ComplexPolygon.cs | 98 ++- src/Shaper2D/ILineSegment.cs | 17 +- src/Shaper2D/IPath.cs | 10 +- src/Shaper2D/IShape.cs | 7 + src/Shaper2D/InternalPath.cs | 4 +- src/Shaper2D/LinearLineSegment.cs | 38 +- src/Shaper2D/Path.cs | 30 +- src/Shaper2D/Point.cs | 17 +- src/Shaper2D/Polygon.cs | 55 +- src/Shaper2D/PolygonClipper/ClipableShape.cs | 45 ++ src/Shaper2D/PolygonClipper/Clipper.cs | 654 +++++++++--------- .../{PolyType.cs => ClippingType.cs} | 8 +- src/Shaper2D/PolygonClipper/Edge.cs | 2 +- src/Shaper2D/PolygonClipper/OutRec.cs | 19 +- src/Shaper2D/PolygonClipper/PolyNode.cs | 63 -- src/Shaper2D/PolygonClipper/PolyTree.cs | 77 --- src/Shaper2D/Rectangle.cs | 75 +- src/Shaper2D/ShapeBuilder.cs | 159 +++++ .../Shaper2D.Tests/BezierLineSegmentTests.cs | 2 +- .../Shaper2D.Tests/LinearLineSegmentTests.cs | 7 +- tests/Shaper2D.Tests/PathTests.cs | 2 +- .../PolygonClipper/ClipperTests.cs | 16 +- tests/Shaper2D.Tests/PolygonTests.cs | 2 +- tests/Shaper2D.Tests/RectangleTests.cs | 24 +- 25 files changed, 935 insertions(+), 540 deletions(-) create mode 100644 src/Shaper2D/PolygonClipper/ClipableShape.cs rename src/Shaper2D/PolygonClipper/{PolyType.cs => ClippingType.cs} (66%) delete mode 100644 src/Shaper2D/PolygonClipper/PolyNode.cs delete mode 100644 src/Shaper2D/PolygonClipper/PolyTree.cs create mode 100644 src/Shaper2D/ShapeBuilder.cs diff --git a/src/Shaper2D/BezierLineSegment.cs b/src/Shaper2D/BezierLineSegment.cs index 6633d98..d903971 100644 --- a/src/Shaper2D/BezierLineSegment.cs +++ b/src/Shaper2D/BezierLineSegment.cs @@ -5,6 +5,7 @@ namespace Shaper2D { + using System; using System.Collections.Immutable; using System.Linq; using System.Numerics; @@ -26,6 +27,7 @@ namespace Shaper2D /// The line points. /// private readonly ImmutableArray linePoints; + private readonly Point[] controlPoints; /// /// Initializes a new instance of the class. @@ -36,7 +38,16 @@ namespace Shaper2D Guard.NotNull(points, nameof(points)); Guard.MustBeGreaterThanOrEqualTo(points.Length, 4, nameof(points)); + int correctPointCount = (points.Length - 1) % 3; + if (correctPointCount != 0) + { + throw new ArgumentOutOfRangeException(nameof(points), "points must be a multiple of 3 plus 1 long."); + } + + this.controlPoints = points.ToArray(); this.linePoints = this.GetDrawingPoints(points); + + this.EndPoint = points[points.Length - 1]; } /// @@ -52,17 +63,48 @@ namespace Shaper2D { } + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public Point EndPoint { get; private set; } + /// /// Returns the current a simple linear path. /// /// /// Returns the current as simple linear path. /// - public ImmutableArray AsSimpleLinearPath() + public ImmutableArray Flatten() { return this.linePoints; } + /// + /// Transforms the current LineSegment using specified matrix. + /// + /// The matrix. + /// A line segment with the matrix applied to it. + public ILineSegment Transform(Matrix3x2 matrix) + { + if (matrix.IsIdentity) + { + // no transform to apply skip it + return this; + } + + var points = new Point[this.controlPoints.Length]; + var i = 0; + foreach (var p in this.controlPoints) + { + points[i++] = p.Transform(matrix); + } + + return new BezierLineSegment(points); + } + /// /// Returns the drawing points along the line. /// diff --git a/src/Shaper2D/ComplexPolygon.cs b/src/Shaper2D/ComplexPolygon.cs index 1e96b69..0aa2569 100644 --- a/src/Shaper2D/ComplexPolygon.cs +++ b/src/Shaper2D/ComplexPolygon.cs @@ -15,8 +15,7 @@ namespace Shaper2D using PolygonClipper; /// - /// Represents a complex polygon made up of one or more outline - /// polygons and one or more holes to punch out of them. + /// Represents a complex polygon made up of one or more shapes overlayed on each other, where overlaps causes holes. /// /// public sealed class ComplexPolygon : IShape @@ -26,31 +25,53 @@ namespace Shaper2D private ImmutableArray paths; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The outline. - /// The holes. - public ComplexPolygon(IShape outline, params IShape[] holes) - : this(new[] { outline }, holes) + /// The shapes. + public ComplexPolygon(params IShape[] shapes) { - } + Guard.NotNull(shapes, nameof(shapes)); + Guard.MustBeGreaterThanOrEqualTo(shapes.Length, 1, nameof(shapes)); - /// - /// Initializes a new instance of the class. - /// - /// The outlines. - /// The holes. - public ComplexPolygon(IShape[] outlines, IShape[] holes) - { - Guard.NotNull(outlines, nameof(outlines)); - Guard.MustBeGreaterThanOrEqualTo(outlines.Length, 1, nameof(outlines)); + this.shapes = shapes; + var pathCount = shapes.Sum(x => x.Paths.Length); + var paths = new IPath[pathCount]; + int index = 0; - this.MaxIntersections = this.FixAndSetShapes(outlines, holes); + float minX = float.MaxValue; + float maxX = float.MinValue; + float minY = float.MaxValue; + float maxY = float.MinValue; - float minX = this.shapes.Min(x => x.Bounds.Left); - float maxX = this.shapes.Max(x => x.Bounds.Right); - float minY = this.shapes.Min(x => x.Bounds.Top); - float maxY = this.shapes.Max(x => x.Bounds.Bottom); + foreach (var s in shapes) + { + if (s.Bounds.Left < minX) + { + minX = s.Bounds.Left; + } + + if (s.Bounds.Right > maxX) + { + maxX = s.Bounds.Right; + } + + if (s.Bounds.Top < minY) + { + minY = s.Bounds.Top; + } + + if (s.Bounds.Bottom > maxY) + { + maxY = s.Bounds.Bottom; + } + + foreach (var p in s.Paths) + { + paths[index++] = p; + } + } + + this.paths = ImmutableArray.Create(paths); this.Bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY); } @@ -189,24 +210,29 @@ namespace Shaper2D } } - private int FixAndSetShapes(IEnumerable outlines, IEnumerable holes) + /// + /// Transforms the shape using the specified matrix. + /// + /// The matrix. + /// + /// A new shape with the matrix applied to it. + /// + public IShape Transform(Matrix3x2 matrix) { - Clipper clipper = new Clipper(); - - // add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses - clipper.AddPaths(outlines, PolyType.Subject); - clipper.AddPaths(holes, PolyType.Clip); - - this.shapes = clipper.Execute(); - this.paths = ImmutableArray.Create(this.shapes.SelectMany(x => x.Paths).ToArray()); - - int intersections = 0; - foreach (IShape s in this.shapes) + if (matrix.IsIdentity) { - intersections += s.MaxIntersections; + // no transform to apply skip it + return this; } - return intersections; + var shapes = new IShape[this.shapes.Length]; + var i = 0; + foreach (var s in this.shapes) + { + shapes[i++] = s.Transform(matrix); + } + + return new ComplexPolygon(shapes); } } } \ No newline at end of file diff --git a/src/Shaper2D/ILineSegment.cs b/src/Shaper2D/ILineSegment.cs index eba73a3..056c951 100644 --- a/src/Shaper2D/ILineSegment.cs +++ b/src/Shaper2D/ILineSegment.cs @@ -13,10 +13,25 @@ namespace Shaper2D /// public interface ILineSegment { + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + Point EndPoint { get; } + /// /// Converts the into a simple linear path.. /// /// Returns the current as simple linear path. - ImmutableArray AsSimpleLinearPath(); // TODO move this over to ReadonlySpan once available + ImmutableArray Flatten(); + + /// + /// Transforms the current LineSegment using specified matrix. + /// + /// The matrix. + /// A line segment with the matrix applied to it. + ILineSegment Transform(Matrix3x2 matrix); } } diff --git a/src/Shaper2D/IPath.cs b/src/Shaper2D/IPath.cs index 2fc4ce9..612aa01 100644 --- a/src/Shaper2D/IPath.cs +++ b/src/Shaper2D/IPath.cs @@ -6,6 +6,7 @@ namespace Shaper2D { using System.Collections.Immutable; + using System.Numerics; /// /// Represents a logic path that can be drawn @@ -41,6 +42,13 @@ namespace Shaper2D /// Converts the into a simple linear path.. /// /// Returns the current as simple linear path. - ImmutableArray AsSimpleLinearPath(); + ImmutableArray Flatten(); + + /// + /// Transforms the path using the specified matrix. + /// + /// The matrix. + /// A new path with the matrix applied to it. + IPath Transform(Matrix3x2 matrix); } } diff --git a/src/Shaper2D/IShape.cs b/src/Shaper2D/IShape.cs index e858ace..a6f5085 100644 --- a/src/Shaper2D/IShape.cs +++ b/src/Shaper2D/IShape.cs @@ -78,5 +78,12 @@ namespace Shaper2D /// The end. /// The locations along the line segment that intersect with the edges of the shape. IEnumerable FindIntersections(Point start, Point end); + + /// + /// Transforms the shape using the specified matrix. + /// + /// The matrix. + /// A new shape with the matrix applied to it. + IShape Transform(Matrix3x2 matrix); } } diff --git a/src/Shaper2D/InternalPath.cs b/src/Shaper2D/InternalPath.cs index 509712c..fb2f755 100644 --- a/src/Shaper2D/InternalPath.cs +++ b/src/Shaper2D/InternalPath.cs @@ -67,7 +67,7 @@ namespace Shaper2D /// The segment. /// if set to true [is closed path]. internal InternalPath(ILineSegment segment, bool isClosedPath) - : this(segment.AsSimpleLinearPath(), isClosedPath) + : this(segment.Flatten(), isClosedPath) { } @@ -395,7 +395,7 @@ namespace Shaper2D List simplified = new List(); foreach (ILineSegment seg in segments) { - simplified.AddRange(seg.AsSimpleLinearPath()); + simplified.AddRange(seg.Flatten()); } return simplified.ToImmutableArray(); diff --git a/src/Shaper2D/LinearLineSegment.cs b/src/Shaper2D/LinearLineSegment.cs index 2228004..51fdc0e 100644 --- a/src/Shaper2D/LinearLineSegment.cs +++ b/src/Shaper2D/LinearLineSegment.cs @@ -5,6 +5,7 @@ namespace Shaper2D { + using System; using System.Collections.Immutable; using System.Linq; using System.Numerics; @@ -51,17 +52,52 @@ namespace Shaper2D Guard.MustBeGreaterThanOrEqualTo(points.Count(), 2, nameof(points)); this.points = ImmutableArray.Create(points); + + this.EndPoint = points[points.Length - 1]; } + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public Point EndPoint { get; private set; } + /// /// Converts the into a simple linear path.. /// /// /// Returns the current as simple linear path. /// - public ImmutableArray AsSimpleLinearPath() + public ImmutableArray Flatten() { return this.points; } + + /// + /// Transforms the current LineSegment using specified matrix. + /// + /// The matrix. + /// + /// A line segment with the matrix applied to it. + /// + public ILineSegment Transform(Matrix3x2 matrix) + { + if (matrix.IsIdentity) + { + // no transform to apply skip it + return this; + } + + var points = new Point[this.points.Length]; + var i = 0; + foreach (var p in this.points) + { + points[i++] = p.Transform(matrix); + } + + return new LinearLineSegment(points); + } } } \ No newline at end of file diff --git a/src/Shaper2D/Path.cs b/src/Shaper2D/Path.cs index 2f675ec..60188cc 100644 --- a/src/Shaper2D/Path.cs +++ b/src/Shaper2D/Path.cs @@ -26,8 +26,17 @@ namespace Shaper2D public Path(params ILineSegment[] segment) { this.innerPath = new InternalPath(segment, false); + this.LineSegments = ImmutableArray.Create(segment); } + /// + /// Gets the line segments. + /// + /// + /// The line segments. + /// + public ImmutableArray LineSegments { get; } + /// public Rectangle Bounds => this.innerPath.Bounds; @@ -35,7 +44,7 @@ namespace Shaper2D public float Length => this.innerPath.Length; /// - public ImmutableArray AsSimpleLinearPath() + public ImmutableArray Flatten() { return this.innerPath.Points; } @@ -45,5 +54,24 @@ namespace Shaper2D { return this.innerPath.DistanceFromPath(point); } + + /// + /// Transforms the rectangle using specified matrix. + /// + /// The matrix. + /// + /// A new path with the matrix applied to it. + /// + public IPath Transform(Matrix3x2 matrix) + { + var segments = new ILineSegment[this.LineSegments.Length]; + var i = 0; + foreach (var s in this.LineSegments) + { + segments[i++] = s.Transform(matrix); + } + + return new Path(segments); + } } } \ No newline at end of file diff --git a/src/Shaper2D/Point.cs b/src/Shaper2D/Point.cs index 37e41f1..dae92c6 100644 --- a/src/Shaper2D/Point.cs +++ b/src/Shaper2D/Point.cs @@ -21,10 +21,15 @@ namespace Shaper2D public struct Point : IEquatable { /// - /// Represents a that has X and Y values set to zero. + /// Represents an unset . /// public static readonly Point Empty = default(Point); + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly Point Zero = new Point(0, 0); + private readonly Vector2 backingVector; private bool isSet; @@ -186,6 +191,16 @@ namespace Shaper2D return new Point(this.backingVector + p.ToVector2()); } + /// + /// Applies the specified matrix to this point + /// + /// The matrix. + /// A new point with the transofrm applied upon it. + public Point Transform(Matrix3x2 matrix) + { + return new Point(Vector2.Transform(this.backingVector, matrix)); + } + /// public override int GetHashCode() { diff --git a/src/Shaper2D/Polygon.cs b/src/Shaper2D/Polygon.cs index b630c16..b78e4f7 100644 --- a/src/Shaper2D/Polygon.cs +++ b/src/Shaper2D/Polygon.cs @@ -25,6 +25,7 @@ namespace Shaper2D /// The segments. public Polygon(params ILineSegment[] segments) { + this.LineSegments = ImmutableArray.Create(segments); this.innerPath = new InternalPath(segments, true); this.pathCollection = ImmutableArray.Create(this); } @@ -35,10 +36,19 @@ namespace Shaper2D /// The segment. public Polygon(ILineSegment segment) { + this.LineSegments = ImmutableArray.Create(segment); this.innerPath = new InternalPath(segment, true); this.pathCollection = ImmutableArray.Create(this); } + /// + /// Gets the line segments. + /// + /// + /// The line segments. + /// + public ImmutableArray LineSegments { get; } + /// /// Gets the bounding box of this shape. /// @@ -119,7 +129,7 @@ namespace Shaper2D /// /// Returns the current as simple linear path. /// - public ImmutableArray AsSimpleLinearPath() + public ImmutableArray Flatten() { return this.innerPath.Points; } @@ -154,5 +164,48 @@ namespace Shaper2D { return this.innerPath.FindIntersections(start, end); } + + /// + /// Transforms the rectangle using specified matrix. + /// + /// The matrix. + /// + /// A new shape with the matrix applied to it. + /// + public IShape Transform(Matrix3x2 matrix) + { + var segments = new ILineSegment[this.LineSegments.Length]; + var i = 0; + foreach (var s in this.LineSegments) + { + segments[i++] = s.Transform(matrix); + } + + return new Polygon(segments); + } + + /// + /// Transforms the path using the specified matrix. + /// + /// The matrix. + /// + /// A new path with the matrix applied to it. + /// + IPath IPath.Transform(Matrix3x2 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + var segments = new ILineSegment[this.LineSegments.Length]; + var i = 0; + foreach (var s in this.LineSegments) + { + segments[i++] = s.Transform(matrix); + } + + return new Polygon(segments); + } } } diff --git a/src/Shaper2D/PolygonClipper/ClipableShape.cs b/src/Shaper2D/PolygonClipper/ClipableShape.cs new file mode 100644 index 0000000..6cdc233 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/ClipableShape.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D.PolygonClipper +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + /// + /// Represents a shape and its type for when clipping is applies. + /// + public struct ClipableShape + { + /// + /// Initializes a new instance of the struct. + /// + /// The shape. + /// The type. + public ClipableShape(IShape shape, ClippingType type) + { + this.Shape = shape; + this.Type = type; + } + + /// + /// Gets the shape. + /// + /// + /// The shape. + /// + public IShape Shape { get; private set; } + + /// + /// Gets the type. + /// + /// + /// The type. + /// + public ClippingType Type { get; private set; } + } +} diff --git a/src/Shaper2D/PolygonClipper/Clipper.cs b/src/Shaper2D/PolygonClipper/Clipper.cs index 84ee429..32ccfd6 100644 --- a/src/Shaper2D/PolygonClipper/Clipper.cs +++ b/src/Shaper2D/PolygonClipper/Clipper.cs @@ -7,6 +7,7 @@ namespace Shaper2D.PolygonClipper { using System; using System.Collections.Generic; + using System.Collections.Immutable; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; @@ -14,7 +15,7 @@ namespace Shaper2D.PolygonClipper /// /// Library to clip polygons. /// - internal class Clipper + public class Clipper { private const double HorizontalDeltaLimit = -3.4E+38; private const int Skip = -2; @@ -27,256 +28,34 @@ namespace Shaper2D.PolygonClipper private readonly List ghostJoins = new List(); private readonly List> edges = new List>(); private readonly List polyOuts = new List(); + private readonly object syncRoot = new object(); private Maxima maxima = null; private Edge sortedEdges = null; - private LocalMinima minimaList; private LocalMinima currentLocalMinima; private Scanbeam scanbeam = null; private Edge activeEdges = null; + private bool resultsDirty = true; + private ImmutableArray results; /// - /// Adds the paths. + /// Initializes a new instance of the class. /// - /// The path. - /// Type of the poly. - public void AddPaths(IEnumerable path, PolyType polyType) + /// The shapes. + public Clipper(IEnumerable shapes) { - foreach (var p in path) - { - this.AddPath(p, polyType); - } + Guard.NotNull(shapes, nameof(shapes)); + this.AddShapes(shapes); } /// - /// Adds the path. + /// Initializes a new instance of the class. /// - /// The path. - /// Type of the poly. - public void AddPath(IShape path, PolyType polyType) + /// The shapes. + public Clipper(params ClipableShape[] shapes) { - if (path is IPath) - { - this.AddPath((IPath)path, polyType); - } - else - { - foreach (var p in path.Paths) - { - this.AddPath(p, polyType); - } - } - } - - /// - /// Adds the path. - /// - /// The path. - /// Type of the poly. - /// True if the path was added. - /// AddPath: Open paths have been disabled. - public bool AddPath(IPath path, PolyType polyType) - { - var points = path.AsSimpleLinearPath(); - - int hi = points.Length - 1; - while (hi > 0 && (points[hi] == points[0])) - { - --hi; - } - - while (hi > 0 && (points[hi] == points[hi - 1])) - { - --hi; - } - - if (hi < 2) - { - throw new ClipperException("must have more than 2 distinct points"); - } - - // create a new edge array ... - List edges = new List(hi + 1); - for (int i = 0; i <= hi; i++) - { - edges.Add(new Edge() { SourcePath = path }); - } - - bool isFlat = true; - - // 1. Basic (first) edge initialization ... - edges[1].Current = points[1]; - - InitEdge(edges[0], edges[1], edges[hi], points[0]); - InitEdge(edges[hi], edges[0], edges[hi - 1], points[hi]); - for (int i = hi - 1; i >= 1; --i) - { - InitEdge(edges[i], edges[i + 1], edges[i - 1], points[i]); - } - - Edge startEdge = edges[0]; - - // 2. Remove duplicate vertices, and (when closed) collinear edges ... - Edge edge = startEdge; - Edge loopStop = startEdge; - while (true) - { - if (edge.Current == edge.NextEdge.Current) - { - //remove unneeded edges - if (edge == edge.NextEdge) - { - break; - } - - if (edge == startEdge) - { - startEdge = edge.NextEdge; - } - - edge = RemoveEdge(edge); - loopStop = edge; - continue; - } - - if (SlopesEqual(edge.PreviousEdge.Current, edge.Current, edge.NextEdge.Current)) - { - // 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 (edge == startEdge) - { - startEdge = edge.NextEdge; - } - - edge = RemoveEdge(edge); - edge = edge.PreviousEdge; - loopStop = edge; - continue; - } - - edge = edge.NextEdge; - if (edge == loopStop) - { - break; - } - } - - if (edge.PreviousEdge == edge.NextEdge) - { - return false; - } - - // 3. Do second stage of edge initialization ... - edge = startEdge; - do - { - this.InitEdge2(edge, polyType); - edge = edge.NextEdge; - if (isFlat && edge.Current.Y != startEdge.Current.Y) - { - isFlat = false; - } - } - while (edge != startEdge); - - // 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) - { - return false; - } - - this.edges.Add(edges); - Edge loopBreakerEdge = null; - - // workaround to avoid an endless loop in the while loop below when - // open paths have matching start and end points ... - if (edge.PreviousEdge.Bottom == edge.PreviousEdge.Top) - { - edge = edge.NextEdge; - } - - while (true) - { - edge = FindNextLocMin(edge); - if (edge == loopBreakerEdge) - { - break; - } - else if (loopBreakerEdge == null) - { - loopBreakerEdge = edge; - } - - // E and E.Prev now share a local minima (left aligned if horizontal). - // Compare their slopes to find which starts which bound ... - LocalMinima locMin = new LocalMinima - { - Next = null, - Y = edge.Bottom.Y - }; - - bool leftBoundIsForward; - if (edge.Dx < edge.PreviousEdge.Dx) - { - locMin.LeftBound = edge.PreviousEdge; - locMin.RightBound = edge; - leftBoundIsForward = false; // Q.nextInLML = Q.prev - } - else - { - locMin.LeftBound = edge; - locMin.RightBound = edge.PreviousEdge; - leftBoundIsForward = true; // Q.nextInLML = Q.next - } - - locMin.LeftBound.Side = EdgeSide.Left; - locMin.RightBound.Side = EdgeSide.Right; - - if (locMin.LeftBound.NextEdge == locMin.RightBound) - { - locMin.LeftBound.WindingDelta = -1; - } - else - { - locMin.LeftBound.WindingDelta = 1; - } - - locMin.RightBound.WindingDelta = -locMin.LeftBound.WindingDelta; - - edge = this.ProcessBound(locMin.LeftBound, leftBoundIsForward); - if (edge.OutIndex == Skip) - { - edge = this.ProcessBound(edge, leftBoundIsForward); - } - - Edge edge2 = this.ProcessBound(locMin.RightBound, !leftBoundIsForward); - if (edge2.OutIndex == Skip) - { - edge2 = this.ProcessBound(edge2, !leftBoundIsForward); - } - - if (locMin.LeftBound.OutIndex == Skip) - { - locMin.LeftBound = null; - } - else if (locMin.RightBound.OutIndex == Skip) - { - locMin.RightBound = null; - } - - this.InsertLocalMinima(locMin); - if (!leftBoundIsForward) - { - edge = edge2; - } - } - - return true; + this.AddShapes(shapes); } /// @@ -285,18 +64,299 @@ namespace Shaper2D.PolygonClipper /// /// Returns the array containing the converted polygons. /// - public IShape[] Execute() + public ImmutableArray GenerateClippedShapes() { - PolyTree polytree = new PolyTree(); - bool succeeded = this.ExecuteInternal(); - - // build the return polygons ... - if (succeeded) + if (!this.resultsDirty) { - this.BuildResult2(polytree); + return this.results; } - return ExtractOutlines(polytree).ToArray(); + lock (this.syncRoot) + { + if (!this.resultsDirty) + { + return this.results; + } + + try + { + bool succeeded = this.ExecuteInternal(); + + // build the return polygons ... + if (succeeded) + { + this.results = this.BuildResult(); + this.resultsDirty = false; + } + } + finally + { + this.DisposeAllPolyPoints(); + } + + return this.results; + } + } + + /// + /// Adds the paths. + /// + /// The clipable shaps. + public void AddShapes(IEnumerable clipableShaps) + { + foreach (var p in clipableShaps) + { + this.AddShape(p.Shape, p.Type); + } + } + + /// + /// Adds the shapes. + /// + /// The shapes. + /// The clipping type. + public void AddShapes(IEnumerable shapes, ClippingType clippingType) + { + foreach (var p in shapes) + { + this.AddShape(p, clippingType); + } + } + + /// + /// Adds the path. + /// + /// The shape. + /// The clipping type. + internal void AddShape(IShape shape, ClippingType clippingType) + { + if (shape is IPath) + { + this.AddPath((IPath)shape, clippingType); + } + else + { + foreach (var p in shape.Paths) + { + this.AddPath(p, clippingType); + } + } + } + + /// + /// Adds the path. + /// + /// The path. + /// Type of the poly. + /// True if the path was added. + /// AddPath: Open paths have been disabled. + internal bool AddPath(IPath path, ClippingType clippingType) + { + // every path we add lock the clipper to prevent state curruption + lock (this.syncRoot) + { + this.resultsDirty = true; + + var points = path.Flatten(); + + int hi = points.Length - 1; + while (hi > 0 && (points[hi] == points[0])) + { + --hi; + } + + while (hi > 0 && (points[hi] == points[hi - 1])) + { + --hi; + } + + if (hi < 2) + { + throw new ClipperException("must have more than 2 distinct points"); + } + + // create a new edge array ... + List edges = new List(hi + 1); + for (int i = 0; i <= hi; i++) + { + edges.Add(new Edge() { SourcePath = path }); + } + + bool isFlat = true; + + // 1. Basic (first) edge initialization ... + edges[1].Current = points[1]; + + InitEdge(edges[0], edges[1], edges[hi], points[0]); + InitEdge(edges[hi], edges[0], edges[hi - 1], points[hi]); + for (int i = hi - 1; i >= 1; --i) + { + InitEdge(edges[i], edges[i + 1], edges[i - 1], points[i]); + } + + Edge startEdge = edges[0]; + + // 2. Remove duplicate vertices, and (when closed) collinear edges ... + Edge edge = startEdge; + Edge loopStop = startEdge; + while (true) + { + if (edge.Current == edge.NextEdge.Current) + { + // remove unneeded edges + if (edge == edge.NextEdge) + { + break; + } + + if (edge == startEdge) + { + startEdge = edge.NextEdge; + } + + edge = RemoveEdge(edge); + loopStop = edge; + continue; + } + + if (SlopesEqual(edge.PreviousEdge.Current, edge.Current, edge.NextEdge.Current)) + { + // 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 (edge == startEdge) + { + startEdge = edge.NextEdge; + } + + edge = RemoveEdge(edge); + edge = edge.PreviousEdge; + loopStop = edge; + continue; + } + + edge = edge.NextEdge; + if (edge == loopStop) + { + break; + } + } + + if (edge.PreviousEdge == edge.NextEdge) + { + return false; + } + + // 3. Do second stage of edge initialization ... + edge = startEdge; + do + { + this.InitEdge2(edge, clippingType); + edge = edge.NextEdge; + if (isFlat && edge.Current.Y != startEdge.Current.Y) + { + isFlat = false; + } + } + while (edge != startEdge); + + // 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) + { + return false; + } + + this.edges.Add(edges); + Edge loopBreakerEdge = null; + + // workaround to avoid an endless loop in the while loop below when + // open paths have matching start and end points ... + if (edge.PreviousEdge.Bottom == edge.PreviousEdge.Top) + { + edge = edge.NextEdge; + } + + while (true) + { + edge = FindNextLocMin(edge); + if (edge == loopBreakerEdge) + { + break; + } + else if (loopBreakerEdge == null) + { + loopBreakerEdge = edge; + } + + // E and E.Prev now share a local minima (left aligned if horizontal). + // Compare their slopes to find which starts which bound ... + LocalMinima locMin = new LocalMinima + { + Next = null, + Y = edge.Bottom.Y + }; + + bool leftBoundIsForward; + if (edge.Dx < edge.PreviousEdge.Dx) + { + locMin.LeftBound = edge.PreviousEdge; + locMin.RightBound = edge; + leftBoundIsForward = false; // Q.nextInLML = Q.prev + } + else + { + locMin.LeftBound = edge; + locMin.RightBound = edge.PreviousEdge; + leftBoundIsForward = true; // Q.nextInLML = Q.next + } + + locMin.LeftBound.Side = EdgeSide.Left; + locMin.RightBound.Side = EdgeSide.Right; + + if (locMin.LeftBound.NextEdge == locMin.RightBound) + { + locMin.LeftBound.WindingDelta = -1; + } + else + { + locMin.LeftBound.WindingDelta = 1; + } + + locMin.RightBound.WindingDelta = -locMin.LeftBound.WindingDelta; + + edge = this.ProcessBound(locMin.LeftBound, leftBoundIsForward); + if (edge.OutIndex == Skip) + { + edge = this.ProcessBound(edge, leftBoundIsForward); + } + + Edge edge2 = this.ProcessBound(locMin.RightBound, !leftBoundIsForward); + if (edge2.OutIndex == Skip) + { + edge2 = this.ProcessBound(edge2, !leftBoundIsForward); + } + + if (locMin.LeftBound.OutIndex == Skip) + { + locMin.LeftBound = null; + } + else if (locMin.RightBound.OutIndex == Skip) + { + locMin.RightBound = null; + } + + this.InsertLocalMinima(locMin); + if (!leftBoundIsForward) + { + edge = edge2; + } + } + + return true; + } } private static float Round(double value) @@ -314,37 +374,6 @@ namespace Shaper2D.PolygonClipper return edge.Bottom.X + Round(edge.Dx * (currentY - edge.Bottom.Y)); } - private static List ExtractOutlines(PolyNode tree) - { - var result = new List(); - ExtractOutlines(tree, result); - return result; - } - - private static void ExtractOutlines(PolyNode tree, List shapes) - { - if (tree.Contour.Any()) - { - // if the source path is set then we clipper retained the full path intact thus we can freely - // use it and get any shape optimizations that are available. - if (tree.SourcePath != null) - { - shapes.Add((IShape)tree.SourcePath); - } - else - { - Polygon polygon = new Polygon(new LinearLineSegment(tree.Contour.Select(x => new Point(x)).ToArray())); - - shapes.Add(polygon); - } - } - - foreach (PolyNode c in tree.Children) - { - ExtractOutlines(c, shapes); - } - } - private static void FixHoleLinkage(OutRec outRec) { // skip if an outermost polygon or @@ -621,6 +650,16 @@ namespace Shaper2D.PolygonClipper // Swap(ref e.Top.X, ref e.Bot.X); } + private void DisposeAllPolyPoints() + { + foreach (var polyout in this.polyOuts) + { + polyout.Points = null; + } + + this.polyOuts.Clear(); + } + private bool ExecuteInternal() { try @@ -886,7 +925,7 @@ namespace Shaper2D.PolygonClipper return false; } - if (edge.PolyType == PolyType.Subject) + if (edge.PolyType == ClippingType.Subject) { return edge.WindingCountInOppositePolyType == 0; } @@ -1659,8 +1698,8 @@ namespace Shaper2D.PolygonClipper } else if (e1Wc == 1 && e2Wc == 1) { - if (((e1.PolyType == PolyType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || - ((e1.PolyType == PolyType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + if (((e1.PolyType == ClippingType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1.PolyType == ClippingType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) { this.AddLocalMinPoly(e1, e2, pt); } @@ -2380,12 +2419,11 @@ namespace Shaper2D.PolygonClipper return result; } - private void BuildResult2(PolyTree polytree) + private ImmutableArray BuildResult() { - polytree.Clear(); + List shapes = new List(this.polyOuts.Count); // add each output polygon/contour to polytree ... - polytree.AllPolygonNodes.Capacity = this.polyOuts.Count; for (int i = 0; i < this.polyOuts.Count; i++) { OutRec outRec = this.polyOuts[i]; @@ -2397,42 +2435,26 @@ namespace Shaper2D.PolygonClipper } FixHoleLinkage(outRec); - PolyNode pn = new PolyNode(); - pn.SourcePath = outRec.SourcePath; - polytree.AllPolygonNodes.Add(pn); - outRec.PolyNode = pn; - pn.Contour.Capacity = cnt; - OutPoint op = outRec.Points.Previous; - for (int j = 0; j < cnt; j++) + var shape = outRec.SourcePath as IShape; + if (shape != null) { - pn.Contour.Add(op.Point); - op = op.Previous; - } - } - - // fixup PolyNode links etc ... - polytree.Children.Capacity = this.polyOuts.Count; - for (int i = 0; i < this.polyOuts.Count; i++) - { - OutRec outRec = this.polyOuts[i]; - if (outRec.PolyNode == null) - { - continue; - } - else if (outRec.IsOpen) - { - polytree.AddChild(outRec.PolyNode); - } - else if (outRec.FirstLeft != null && - outRec.FirstLeft.PolyNode != null) - { - outRec.FirstLeft.PolyNode.AddChild(outRec.PolyNode); + shapes.Add(shape); } else { - polytree.AddChild(outRec.PolyNode); + var points = new Point[cnt]; + OutPoint op = outRec.Points.Previous; + for (int j = 0; j < cnt; j++) + { + points[i] = op.Point; + op = op.Previous; + } + + shapes.Add(new Polygon(new LinearLineSegment(points))); } } + + return shapes.ToImmutableArray(); } private void FixupOutPolyline(OutRec outrec) @@ -3305,16 +3327,8 @@ namespace Shaper2D.PolygonClipper private OutRec CreateOutRec() { - OutRec result = new OutRec(); - result.Index = Unassigned; - result.IsHole = false; - result.IsOpen = false; - result.FirstLeft = null; - result.Points = null; - result.BottomPoint = null; - result.PolyNode = null; + var result = new OutRec(this.polyOuts.Count); this.polyOuts.Add(result); - result.Index = this.polyOuts.Count - 1; return result; } @@ -3469,7 +3483,7 @@ namespace Shaper2D.PolygonClipper e.PreviousInAEL = null; } - private void InitEdge2(Edge e, PolyType polyType) + private void InitEdge2(Edge e, ClippingType polyType) { if (e.Current.Y >= e.NextEdge.Current.Y) { diff --git a/src/Shaper2D/PolygonClipper/PolyType.cs b/src/Shaper2D/PolygonClipper/ClippingType.cs similarity index 66% rename from src/Shaper2D/PolygonClipper/PolyType.cs rename to src/Shaper2D/PolygonClipper/ClippingType.cs index 5019571..bdb9d51 100644 --- a/src/Shaper2D/PolygonClipper/PolyType.cs +++ b/src/Shaper2D/PolygonClipper/ClippingType.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Scott Williams and contributors. // Licensed under the Apache License, Version 2.0. // @@ -14,15 +14,15 @@ namespace Shaper2D.PolygonClipper /// /// Poly Type /// - internal enum PolyType + public enum ClippingType { /// - /// The subject + /// Represent a main shape to act as a main subject whoes path will be clipped or merged. /// Subject, /// - /// The clip + /// Represents a shape to act and a clipped path. /// Clip } diff --git a/src/Shaper2D/PolygonClipper/Edge.cs b/src/Shaper2D/PolygonClipper/Edge.cs index f9c4c9e..07d6081 100644 --- a/src/Shaper2D/PolygonClipper/Edge.cs +++ b/src/Shaper2D/PolygonClipper/Edge.cs @@ -73,7 +73,7 @@ namespace Shaper2D.PolygonClipper /// /// The poly type. /// - public PolyType PolyType { get; set; } + public ClippingType PolyType { get; set; } /// /// Gets or sets the side. diff --git a/src/Shaper2D/PolygonClipper/OutRec.cs b/src/Shaper2D/PolygonClipper/OutRec.cs index 72f610b..d709365 100644 --- a/src/Shaper2D/PolygonClipper/OutRec.cs +++ b/src/Shaper2D/PolygonClipper/OutRec.cs @@ -17,6 +17,20 @@ namespace Shaper2D.PolygonClipper /// internal class OutRec { + /// + /// Initializes a new instance of the class. + /// + /// The index. + public OutRec(int index) + { + this.Index = index; + this.IsHole = false; + this.IsOpen = false; + this.FirstLeft = null; + this.Points = null; + this.BottomPoint = null; + } + /// /// Gets or sets the source path /// @@ -60,10 +74,5 @@ namespace Shaper2D.PolygonClipper /// The bottom point. /// public OutPoint BottomPoint { get; set; } - - /// - /// Gets or sets the poly node - /// - public PolyNode PolyNode { get; set; } } } \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/PolyNode.cs b/src/Shaper2D/PolygonClipper/PolyNode.cs deleted file mode 100644 index a71e30d..0000000 --- a/src/Shaper2D/PolygonClipper/PolyNode.cs +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright (c) Scott Williams and contributors. -// Licensed under the Apache License, Version 2.0. -// - -namespace Shaper2D.PolygonClipper -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Numerics; - using System.Runtime.CompilerServices; - - /// - /// Poly Node - /// - internal class PolyNode - { - private List children = new List(); - - private List polygon = new List(); - - /// - /// Gets or sets the index - /// - public int Index { get; set; } - - /// - /// Gets the contour. - /// - /// - /// The contour. - /// - public List Contour => this.polygon; - - /// - /// Gets the children. - /// - /// - /// The children. - /// - public List Children => this.children; - - /// - /// Gets or sets the source path. - /// - /// - /// The source path. - /// - public IPath SourcePath { get; internal set; } - - /// - /// Adds the child. - /// - /// The child. - internal void AddChild(PolyNode child) - { - int cnt = this.children.Count; - this.children.Add(child); - child.Index = cnt; - } - } -} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/PolyTree.cs b/src/Shaper2D/PolygonClipper/PolyTree.cs deleted file mode 100644 index 7a698be..0000000 --- a/src/Shaper2D/PolygonClipper/PolyTree.cs +++ /dev/null @@ -1,77 +0,0 @@ -// -// Copyright (c) Scott Williams and contributors. -// Licensed under the Apache License, Version 2.0. -// - -namespace Shaper2D.PolygonClipper -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Numerics; - using System.Runtime.CompilerServices; - - /// - /// Poly Tree - /// - /// - internal class PolyTree : PolyNode - { - /// - /// Gets or sets all polygon nodes. - /// - public List AllPolygonNodes { get; set; } = new List(); - - /// - /// Gets the total. - /// - /// - /// The total. - /// - public int Total - { - get - { - int result = this.AllPolygonNodes.Count; - - // with negative offsets, ignore the hidden outer polygon ... - if (result > 0 && this.Children[0] != this.AllPolygonNodes[0]) - { - result--; - } - - return result; - } - } - - /// - /// Clears this instance. - /// - public void Clear() - { - for (int i = 0; i < this.AllPolygonNodes.Count; i++) - { - this.AllPolygonNodes[i] = null; - } - - this.AllPolygonNodes.Clear(); - this.Children.Clear(); - } - - /// - /// Gets the first. - /// - /// the first node - public PolyNode GetFirst() - { - if (this.Children.Count > 0) - { - return this.Children[0]; - } - else - { - return null; - } - } - } -} diff --git a/src/Shaper2D/Rectangle.cs b/src/Shaper2D/Rectangle.cs index 1ad9105..6dc3b70 100644 --- a/src/Shaper2D/Rectangle.cs +++ b/src/Shaper2D/Rectangle.cs @@ -39,16 +39,16 @@ namespace Shaper2D } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The location. - /// The size. - public Rectangle(Point location, Size size) + /// The top left. + /// The bottom right. + public Rectangle(Point topLeft, Point bottomRight) { - this.Location = location; - this.topLeft = location; - this.bottomRight = location.Offset(size); - this.Size = size; + this.Location = topLeft; + this.topLeft = topLeft; + this.bottomRight = bottomRight; + this.Size = new Size(bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); this.points = ImmutableArray.Create(new Point[4] { @@ -58,11 +58,21 @@ namespace Shaper2D new Vector2(this.topLeft.X, this.bottomRight.Y) }); - this.halfLength = size.Width + size.Height; + this.halfLength = this.Size.Width + this.Size.Height; this.length = this.halfLength * 2; this.pathCollection = ImmutableArray.Create(this); } + /// + /// Initializes a new instance of the class. + /// + /// The location. + /// The size. + public Rectangle(Point location, Size size) + : this(location, location.Offset(size)) + { + } + /// /// Gets the location. /// @@ -79,6 +89,14 @@ namespace Shaper2D /// public float Left => this.topLeft.X; + /// + /// Gets the X. + /// + /// + /// The X. + /// + public float X => this.topLeft.X; + /// /// Gets the right. /// @@ -95,6 +113,14 @@ namespace Shaper2D /// public float Top => this.topLeft.Y; + /// + /// Gets the Y. + /// + /// + /// The Y. + /// + public float Y => this.topLeft.Y; + /// /// Gets the bottom. /// @@ -262,13 +288,42 @@ namespace Shaper2D return discovered; } + /// + /// Transforms the rectangle using specified matrix. + /// + /// The matrix. + /// + /// A new shape with the matrix applied to it. + /// + public IShape Transform(Matrix3x2 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + return new Rectangle(Vector2.Transform(this.topLeft, matrix), Vector2.Transform(this.bottomRight, matrix)); + } + + /// + /// Transforms the rectangle using specified matrix. + /// + /// The matrix. + /// + /// A new path with the matrix applied to it. + /// + IPath IPath.Transform(Matrix3x2 matrix) + { + return (IPath)this.Transform(matrix); + } + /// /// Converts the into a simple linear path.. /// /// /// Returns the current as simple linear path. /// - ImmutableArray IPath.AsSimpleLinearPath() + ImmutableArray IPath.Flatten() { return this.points; } diff --git a/src/Shaper2D/ShapeBuilder.cs b/src/Shaper2D/ShapeBuilder.cs new file mode 100644 index 0000000..a5e124b --- /dev/null +++ b/src/Shaper2D/ShapeBuilder.cs @@ -0,0 +1,159 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Numerics; + using System.Threading.Tasks; + + /// + /// Allow you to delacatrivly build shapes and paths. + /// + public class ShapeBuilder + { + private readonly Matrix3x2 defaultTransform; + private Point currentPoint = Point.Empty; + private Matrix3x2 currentTransform; + + private List figures = new List(); + private List segments = new List(); + + /// + /// Initializes a new instance of the class. + /// + public ShapeBuilder() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The default transform. + public ShapeBuilder(Matrix3x2 defaultTransform) + { + this.defaultTransform = defaultTransform; + this.ResetTransform(); + } + + /// + /// Sets the translation to be applied to all items to follow being applied to the . + /// + /// The translation. + public void SetTransform(Matrix3x2 translation) + { + this.currentTransform = translation; + } + + /// + /// Resets the translation to the default. + /// + public void ResetTransform() + { + this.currentTransform = this.defaultTransform; + } + + /// + /// Adds the line connecting the current point to the new point. + /// + /// The point. + public void AddLine(Point point) + { + var endPoint = point.Transform(this.currentTransform); + this.segments.Add(new LinearLineSegment(this.currentPoint, endPoint)); + this.currentPoint = endPoint; + } + + /// + /// Adds a series of line segments connecting the current point to the new points. + /// + /// The points. + public void AddLines(IEnumerable points) + { + foreach (var p in points) + { + this.AddLine(p); + } + } + + /// + /// Adds the segment. + /// + /// The segment. + public void AddSegment(ILineSegment segment) + { + var segments = segment.Transform(this.currentTransform); + this.segments.Add(segments); + this.currentPoint = segments.EndPoint; + } + + /// + /// Adds a bezier curve to the current figure joining the last point to the endPoint. + /// + /// The control point1. + /// The control point2. + /// The end point. + public void AddBezier(Point controlPoint1, Point controlPoint2, Point endPoint) + { + endPoint = endPoint.Transform(this.currentTransform); + this.segments.Add(new BezierLineSegment( + this.currentPoint, + controlPoint1.Transform(this.currentTransform), + controlPoint2.Transform(this.currentTransform), + endPoint)); + this.currentPoint = endPoint; + } + + /// + /// Moves the current point. + /// + /// The point. + public void MoveTo(Point point) + { + if (this.segments.Any()) + { + this.figures.Add(this.segments.ToArray()); + this.segments.Clear(); + } + + this.currentPoint = point.Transform(this.currentTransform); + } + + /// + /// Moves the current point + /// + /// The x. + /// The y. + public void MoveTo(float x, float y) + { + this.MoveTo(new Point(x, y)); + } + + /// + /// Builds a complex polygon fromn the current working set of working operations. + /// + /// The current set of operations as a complex polygon + public ComplexPolygon Build() + { + var isWorking = this.segments.Any(); + + var shapes = new IShape[this.figures.Count + (isWorking ? 1 : 0)]; + var index = 0; + foreach (var segments in this.figures) + { + shapes[index++] = new Polygon(segments); + } + + if (isWorking) + { + shapes[index++] = new Polygon(this.segments.ToArray()); + } + + return new ComplexPolygon(shapes); + } + } +} diff --git a/tests/Shaper2D.Tests/BezierLineSegmentTests.cs b/tests/Shaper2D.Tests/BezierLineSegmentTests.cs index a1d8976..d6b101f 100644 --- a/tests/Shaper2D.Tests/BezierLineSegmentTests.cs +++ b/tests/Shaper2D.Tests/BezierLineSegmentTests.cs @@ -12,7 +12,7 @@ namespace Shaper2D.Tests public void SingleSegmentConstructor() { var segment = new BezierLineSegment(new Point(0, 0), new Point(10, 0), new Point(10, 0), new Point(20, 0)); - var points = segment.AsSimpleLinearPath(); + var points = segment.Flatten(); Assert.Equal(51, points.Length); Assert.Contains(new Point(0, 0), points); Assert.Contains(new Point(10, 0), points); diff --git a/tests/Shaper2D.Tests/LinearLineSegmentTests.cs b/tests/Shaper2D.Tests/LinearLineSegmentTests.cs index b2df835..95d67cf 100644 --- a/tests/Shaper2D.Tests/LinearLineSegmentTests.cs +++ b/tests/Shaper2D.Tests/LinearLineSegmentTests.cs @@ -12,9 +12,10 @@ namespace Shaper2D.Tests public void SingleSegmentConstructor() { var segment = new LinearLineSegment(new Point(0, 0), new Point(10, 10)); - Assert.Equal(2, segment.AsSimpleLinearPath().Length); - Assert.Equal(new Point(0, 0), segment.AsSimpleLinearPath()[0]); - Assert.Equal(new Point(10, 10), segment.AsSimpleLinearPath()[1]); + var flatPath = segment.Flatten(); + Assert.Equal(2, flatPath.Length); + Assert.Equal(new Point(0, 0), flatPath[0]); + Assert.Equal(new Point(10, 10), flatPath[1]); } [Fact] diff --git a/tests/Shaper2D.Tests/PathTests.cs b/tests/Shaper2D.Tests/PathTests.cs index 3cea8e6..2766e46 100644 --- a/tests/Shaper2D.Tests/PathTests.cs +++ b/tests/Shaper2D.Tests/PathTests.cs @@ -75,7 +75,7 @@ namespace Shaper2D.Tests public void SimplePath() { var path = new Path(new LinearLineSegment(new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10))); - var points = path.AsSimpleLinearPath(); + var points = path.Flatten(); Assert.Equal(4, points.Length); Assert.Equal(new Point(0, 0), points[0]); diff --git a/tests/Shaper2D.Tests/PolygonClipper/ClipperTests.cs b/tests/Shaper2D.Tests/PolygonClipper/ClipperTests.cs index b813403..039bdce 100644 --- a/tests/Shaper2D.Tests/PolygonClipper/ClipperTests.cs +++ b/tests/Shaper2D.Tests/PolygonClipper/ClipperTests.cs @@ -19,19 +19,19 @@ namespace Shaper2D.Tests.PolygonClipper private Rectangle TopLeft = new Rectangle(0,0, 20,20); private Rectangle TopRight = new Rectangle(30,0, 20,20); - private IShape[] Clip(IPath shape, params IPath[] hole) + private ImmutableArray Clip(IPath shape, params IPath[] hole) { var clipper = new Clipper(); - clipper.AddPath(shape, PolyType.Subject); + clipper.AddPath(shape, ClippingType.Subject); if (hole != null) { foreach (var s in hole) { - clipper.AddPath(s, PolyType.Clip); + clipper.AddPath(s, ClippingType.Clip); } } - return clipper.Execute(); + return clipper.GenerateClippedShapes(); } [Fact] @@ -66,10 +66,10 @@ namespace Shaper2D.Tests.PolygonClipper { var clipper = new Clipper(); var mockPath = new Mock(); - mockPath.Setup(x => x.AsSimpleLinearPath()) + mockPath.Setup(x => x.Flatten()) .Returns(ImmutableArray.Create(new Point(0, 0), new Point(1, 1), new Point(0, 0))); - Assert.Throws(() => { clipper.AddPath(mockPath.Object, PolyType.Subject); }); + Assert.Throws(() => { clipper.AddPath(mockPath.Object, ClippingType.Subject); }); } [Fact] @@ -77,10 +77,10 @@ namespace Shaper2D.Tests.PolygonClipper { var clipper = new Clipper(); var mockPath = new Mock(); - mockPath.Setup(x => x.AsSimpleLinearPath()) + mockPath.Setup(x => x.Flatten()) .Returns(ImmutableArray.Create(new Point(0, 0), new Point(1, 1), new Point(1, 1))); - Assert.Throws(() => { clipper.AddPath(mockPath.Object, PolyType.Subject); }); + Assert.Throws(() => { clipper.AddPath(mockPath.Object, ClippingType.Subject); }); } } } diff --git a/tests/Shaper2D.Tests/PolygonTests.cs b/tests/Shaper2D.Tests/PolygonTests.cs index eb3515f..21d21cc 100644 --- a/tests/Shaper2D.Tests/PolygonTests.cs +++ b/tests/Shaper2D.Tests/PolygonTests.cs @@ -111,7 +111,7 @@ namespace Shaper2D.Tests public void AsSimpleLinearPath() { var poly = new Polygon(new LinearLineSegment(new Point(0, 0), new Point(0, 10), new Point(5, 5))); - var paths = poly.AsSimpleLinearPath(); + var paths = poly.Flatten(); Assert.Equal(3, paths.Length); Assert.Equal(new Point(0, 0), paths[0]); Assert.Equal(new Point(0, 10), paths[1]); diff --git a/tests/Shaper2D.Tests/RectangleTests.cs b/tests/Shaper2D.Tests/RectangleTests.cs index c936905..007d43e 100644 --- a/tests/Shaper2D.Tests/RectangleTests.cs +++ b/tests/Shaper2D.Tests/RectangleTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Threading.Tasks; using Xunit; @@ -175,7 +176,7 @@ namespace Shaper2D.Tests public void LienearSegements() { IPath shape = new Rectangle(10, 11, 12, 13); - var segemnts = shape.AsSimpleLinearPath(); + var segemnts = shape.Flatten(); Assert.Equal(new Point(10, 11), segemnts[0]); Assert.Equal(new Point(22, 11), segemnts[1]); Assert.Equal(new Point(22, 24), segemnts[2]); @@ -243,5 +244,26 @@ namespace Shaper2D.Tests Assert.Equal((IPath)shape, shape.Paths.Single()); } + + [Fact] + public void TransformIdnetityReturnsSahpeObject() + { + + Rectangle shape = new Rectangle(0, 0, 200, 60); + var transformdShape = shape.Transform(Matrix3x2.Identity); + + Assert.Same(shape, transformdShape); + } + + [Fact] + public void Transform() + { + Rectangle shape = new Rectangle(0, 0, 200, 60); + + var newShape = (Rectangle)shape.Transform(new Matrix3x2(0, 1, 1, 0, 20, 2)); + + Assert.Equal(new Point(20, 2), newShape.Location); + Assert.Equal(new Size(60, 200), newShape.Size); + } } }