From 10cf98fab8eaf5e8f550c2cfd4ba296aa61f6feb Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sat, 14 Jan 2017 11:40:36 +0000 Subject: [PATCH] Migrate shaper code from ImageSharp to Shaper2D --- .editorconfig | 3 + APACHE-2.0-LICENSE.txt | 13 + README.md | 124 + Shaper2D.ruleset | 6 + Shaper2D.sln | 38 +- global.json | 6 + src/Shaper2D/BezierLineSegment.cs | 120 + src/Shaper2D/BezierPolygon.cs | 78 + src/Shaper2D/ComplexPolygon.cs | 230 + src/Shaper2D/Guard.cs | 234 + src/Shaper2D/ILineSegment.cs | 22 + src/Shaper2D/IPath.cs | 48 + src/Shaper2D/IShape.cs | 63 + src/Shaper2D/InternalPath.cs | 517 +++ src/Shaper2D/LinearLineSegment.cs | 56 + src/Shaper2D/LinearPolygon.cs | 84 + src/Shaper2D/Path.cs | 52 + src/Shaper2D/Point.cs | 227 + src/Shaper2D/PointInfo.cs | 35 + src/Shaper2D/Polygon.cs | 141 + src/Shaper2D/PolygonClipper/Clipper.cs | 3858 +++++++++++++++++ .../PolygonClipper/ClipperException.cs | 29 + src/Shaper2D/PolygonClipper/Direction.cs | 29 + src/Shaper2D/PolygonClipper/EdgeSide.cs | 29 + src/Shaper2D/PolygonClipper/IntersectNode.cs | 36 + .../PolygonClipper/IntersectNodeSort.cs | 46 + src/Shaper2D/PolygonClipper/Join.cs | 36 + src/Shaper2D/PolygonClipper/LocalMinima.cs | 42 + src/Shaper2D/PolygonClipper/Maxima.cs | 36 + src/Shaper2D/PolygonClipper/OutPt.cs | 41 + src/Shaper2D/PolygonClipper/OutRec.cs | 62 + src/Shaper2D/PolygonClipper/PolyNode.cs | 177 + src/Shaper2D/PolygonClipper/PolyTree.cs | 79 + src/Shaper2D/PolygonClipper/PolyType.cs | 29 + src/Shaper2D/PolygonClipper/README.md | 40 + src/Shaper2D/PolygonClipper/Scanbeam.cs | 31 + src/Shaper2D/PolygonClipper/TEdge.cs | 116 + src/Shaper2D/Properties/AssemblyInfo.cs | 6 + src/Shaper2D/Rectangle.cs | 345 ++ src/Shaper2D/Shaper2D.xproj | 25 + src/Shaper2D/Size.cs | 163 + src/Shaper2D/project.json | 73 + src/Shared/AssemblyInfo.Common.cs | 37 + src/Shared/stylecop.json | 9 + .../Shaper2D.Tests/Properties/AssemblyInfo.cs | 23 + tests/Shaper2D.Tests/Shaper2D.Tests.xproj | 22 + tests/Shaper2D.Tests/project.json | 34 + 47 files changed, 7544 insertions(+), 6 deletions(-) create mode 100644 .editorconfig create mode 100644 APACHE-2.0-LICENSE.txt create mode 100644 README.md create mode 100644 Shaper2D.ruleset create mode 100644 global.json create mode 100644 src/Shaper2D/BezierLineSegment.cs create mode 100644 src/Shaper2D/BezierPolygon.cs create mode 100644 src/Shaper2D/ComplexPolygon.cs create mode 100644 src/Shaper2D/Guard.cs create mode 100644 src/Shaper2D/ILineSegment.cs create mode 100644 src/Shaper2D/IPath.cs create mode 100644 src/Shaper2D/IShape.cs create mode 100644 src/Shaper2D/InternalPath.cs create mode 100644 src/Shaper2D/LinearLineSegment.cs create mode 100644 src/Shaper2D/LinearPolygon.cs create mode 100644 src/Shaper2D/Path.cs create mode 100644 src/Shaper2D/Point.cs create mode 100644 src/Shaper2D/PointInfo.cs create mode 100644 src/Shaper2D/Polygon.cs create mode 100644 src/Shaper2D/PolygonClipper/Clipper.cs create mode 100644 src/Shaper2D/PolygonClipper/ClipperException.cs create mode 100644 src/Shaper2D/PolygonClipper/Direction.cs create mode 100644 src/Shaper2D/PolygonClipper/EdgeSide.cs create mode 100644 src/Shaper2D/PolygonClipper/IntersectNode.cs create mode 100644 src/Shaper2D/PolygonClipper/IntersectNodeSort.cs create mode 100644 src/Shaper2D/PolygonClipper/Join.cs create mode 100644 src/Shaper2D/PolygonClipper/LocalMinima.cs create mode 100644 src/Shaper2D/PolygonClipper/Maxima.cs create mode 100644 src/Shaper2D/PolygonClipper/OutPt.cs create mode 100644 src/Shaper2D/PolygonClipper/OutRec.cs create mode 100644 src/Shaper2D/PolygonClipper/PolyNode.cs create mode 100644 src/Shaper2D/PolygonClipper/PolyTree.cs create mode 100644 src/Shaper2D/PolygonClipper/PolyType.cs create mode 100644 src/Shaper2D/PolygonClipper/README.md create mode 100644 src/Shaper2D/PolygonClipper/Scanbeam.cs create mode 100644 src/Shaper2D/PolygonClipper/TEdge.cs create mode 100644 src/Shaper2D/Properties/AssemblyInfo.cs create mode 100644 src/Shaper2D/Rectangle.cs create mode 100644 src/Shaper2D/Shaper2D.xproj create mode 100644 src/Shaper2D/Size.cs create mode 100644 src/Shaper2D/project.json create mode 100644 src/Shared/AssemblyInfo.Common.cs create mode 100644 src/Shared/stylecop.json create mode 100644 tests/Shaper2D.Tests/Properties/AssemblyInfo.cs create mode 100644 tests/Shaper2D.Tests/Shaper2D.Tests.xproj create mode 100644 tests/Shaper2D.Tests/project.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f39b267 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.cs] +indent_style = space +indent_size = 4 diff --git a/APACHE-2.0-LICENSE.txt b/APACHE-2.0-LICENSE.txt new file mode 100644 index 0000000..a666c6e --- /dev/null +++ b/APACHE-2.0-LICENSE.txt @@ -0,0 +1,13 @@ +Copyright 2012 James South + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c50f84 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ + +# ImageSharp + +**ImageSharp** is a new cross-platform 2D graphics API designed to allow the processing of images without the use of `System.Drawing`. + +> **ImageSharp is still in early stages (alpha) but progress has been pretty quick. As such, please do not use on production environments until the library reaches release candidate status. Pre-release downloads are available from the [MyGet package repository](https://www.myget.org/gallery/imagesharp).** + +[![GitHub license](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/JimBobSquarePants/ImageSharp/master/APACHE-2.0-LICENSE.txt) +[![GitHub issues](https://img.shields.io/github/issues/JimBobSquarePants/ImageSharp.svg)](https://github.com/JimBobSquarePants/ImageSharp/issues) +[![GitHub stars](https://img.shields.io/github/stars/JimBobSquarePants/ImageSharp.svg)](https://github.com/JimBobSquarePants/ImageSharp/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/JimBobSquarePants/ImageSharp.svg)](https://github.com/JimBobSquarePants/ImageSharp/network) +[![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/ImageSharp/General?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Twitter](https://img.shields.io/twitter/url/https/github.com/JimBobSquarePants/ImageSharp.svg?style=social)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fJimBobSquarePants%2fImageSharp&via=james_m_south) + + +| |Build Status|Code Coverage| +|-------------|:----------:|:-----------:| +|**Linux/Mac**|[![Build Status](https://travis-ci.org/JimBobSquarePants/ImageSharp.svg)](https://travis-ci.org/JimBobSquarePants/ImageSharp)|[![Code coverage](https://codecov.io/gh/JimBobSquarePants/ImageSharp/branch/master/graph/badge.svg)](https://codecov.io/gh/JimBobSquarePants/ImageSharp)| +|**Windows** |[![Build Status](https://ci.appveyor.com/api/projects/status/hu6d1gdpxdw0q360/branch/master?svg=true)](https://ci.appveyor.com/project/JamesSouth/imagesharp/branch/master)|[![Code coverage](https://codecov.io/gh/JimBobSquarePants/ImageSharp/branch/master/graph/badge.svg)](https://codecov.io/gh/JimBobSquarePants/ImageSharp)| + + +### Installation +At present the code is pre-release but when ready it will be available on [Nuget](http://www.nuget.org). + +**Pre-release downloads** + +We already have a [MyGet package repository](https://www.myget.org/gallery/imagesharp) - for bleeding-edge / development NuGet releases. + +### Packages + +The **ImageSharp** library is made up of multiple packages, to make **ImageSharp** do anything useful you will want to make sure you include at least one format as a dependency otherwise you will not be able to save/load any images. + +Packages include: +- **ImageSharp** + Contains the Image classes, Colors, Primitives, Bootstrapper, IImageFormat interface, and other core functionality. +- **ImageSharp.Formats.Jpeg** + The jpeg decoder/encoder (Auto registered) +- **ImageSharp.Formats.Png** + The png decoder/encoder (Auto registered) +- **ImageSharp.Formats.Gif** + The gif decoder/encoder (Auto registered) +- **ImageSharp.Formats.Bmp** + The bmp decoder/encoder (Auto registered) +- **ImageSharp.Processing** + Contains methods like Resize, Crop, Skew, Rotate - Anything that alters the dimensions of the image. + Contains methods like Gaussian Blur, Pixelate, Edge Detection - Anything that maintains the original image dimensions. +- **ImageSharp.Drawing** + Brushes and various drawing algorithms. + +### Manual build + +If you prefer, you can compile ImageSharp yourself (please do and help!), you'll need: + +- [Visual Studio 2015 with Update 3 (or above)](https://www.visualstudio.com/news/releasenotes/vs2015-update3-vs) +- The [.NET Core 1.0 SDK Installer](https://www.microsoft.com/net/core#windows) - Non VSCode link. + +To clone it locally click the "Clone in Windows" button above or run the following git commands. + +```bash +git clone https://github.com/JimBobSquarePants/ImageSharp +``` + +### Features + +There's plenty there and more coming. Check out the [current features](features.md)! + +### API + +Without the constraints of `System.Drawing` We have been able to develop something much more flexible, easier to code against, and much, much less prone to memory leaks. Gone are system-wide process-locks. Images and processors are thread safe usable in parallel processing utilizing all the availables cores. + +Many `Image` methods are also fluent. + +Here's an example of the code required to resize an image using the default Bicubic resampler then turn the colors into their grayscale equivalent using the BT709 standard matrix. + +```csharp +using (FileStream stream = File.OpenRead("foo.jpg")) +using (FileStream output = File.OpenWrite("bar.jpg")) +{ + Image image = new Image(stream); + image.Resize(image.Width / 2, image.Height / 2) + .Grayscale() + .Save(output); +} +``` + +Individual processors can be initialised and apply processing against images. This allows nesting which brings the potential for powerful combinations of processing methods: + +```csharp +new BrightnessProcessor(50).Apply(sourceImage, sourceImage.Bounds); +``` + +Setting individual pixel values is perfomed as follows: + +```csharp +Image image = new Image(400, 400); +using (var pixels = image.Lock()) +{ + pixels[200, 200] = Color.White; +} +``` + +For advanced usage the `Image` and `PixelAccessor` classes are available allowing developers to implement their own color models in the same manner as Microsoft XNA Game Studio and MonoGame. + +All in all this should allow image processing to be much more accessible to developers which has always been my goal from the start. + +### How can you help? + +Please... Spread the word, contribute algorithms, submit performance improvements, unit tests. + +Performance is a biggie, if you know anything about the new vector types and can apply some fancy new stuff with that it would be awesome. + +There's a lot of developers out there who could write this stuff a lot better and faster than I and I would love to see what we collectively can come up with so please, if you can help in any way it would be most welcome and benificial for all. + +### The ImageSharp Team + +Grand High Eternal Dictator +- [Scott Williams](https://github.com/jimbobsquarepants) + +Core Team +- [Dirk Lemstra](https://github.com/dlemstra) +- [Jeavon Leopold](https://github.com/jeavon) +- [Anton Firsov](https://github.com/antonfirsov) +- [Olivia Ifrim](https://github.com/olivif) +- [Scott Williams](https://github.com/tocsoft) diff --git a/Shaper2D.ruleset b/Shaper2D.ruleset new file mode 100644 index 0000000..554dc16 --- /dev/null +++ b/Shaper2D.ruleset @@ -0,0 +1,6 @@ + + + + + + diff --git a/Shaper2D.sln b/Shaper2D.sln index 20e03f6..3849929 100644 --- a/Shaper2D.sln +++ b/Shaper2D.sln @@ -3,9 +3,26 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shaper2D", "Shaper2D\Shaper2D.csproj", "{8EC582C9-750F-48BC-B48E-B3F89A5BA7B7}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + global.json = global.json + README.md = README.md + EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Items", "Items", "{20777789-C567-4304-BB8D-11FAF5328812}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{815C0625-CD3D-440F-9F80-2D83856AB7AE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{56801022-D71A-4FBE-BC5B-CBA08E2284EC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{9E574A07-F879-4811-9C41-5CBDC6BAFDB7}" + ProjectSection(SolutionItems) = preProject + src\Shared\AssemblyInfo.Common.cs = src\Shared\AssemblyInfo.Common.cs + src\Shared\stylecop.json = src\Shared\stylecop.json + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Shaper2D", "src\Shaper2D\Shaper2D.xproj", "{2E33181E-6E28-4662-A801-E2E7DC206029}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Shaper2D.Tests", "tests\Shaper2D.Tests\Shaper2D.Tests.xproj", "{F836E8E6-B4D9-4208-8346-140C74678B91}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -13,12 +30,21 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8EC582C9-750F-48BC-B48E-B3F89A5BA7B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8EC582C9-750F-48BC-B48E-B3F89A5BA7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8EC582C9-750F-48BC-B48E-B3F89A5BA7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8EC582C9-750F-48BC-B48E-B3F89A5BA7B7}.Release|Any CPU.Build.0 = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.Build.0 = Release|Any CPU + {F836E8E6-B4D9-4208-8346-140C74678B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F836E8E6-B4D9-4208-8346-140C74678B91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F836E8E6-B4D9-4208-8346-140C74678B91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F836E8E6-B4D9-4208-8346-140C74678B91}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9E574A07-F879-4811-9C41-5CBDC6BAFDB7} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {2E33181E-6E28-4662-A801-E2E7DC206029} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {F836E8E6-B4D9-4208-8346-140C74678B91} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} + EndGlobalSection EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..7346bdc --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src" ], + "sdk": { + "version": "1.0.0-preview2-003121" + } +} \ No newline at end of file diff --git a/src/Shaper2D/BezierLineSegment.cs b/src/Shaper2D/BezierLineSegment.cs new file mode 100644 index 0000000..0d70a0f --- /dev/null +++ b/src/Shaper2D/BezierLineSegment.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections.Immutable; + using System.Numerics; + + /// + /// Represents a line segment that conistst of control points that will be rendered as a cubic bezier curve + /// + /// + public class BezierLineSegment : ILineSegment + { + /// + /// The segments per curve. + /// code for this taken from + /// + private const int SegmentsPerCurve = 50; + + /// + /// The line points. + /// + private readonly Point[] linePoints; + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public BezierLineSegment(params Point[] points) + { + Guard.NotNull(points, nameof(points)); + Guard.MustBeGreaterThanOrEqualTo(points.Length, 4, nameof(points)); + + this.linePoints = this.GetDrawingPoints(points); + } + + /// + /// Returns the current a simple linear path. + /// + /// + /// Returns the current as simple linear path. + /// + public ImmutableArray AsSimpleLinearPath() + { + return ImmutableArray.Create(this.linePoints); + } + + /// + /// Returns the drawing points along the line. + /// + /// The control points. + /// + /// The . + /// + private Point[] GetDrawingPoints(Point[] controlPoints) + { + // TODO we need to calculate an optimal SegmentsPerCurve value + // depending on the calcualted length of this curve + int curveCount = (controlPoints.Length - 1) / 3; + int finalPointCount = (SegmentsPerCurve * curveCount) + 1; // we have SegmentsPerCurve for each curve plus the origon point; + + Point[] drawingPoints = new Point[finalPointCount]; + + int position = 0; + int targetPoint = controlPoints.Length - 3; + for (int i = 0; i < targetPoint; i += 3) + { + Vector2 p0 = controlPoints[i]; + Vector2 p1 = controlPoints[i + 1]; + Vector2 p2 = controlPoints[i + 2]; + Vector2 p3 = controlPoints[i + 3]; + + // only do this for the first end point. When i != 0, this coincides with the end point of the previous segment, + if (i == 0) + { + drawingPoints[position++] = this.CalculateBezierPoint(0, p0, p1, p2, p3); + } + + for (int j = 1; j <= SegmentsPerCurve; j++) + { + float t = j / (float)SegmentsPerCurve; + drawingPoints[position++] = this.CalculateBezierPoint(t, p0, p1, p2, p3); + } + } + + return drawingPoints; + } + + /// + /// Calculates the bezier point along the line. + /// + /// The position within the line. + /// The p 0. + /// The p 1. + /// The p 2. + /// The p 3. + /// + /// The . + /// + private Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) + { + float u = 1 - t; + float tt = t * t; + float uu = u * u; + float uuu = uu * u; + float ttt = tt * t; + + Vector2 p = uuu * p0; // first term + + p += 3 * uu * t * p1; // second term + p += 3 * u * tt * p2; // third term + p += ttt * p3; // fourth term + + return p; + } + } +} diff --git a/src/Shaper2D/BezierPolygon.cs b/src/Shaper2D/BezierPolygon.cs new file mode 100644 index 0000000..5bc44ff --- /dev/null +++ b/src/Shaper2D/BezierPolygon.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections; + using System.Collections.Generic; + using System.Numerics; + + /// + /// Represents a polygon made up exclusivly of a single close cubic Bezier curve. + /// + public sealed class BezierPolygon : IShape + { + private Polygon innerPolygon; + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public BezierPolygon(params Point[] points) + { + this.innerPolygon = new Polygon(new BezierLineSegment(points)); + } + + /// + /// Gets the bounding box of this shape. + /// + /// + /// The bounds. + /// + public Rectangle Bounds => this.innerPolygon.Bounds; + + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => this.innerPolygon.MaxIntersections; + + /// + /// Gets the paths that make up this shape + /// + /// + /// The paths. + /// + public IEnumerable Paths => this.innerPolygon.Paths; + + /// + /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds + /// + /// The point. + /// + /// The distance from the shape. + /// + public float Distance(Point point) => this.innerPolygon.Distance(point); + + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Point start, Point end, Point[] buffer, int count, int offset) + { + return this.innerPolygon.FindIntersections(start, end, buffer, count, offset); + } + } +} diff --git a/src/Shaper2D/ComplexPolygon.cs b/src/Shaper2D/ComplexPolygon.cs new file mode 100644 index 0000000..9b024d0 --- /dev/null +++ b/src/Shaper2D/ComplexPolygon.cs @@ -0,0 +1,230 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Numerics; + + using PolygonClipper; + + /// + /// Represents a complex polygon made up of one or more outline + /// polygons and one or more holes to punch out of them. + /// + /// + public sealed class ComplexPolygon : IShape + { + private const float ClipperScaleFactor = 100f; + private IShape[] shapes; + private IEnumerable paths; + + /// + /// Initializes a new instance of the class. + /// + /// The outline. + /// The holes. + public ComplexPolygon(IShape outline, params IShape[] holes) + : this(new[] { outline }, holes) + { + } + + /// + /// 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.MaxIntersections = this.FixAndSetShapes(outlines, holes); + + 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); + + this.Bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + + /// + /// Gets the paths that make up this shape + /// + /// + /// The paths. + /// + public IEnumerable Paths => this.paths; + + /// + /// Gets the bounding box of this shape. + /// + /// + /// The bounds. + /// + public Rectangle Bounds { get; } + + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections { get; } + + /// + /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds + /// + /// The point. + /// + /// Returns the distance from thr shape to the point + /// + /// + /// Due to the clipping we did during construction we know that out shapes do not overlap at there edges + /// therefore for apoint to be in more that one we must be in a hole of another, theoretically this could + /// then flip again to be in a outlin inside a hole inside an outline :) + /// + float IShape.Distance(Point point) + { + float dist = float.MaxValue; + bool inside = false; + foreach (IShape shape in this.shapes) + { + float d = shape.Distance(point); + + if (d <= 0) + { + // we are inside a poly + d = -d; // flip the sign + inside ^= true; // flip the inside flag + } + + if (d < dist) + { + dist = d; + } + } + + if (inside) + { + return -dist; + } + + return dist; + } + + /// + /// Based on a line described by and + /// populate a buffer for all points on all the polygons, that make up this complex shape, + /// that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Point start, Point end, Point[] buffer, int count, int offset) + { + int totalAdded = 0; + for (int i = 0; i < this.shapes.Length; i++) + { + int added = this.shapes[i].FindIntersections(start, end, buffer, count, offset); + count -= added; + offset += added; + totalAdded += added; + } + + return totalAdded; + } + + private void AddPoints(Clipper clipper, IShape shape, PolyType polyType) + { + // if the path is already the shape use it directly and skip the path loop. + if (shape is IPath) + { + clipper.AddPath( + (IPath)shape, + polyType); + } + else + { + foreach (IPath path in shape.Paths) + { + clipper.AddPath( + path, + polyType); + } + } + } + + private void AddPoints(Clipper clipper, IEnumerable shapes, PolyType polyType) + { + foreach (IShape shape in shapes) + { + this.AddPoints(clipper, shape, polyType); + } + } + + private void ExtractOutlines(PolyNode tree, List shapes, List paths) + { + 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 optimisations that are availible. + if (tree.SourcePath != null) + { + shapes.Add((IShape)tree.SourcePath); + paths.Add(tree.SourcePath); + } + else + { + Polygon polygon = new Polygon(new LinearLineSegment(tree.Contour.Select(x => new Point(x)).ToArray())); + + shapes.Add(polygon); + paths.Add(polygon); + } + } + + foreach (PolyNode c in tree.Children) + { + this.ExtractOutlines(c, shapes, paths); + } + } + + private int FixAndSetShapes(IEnumerable outlines, IEnumerable holes) + { + 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 + this.AddPoints(clipper, outlines, PolyType.Subject); + this.AddPoints(clipper, holes, PolyType.Clip); + + PolyTree tree = clipper.Execute(); + + List shapes = new List(); + List paths = new List(); + + // convert the 'tree' back to paths + this.ExtractOutlines(tree, shapes, paths); + this.shapes = shapes.ToArray(); + this.paths = paths.ToArray(); + + int intersections = 0; + foreach (IShape s in this.shapes) + { + intersections += s.MaxIntersections; + } + + return intersections; + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/Guard.cs b/src/Shaper2D/Guard.cs new file mode 100644 index 0000000..a0f11e8 --- /dev/null +++ b/src/Shaper2D/Guard.cs @@ -0,0 +1,234 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + + /// + /// Provides methods to protect against invalid parameters. + /// + [DebuggerStepThrough] + internal static class Guard + { + /// + /// Verifies, that the method parameter with specified object value is not null + /// and throws an exception if it is found to be so. + /// + /// The target object, which cannot be null. + /// The name of the parameter that is to be checked. + /// The error message, if any to add to the exception. + /// is null + public static void NotNull(object target, string parameterName, string message = "") + { + if (target == null) + { + if (!string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentNullException(parameterName, message); + } + + throw new ArgumentNullException(parameterName); + } + } + + /// + /// Verifies, that the string method parameter with specified object value and message + /// is not null, not empty and does not contain only blanks and throws an exception + /// if the object is null. + /// + /// The target string, which should be checked against being null or empty. + /// Name of the parameter. + /// The error message, if any to add to the exception. + /// is null. + /// is empty or contains only blanks. + public static void NotNullOrEmpty(string target, string parameterName, string message = "") + { + NotNull(target, parameterName, message); + + if (string.IsNullOrWhiteSpace(target)) + { + if (!string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentException(message, parameterName); + } + + throw new ArgumentException("Value cannot be null or empty and cannot contain only blanks.", parameterName); + } + } + + /// + /// Verifies, that the enumeration is not null and not empty. + /// + /// The type of objects in the + /// The target enumeration, which should be checked against being null or empty. + /// Name of the parameter. + /// The error message, if any to add to the exception. + /// is null. + /// is empty. + public static void NotNullOrEmpty(IEnumerable target, string parameterName, string message = "") + { + NotNull(target, parameterName, message); + + if (!target.Any()) + { + if (!string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentException(message, parameterName); + } + + throw new ArgumentException("Value cannot be empty.", parameterName); + } + } + + /// + /// Verifies that the specified value is less than a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is greater than the maximum value. + /// + public static void MustBeLessThan(TValue value, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(max) >= 0) + { + throw new ArgumentOutOfRangeException(parameterName, $"Value must be less than {max}."); + } + } + + /// + /// Verifies that the specified value is less than or equal to a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is greater than the maximum value. + /// + public static void MustBeLessThanOrEqualTo(TValue value, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(max) > 0) + { + throw new ArgumentOutOfRangeException(parameterName, $"Value must be less than or equal to {max}."); + } + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value. + /// + public static void MustBeGreaterThan(TValue value, TValue min, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) <= 0) + { + throw new ArgumentOutOfRangeException( + parameterName, + $"Value must be greater than {min}."); + } + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value. + /// + public static void MustBeGreaterThanOrEqualTo(TValue value, TValue min, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) < 0) + { + throw new ArgumentOutOfRangeException(parameterName, $"Value must be greater than or equal to {min}."); + } + } + + /// + /// Verifies that the specified value is greater than or equal to a minimum value and less than + /// or equal to a maximum value and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value of greater than the maximum value. + /// + public static void MustBeBetweenOrEqualTo(TValue value, TValue min, TValue max, string parameterName) + where TValue : IComparable + { + if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0) + { + throw new ArgumentOutOfRangeException(parameterName, $"Value must be greater than or equal to {min} and less than or equal to {max}."); + } + } + + /// + /// Verifies, that the method parameter with specified target value is true + /// and throws an exception if it is found to be so. + /// + /// + /// The target value, which cannot be false. + /// + /// + /// The name of the parameter that is to be checked. + /// + /// + /// The error message, if any to add to the exception. + /// + /// + /// is false + /// + public static void IsTrue(bool target, string parameterName, string message) + { + if (!target) + { + throw new ArgumentException(message, parameterName); + } + } + + /// + /// Verifies, that the method parameter with specified target value is false + /// and throws an exception if it is found to be so. + /// + /// The target value, which cannot be true. + /// The name of the parameter that is to be checked. + /// The error message, if any to add to the exception. + /// + /// is true + /// + public static void IsFalse(bool target, string parameterName, string message) + { + if (target) + { + throw new ArgumentException(message, parameterName); + } + } + } +} diff --git a/src/Shaper2D/ILineSegment.cs b/src/Shaper2D/ILineSegment.cs new file mode 100644 index 0000000..eba73a3 --- /dev/null +++ b/src/Shaper2D/ILineSegment.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections.Immutable; + using System.Numerics; + + /// + /// Represents a simple path segment + /// + public interface ILineSegment + { + /// + /// Converts the into a simple linear path.. + /// + /// Returns the current as simple linear path. + ImmutableArray AsSimpleLinearPath(); // TODO move this over to ReadonlySpan once available + } +} diff --git a/src/Shaper2D/IPath.cs b/src/Shaper2D/IPath.cs new file mode 100644 index 0000000..e847792 --- /dev/null +++ b/src/Shaper2D/IPath.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Numerics; + + /// + /// Represents a logic path that can be drawn + /// + public interface IPath : ILineSegment + { + /// + /// Gets the bounds enclosing the path + /// + /// + /// The bounds. + /// + Rectangle Bounds { get; } + + /// + /// Gets a value indicating whether this instance is closed. + /// + /// + /// true if this instance is closed; otherwise, false. + /// + bool IsClosed { get; } + + /// + /// Gets the length of the path + /// + /// + /// The length. + /// + float Length { get; } + + /// + /// Calculates the distance along and away from the path for a specified point. + /// + /// The point along the path. + /// + /// Returns details about the point and its distance away from the path. + /// + PointInfo Distance(Point point); + } +} diff --git a/src/Shaper2D/IShape.cs b/src/Shaper2D/IShape.cs new file mode 100644 index 0000000..3a54098 --- /dev/null +++ b/src/Shaper2D/IShape.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections.Generic; + using System.Numerics; + + /// + /// Represents a closed set of paths making up a single shape. + /// + public interface IShape + { + /// + /// Gets the bounding box of this shape. + /// + /// + /// The bounds. + /// + Rectangle Bounds { get; } + + /// + /// Gets the paths that make up this shape + /// + /// + /// The paths. + /// + IEnumerable Paths { get; } + + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + int MaxIntersections { get; } + + /// + /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds + /// + /// The point. + /// + /// Returns the distance from the shape to the point + /// + float Distance(Point point); + + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + int FindIntersections(Point start, Point end, Point[] buffer, int count, int offset); + } +} diff --git a/src/Shaper2D/InternalPath.cs b/src/Shaper2D/InternalPath.cs new file mode 100644 index 0000000..260752d --- /dev/null +++ b/src/Shaper2D/InternalPath.cs @@ -0,0 +1,517 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// +namespace Shaper2D +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Numerics; + + /// + /// Internal logic for integrating linear paths. + /// + internal class InternalPath + { + /// + /// The maximum vector + /// + private static readonly Vector2 MaxVector = new Vector2(float.MaxValue); + + /// + /// The locker. + /// + private static readonly object Locker = new object(); + + /// + /// The points. + /// + private readonly ImmutableArray points; + + /// + /// The closed path. + /// + private readonly bool closedPath; + + /// + /// The total distance. + /// + private readonly Lazy totalDistance; + + /// + /// The constant. + /// + private float[] constant; + + /// + /// The multiples. + /// + private float[] multiple; + + /// + /// The distances. + /// + private float[] distance; + + /// + /// The calculated. + /// + private bool calculated = false; + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + /// if set to true [is closed path]. + internal InternalPath(ILineSegment[] segments, bool isClosedPath) + : this(Simplify(segments), isClosedPath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segment. + /// if set to true [is closed path]. + internal InternalPath(ILineSegment segment, bool isClosedPath) + : this(segment.AsSimpleLinearPath(), isClosedPath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + /// if set to true [is closed path]. + internal InternalPath(ImmutableArray points, bool isClosedPath) + { + this.points = points; + this.closedPath = isClosedPath; + + float minX = this.points.Min(x => x.X); + float maxX = this.points.Max(x => x.X); + float minY = this.points.Min(x => x.Y); + float maxY = this.points.Max(x => x.Y); + + this.Bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY); + this.totalDistance = new Lazy(this.CalculateLength); + } + + /// + /// Gets the bounds. + /// + /// + /// The bounds. + /// + public Rectangle Bounds + { + get; + } + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public float Length => this.totalDistance.Value; + + /// + /// Gets the points. + /// + /// + /// The points. + /// + internal ImmutableArray Points => this.points; + + /// + /// Calculates the distance from the path. + /// + /// The point. + /// Returns the distance from the path + public PointInfo DistanceFromPath(Vector2 point) + { + this.CalculateConstants(); + + PointInfoInternal internalInfo = default(PointInfoInternal); + internalInfo.DistanceSquared = float.MaxValue; // Set it to max so that CalculateShorterDistance can reduce it back down + + int polyCorners = this.points.Length; + + if (!this.closedPath) + { + polyCorners -= 1; + } + + int closestPoint = 0; + for (int i = 0; i < polyCorners; i++) + { + int next = i + 1; + if (this.closedPath && next == polyCorners) + { + next = 0; + } + + if (this.CalculateShorterDistance(this.points[i], this.points[next], point, ref internalInfo)) + { + closestPoint = i; + } + } + + return new PointInfo + { + DistanceAlongPath = this.distance[closestPoint] + Vector2.Distance(this.points[closestPoint], point), + DistanceFromPath = (float)Math.Sqrt(internalInfo.DistanceSquared), + SearchPoint = point, + ClosestPointOnPath = internalInfo.PointOnLine + }; + } + + /// + /// Based on a line described by and + /// populate a buffer for all points on the path that the line intersects. + /// + /// The start. + /// The end. + /// The buffer. + /// The count. + /// The offset. + /// number iof intersections hit + public int FindIntersections(Vector2 start, Vector2 end, Point[] buffer, int count, int offset) + { + int polyCorners = this.points.Length; + + if (!this.closedPath) + { + polyCorners -= 1; + } + + int position = 0; + for (int i = 0; i < polyCorners && count > 0; i++) + { + int next = i + 1; + if (this.closedPath && next == polyCorners) + { + next = 0; + } + + Vector2 point = FindIntersection(this.points[i], this.points[next], start, end); + if (point != MaxVector) + { + buffer[position + offset] = point; + position++; + count--; + } + } + + return position; + } + + /// + /// Determines if the specified point is inside or outside the path. + /// + /// The point. + /// Returns true if the point is inside the closed path. + public bool PointInPolygon(Point point) + { + // You can only be inside a path if its "closed" + if (!this.closedPath) + { + return false; + } + + if (!this.Bounds.Contains(point)) + { + return false; + } + + this.CalculateConstants(); + + ImmutableArray poly = this.points; + int polyCorners = poly.Length; + + int j = polyCorners - 1; + bool oddNodes = false; + + for (int i = 0; i < polyCorners; i++) + { + if ((poly[i].Y < point.Y && poly[j].Y >= point.Y) + || (poly[j].Y < point.Y && poly[i].Y >= point.Y)) + { + oddNodes ^= (point.Y * this.multiple[i]) + this.constant[i] < point.X; + } + + j = i; + } + + return oddNodes; + } + + /// + /// Determins if the bounding box for 2 lines + /// described by and + /// and and overlap. + /// + /// The line1 start. + /// The line1 end. + /// The line2 start. + /// The line2 end. + /// Returns true it the bounding box of the 2 lines intersect + private static bool BoundingBoxesIntersect(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End) + { + Vector2 topLeft1 = Vector2.Min(line1Start, line1End); + Vector2 bottomRight1 = Vector2.Max(line1Start, line1End); + + Vector2 topLeft2 = Vector2.Min(line2Start, line2End); + Vector2 bottomRight2 = Vector2.Max(line2Start, line2End); + + float left1 = topLeft1.X; + float right1 = bottomRight1.X; + float top1 = topLeft1.Y; + float bottom1 = bottomRight1.Y; + + float left2 = topLeft2.X; + float right2 = bottomRight2.X; + float top2 = topLeft2.Y; + float bottom2 = bottomRight2.Y; + + return left1 <= right2 && right1 >= left2 + && + top1 <= bottom2 && bottom1 >= top2; + } + + /// + /// Finds the point on line described by and + /// that intersects with line described by and + /// + /// The line1 start. + /// The line1 end. + /// The line2 start. + /// The line2 end. + /// + /// A describing the point that the 2 lines cross or if they do not. + /// + private static Vector2 FindIntersection(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End) + { + // do bounding boxes overlap, if not then the lines can't and return fast. + if (!BoundingBoxesIntersect(line1Start, line1End, line2Start, line2End)) + { + return MaxVector; + } + + Vector2 line1Diff = line1End - line1Start; + Vector2 line2Diff = line2End - line2Start; + + Vector2 point; + if (line1Diff.X == 0) + { + float slope = line2Diff.Y / line2Diff.X; + float yinter = line2Start.Y - (slope * line2Start.X); + float y = (line1Start.X * slope) + yinter; + point = new Vector2(line1Start.X, y); + + // horizontal and vertical lines + } + else if (line2Diff.X == 0) + { + float slope = line1Diff.Y / line1Diff.X; + float yinter = line1Start.Y - (slope * line1Start.X); + float y = (line2Start.X * slope) + yinter; + point = new Vector2(line2Start.X, y); + + // horizontal and vertical lines + } + else + { + float slope1 = line1Diff.Y / line1Diff.X; + float slope2 = line2Diff.Y / line2Diff.X; + + float yinter1 = line1Start.Y - (slope1 * line1Start.X); + float yinter2 = line2Start.Y - (slope2 * line2Start.X); + + if (slope1 == slope2 && yinter1 != yinter2) + { + return MaxVector; + } + + float x = (yinter2 - yinter1) / (slope1 - slope2); + float y = (slope1 * x) + yinter1; + + point = new Vector2(x, y); + } + + if (BoundingBoxesIntersect(line1Start, line1End, point, point)) + { + return point; + } + else if (BoundingBoxesIntersect(line2Start, line2End, point, point)) + { + return point; + } + + return MaxVector; + } + + /// + /// Simplifies the collection of segments. + /// + /// The segments. + /// + /// The . + /// + private static ImmutableArray Simplify(ILineSegment[] segments) + { + List simplified = new List(); + foreach (ILineSegment seg in segments) + { + simplified.AddRange(seg.AsSimpleLinearPath()); + } + + return simplified.ToImmutableArray(); + } + + /// + /// Returns the length of the path. + /// + /// + /// The . + /// + private float CalculateLength() + { + float length = 0; + int polyCorners = this.points.Length; + + if (!this.closedPath) + { + polyCorners -= 1; + } + + for (int i = 0; i < polyCorners; i++) + { + int next = i + 1; + if (this.closedPath && next == polyCorners) + { + next = 0; + } + + length += Vector2.Distance(this.points[i], this.points[next]); + } + + return length; + } + + /// + /// Calculate the constants. + /// + private void CalculateConstants() + { + // http://alienryderflex.com/polygon/ source for point in polygon logic + if (this.calculated) + { + return; + } + + lock (Locker) + { + if (this.calculated) + { + return; + } + + ImmutableArray poly = this.points; + int polyCorners = poly.Length; + this.constant = new float[polyCorners]; + this.multiple = new float[polyCorners]; + this.distance = new float[polyCorners]; + int i, j = polyCorners - 1; + + this.distance[0] = 0; + + for (i = 0; i < polyCorners; i++) + { + this.distance[j] = this.distance[i] + Vector2.Distance(poly[i], poly[j]); + if (poly[j].Y == poly[i].Y) + { + this.constant[i] = poly[i].X; + this.multiple[i] = 0; + } + else + { + Vector2 subtracted = poly[j] - poly[i]; + this.constant[i] = (poly[i].X - ((poly[i].Y * poly[j].X) / subtracted.Y)) + ((poly[i].Y * poly[i].X) / subtracted.Y); + this.multiple[i] = subtracted.X / subtracted.Y; + } + + j = i; + } + + this.calculated = true; + } + } + + /// + /// Calculate any shorter distances along the path. + /// + /// The start position. + /// The end position. + /// The current point. + /// The info. + /// + /// The . + /// + private bool CalculateShorterDistance(Vector2 start, Vector2 end, Vector2 point, ref PointInfoInternal info) + { + Vector2 diffEnds = end - start; + + float lengthSquared = diffEnds.LengthSquared(); + Vector2 diff = point - start; + + Vector2 multiplied = diff * diffEnds; + float u = (multiplied.X + multiplied.Y) / lengthSquared; + + if (u > 1) + { + u = 1; + } + else if (u < 0) + { + u = 0; + } + + Vector2 multipliedByU = diffEnds * u; + + Vector2 pointOnLine = start + multipliedByU; + + Vector2 d = pointOnLine - point; + + float dist = d.LengthSquared(); + + if (info.DistanceSquared > dist) + { + info.DistanceSquared = dist; + info.PointOnLine = pointOnLine; + return true; + } + + return false; + } + + /// + /// Contains information about the current point. + /// + private struct PointInfoInternal + { + /// + /// The distance squared. + /// + public float DistanceSquared; + + /// + /// The point on the current line. + /// + public Point PointOnLine; + } + } +} diff --git a/src/Shaper2D/LinearLineSegment.cs b/src/Shaper2D/LinearLineSegment.cs new file mode 100644 index 0000000..df64fea --- /dev/null +++ b/src/Shaper2D/LinearLineSegment.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections.Immutable; + using System.Linq; + using System.Numerics; + + /// + /// Represents a series of control points that will be joined by straight lines + /// + /// + public class LinearLineSegment : ILineSegment + { + /// + /// The collection of points. + /// + private readonly ImmutableArray points; + + /// + /// Initializes a new instance of the class. + /// + /// The start. + /// The end. + public LinearLineSegment(Point start, Point end) + : this(new[] { start, end }) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public LinearLineSegment(params Point[] points) + { + Guard.NotNull(points, nameof(points)); + Guard.MustBeGreaterThanOrEqualTo(points.Count(), 2, nameof(points)); + + this.points = ImmutableArray.Create(points); + } + + /// + /// Converts the into a simple linear path.. + /// + /// + /// Returns the current as simple linear path. + /// + public ImmutableArray AsSimpleLinearPath() + { + return this.points; + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/LinearPolygon.cs b/src/Shaper2D/LinearPolygon.cs new file mode 100644 index 0000000..d43ac23 --- /dev/null +++ b/src/Shaper2D/LinearPolygon.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections; + using System.Collections.Generic; + using System.Numerics; + + /// + /// Represents a polygon made up exclusivly of a single Linear path. + /// + public sealed class LinearPolygon : IShape + { + private Polygon innerPolygon; + + /// + /// Initializes a new instance of the class. + /// + /// The points. + public LinearPolygon(params Point[] points) + { + this.innerPolygon = new Polygon(new LinearLineSegment(points)); + } + + /// + /// Gets the bounding box of this shape. + /// + /// + /// The bounds. + /// + public Rectangle Bounds => this.innerPolygon.Bounds; + + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections + { + get + { + return this.innerPolygon.MaxIntersections; + } + } + + /// + /// Gets the paths that make up this shape + /// + /// + /// The paths. + /// + public IEnumerable Paths => this.innerPolygon.Paths; + + /// + /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds + /// + /// The point. + /// + /// Returns the distance from the shape to the point + /// + public float Distance(Point point) => this.innerPolygon.Distance(point); + + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Point start, Point end, Point[] buffer, int count, int offset) + { + return this.innerPolygon.FindIntersections(start, end, buffer, count, offset); + } + } +} diff --git a/src/Shaper2D/Path.cs b/src/Shaper2D/Path.cs new file mode 100644 index 0000000..f3f3d1e --- /dev/null +++ b/src/Shaper2D/Path.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections.Immutable; + using System.Numerics; + + /// + /// A aggregate of s making a single logical path + /// + /// + public class Path : IPath + { + /// + /// The inner path. + /// + private readonly InternalPath innerPath; + + /// + /// Initializes a new instance of the class. + /// + /// The segment. + public Path(params ILineSegment[] segment) + { + this.innerPath = new InternalPath(segment, false); + } + + /// + public Rectangle Bounds => this.innerPath.Bounds; + + /// + public bool IsClosed => false; + + /// + public float Length => this.innerPath.Length; + + /// + public ImmutableArray AsSimpleLinearPath() + { + return this.innerPath.Points; + } + + /// + public PointInfo Distance(Point point) + { + return this.innerPath.DistanceFromPath(point); + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/Point.cs b/src/Shaper2D/Point.cs new file mode 100644 index 0000000..c686e4c --- /dev/null +++ b/src/Shaper2D/Point.cs @@ -0,0 +1,227 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System; + using System.ComponentModel; + using System.Numerics; + using System.Runtime.CompilerServices; + + /// + /// Represents an ordered pair of integer x- and y-coordinates that defines a point in + /// a two-dimensional plane. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Point : IEquatable + { + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly Point Empty = default(Point); + + private readonly Vector2 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the point. + /// The vertical position of the point. + public Point(float x, float y) + : this(new Vector2(x, y)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The vector representing the width and height. + /// + public Point(Vector2 vector) + { + this.backingVector = vector; + } + + /// + /// Gets the x-coordinate of this . + /// + public float X => this.backingVector.X; + + /// + /// Gets the y-coordinate of this . + /// + public float Y => this.backingVector.Y; + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Performs an implicit conversion from to . + /// + /// The d. + /// + /// The result of the conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector2(Point d) + { + return d.backingVector; + } + + /// + /// Performs an implicit conversion from to . + /// + /// The d. + /// + /// The result of the conversion. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Point(Vector2 d) + { + return new Point(d); + } + + /// + /// Computes the sum of adding two points. + /// + /// The point on the left hand of the operand. + /// The point on the right hand of the operand. + /// + /// The + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point operator +(Point left, Point right) + { + return new Point(left.backingVector + right.backingVector); + } + + /// + /// Computes the difference left by subtracting one point from another. + /// + /// The point on the left hand of the operand. + /// The point on the right hand of the operand. + /// + /// The + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point operator -(Point left, Point right) + { + return new Point(left.backingVector - right.backingVector); + } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Point left, Point right) + { + return left.backingVector == right.backingVector; + } + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Point left, Point right) + { + return left.backingVector != right.backingVector; + } + + /// + /// Gets a representation for this . + /// + /// A representation for this object. + public Vector2 ToVector2() + { + return this.backingVector; + } + + /// + /// Translates this by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + /// A new point offset by the size + public Point Offset(float dx, float dy) + { + return new Point(this.backingVector + new Vector2(dx, dy)); + } + + /// + /// Translates this by the specified amount. + /// + /// The used offset this . + /// A new point offset by the size + public Point Offset(Size p) + { + return new Point(this.backingVector + new Vector2(p.Width, p.Height)); + } + + /// + public override int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "Point [ Empty ]"; + } + + return $"Point [ X={this.X}, Y={this.Y} ]"; + } + + /// + public override bool Equals(object obj) + { + if (obj is Point) + { + return this.backingVector == ((Point)obj).backingVector; + } + + return false; + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + public bool Equals(Point other) + { + return this.backingVector == other.backingVector; + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/PointInfo.cs b/src/Shaper2D/PointInfo.cs new file mode 100644 index 0000000..56a8868 --- /dev/null +++ b/src/Shaper2D/PointInfo.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Numerics; + + /// + /// Returns meta data about the nearest point on a path from a vector + /// + public struct PointInfo + { + /// + /// The search point + /// + public Point SearchPoint; + + /// + /// The distance along path is away from the start of the path + /// + public float DistanceAlongPath; + + /// + /// The distance is away from . + /// + public float DistanceFromPath; + + /// + /// The closest point to that lies on the path. + /// + public Point ClosestPointOnPath; + } +} diff --git a/src/Shaper2D/Polygon.cs b/src/Shaper2D/Polygon.cs new file mode 100644 index 0000000..82600e0 --- /dev/null +++ b/src/Shaper2D/Polygon.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Numerics; + + /// + /// A shape made up of a single path made up of one of more s + /// + public sealed class Polygon : IShape, IPath + { + private readonly InternalPath innerPath; + private readonly IEnumerable pathCollection; + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + public Polygon(params ILineSegment[] segments) + { + this.innerPath = new InternalPath(segments, true); + this.pathCollection = new[] { this }; + } + + /// + /// Initializes a new instance of the class. + /// + /// The segment. + public Polygon(ILineSegment segment) + { + this.innerPath = new InternalPath(segment, true); + this.pathCollection = new[] { this }; + } + + /// + /// Gets the bounding box of this shape. + /// + /// + /// The bounds. + /// + public Rectangle Bounds => this.innerPath.Bounds; + + /// + /// Gets the length of the path + /// + /// + /// The length. + /// + public float Length => this.innerPath.Length; + + /// + /// Gets a value indicating whether this instance is closed. + /// + /// + /// true if this instance is closed; otherwise, false. + /// + public bool IsClosed => true; + + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => this.innerPath.Points.Length; + + /// + /// Gets the paths that make up this shape + /// + /// + /// The paths. + /// + public IEnumerable Paths => this.pathCollection; + + /// + /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds + /// + /// The point. + /// + /// The distance of the point away from the shape + /// + public float Distance(Point point) + { + bool isInside = this.innerPath.PointInPolygon(point); + + float distance = this.innerPath.DistanceFromPath(point).DistanceFromPath; + if (isInside) + { + return -distance; + } + + return distance; + } + + /// + /// Calcualtes the distance along and away from the path for a specified point. + /// + /// The point along the path. + /// + /// distance metadata about the point. + /// + PointInfo IPath.Distance(Point point) + { + return this.innerPath.DistanceFromPath(point); + } + + /// + /// Returns the current shape as a simple linear path. + /// + /// + /// Returns the current as simple linear path. + /// + public ImmutableArray AsSimpleLinearPath() + { + return this.innerPath.Points; + } + + /// + /// Based on a line described by and + /// populate a buffer for all points on the polygon that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Point start, Point end, Point[] buffer, int count, int offset) + { + return this.innerPath.FindIntersections(start, end, buffer, count, offset); + } + } +} diff --git a/src/Shaper2D/PolygonClipper/Clipper.cs b/src/Shaper2D/PolygonClipper/Clipper.cs new file mode 100644 index 0000000..2867acd --- /dev/null +++ b/src/Shaper2D/PolygonClipper/Clipper.cs @@ -0,0 +1,3858 @@ +// +// 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; + + /// + /// Library to clip polygons. + /// + internal class Clipper + { + private const double HorizontalDeltaLimit = -3.4E+38; + private const int Skip = -2; + private const int Unassigned = -1; // InitOptions that can be passed to the constructor ... + + private Maxima maxima; + private TEdge sortedEdges; + private List intersectList; + private IComparer intersectNodeComparer = new IntersectNodeSort(); + private bool executeLocked; + + private List joins; + private List ghostJoins; + private bool usingPolyTree; + private LocalMinima minimaList = null; + private LocalMinima currentLM = null; + private List> edges = new List>(); + private Scanbeam scanbeam; + private List polyOuts; + private TEdge activeEdges; + + /// + /// Initializes a new instance of the class. + /// + public Clipper() + { + this.scanbeam = null; + this.maxima = null; + this.activeEdges = null; + this.sortedEdges = null; + this.intersectList = new List(); + this.executeLocked = false; + this.usingPolyTree = false; + this.polyOuts = new List(); + this.joins = new List(); + this.ghostJoins = new List(); + } + + /// + /// Node types + /// + private enum NodeType + { + /// + /// Any + /// + Any, + + /// + /// The open + /// + Open, + + /// + /// The closed + /// + Closed + } + + /// + /// 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 pg = path.AsSimpleLinearPath(); + + int highI = pg.Length - 1; + while (highI > 0 && (pg[highI] == pg[0])) + { + --highI; + } + + while (highI > 0 && (pg[highI] == pg[highI - 1])) + { + --highI; + } + + if (highI < 2) + { + return false; + } + + // create a new edge array ... + List edges = new List(highI + 1); + for (int i = 0; i <= highI; i++) + { + edges.Add(new TEdge() { SourcePath = path }); + } + + bool isFlat = true; + + // 1. Basic (first) edge initialization ... + edges[1].Curr = pg[1]; + + InitEdge(edges[0], edges[1], edges[highI], pg[0]); + InitEdge(edges[highI], edges[0], edges[highI - 1], pg[highI]); + for (int i = highI - 1; i >= 1; --i) + { + InitEdge(edges[i], edges[i + 1], edges[i - 1], pg[i]); + } + + TEdge eStart = edges[0]; + + // 2. Remove duplicate vertices, and (when closed) collinear edges ... + TEdge edge = eStart, eLoopStop = eStart; + while (true) + { + // nb: allows matching start and end points when not Closed ... + if (edge.Curr == edge.Next.Curr) + { + if (edge == edge.Next) + { + break; + } + + if (edge == eStart) + { + eStart = edge.Next; + } + + edge = RemoveEdge(edge); + eLoopStop = edge; + continue; + } + + if (edge.Prev == edge.Next) + { + break; // only two vertices + } + else if (SlopesEqual(edge.Prev.Curr, edge.Curr, edge.Next.Curr)) + { + // 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 == eStart) + { + eStart = edge.Next; + } + + edge = RemoveEdge(edge); + edge = edge.Prev; + eLoopStop = edge; + continue; + } + + edge = edge.Next; + if (edge == eLoopStop) + { + break; + } + } + + if (edge.Prev == edge.Next) + { + return false; + } + + // 3. Do second stage of edge initialization ... + edge = eStart; + do + { + this.InitEdge2(edge, polyType); + edge = edge.Next; + if (isFlat && edge.Curr.Y != eStart.Curr.Y) + { + isFlat = false; + } + } + while (edge != eStart); + + // 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); + bool leftBoundIsForward; + TEdge emIn = null; + + // workaround to avoid an endless loop in the while loop below when + // open paths have matching start and end points ... + if (edge.Prev.Bot == edge.Prev.Top) + { + edge = edge.Next; + } + + while (true) + { + edge = FindNextLocMin(edge); + if (edge == emIn) + { + break; + } + else if (emIn == null) + { + emIn = 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(); + locMin.Next = null; + locMin.Y = edge.Bot.Y; + if (edge.Dx < edge.Prev.Dx) + { + locMin.LeftBound = edge.Prev; + locMin.RightBound = edge; + leftBoundIsForward = false; // Q.nextInLML = Q.prev + } + else + { + locMin.LeftBound = edge; + locMin.RightBound = edge.Prev; + leftBoundIsForward = true; // Q.nextInLML = Q.next + } + + locMin.LeftBound.Side = EdgeSide.Left; + locMin.RightBound.Side = EdgeSide.Right; + + if (locMin.LeftBound.Next == locMin.RightBound) + { + locMin.LeftBound.WindDelta = -1; + } + else + { + locMin.LeftBound.WindDelta = 1; + } + + locMin.RightBound.WindDelta = -locMin.LeftBound.WindDelta; + + edge = this.ProcessBound(locMin.LeftBound, leftBoundIsForward); + if (edge.OutIdx == Skip) + { + edge = this.ProcessBound(edge, leftBoundIsForward); + } + + TEdge edge2 = this.ProcessBound(locMin.RightBound, !leftBoundIsForward); + if (edge2.OutIdx == Skip) + { + edge2 = this.ProcessBound(edge2, !leftBoundIsForward); + } + + if (locMin.LeftBound.OutIdx == Skip) + { + locMin.LeftBound = null; + } + else if (locMin.RightBound.OutIdx == Skip) + { + locMin.RightBound = null; + } + + this.InsertLocalMinima(locMin); + if (!leftBoundIsForward) + { + edge = edge2; + } + } + + return true; + } + + /// + /// Executes the specified clip type. + /// + /// + /// Returns the polytree containing the converted polygons. + /// + public PolyTree Execute() + { + PolyTree polytree = new PolyTree(); + + if (this.executeLocked) + { + return null; + } + + this.executeLocked = true; + this.usingPolyTree = true; + bool succeeded; + try + { + succeeded = this.ExecuteInternal(); + + // build the return polygons ... + if (succeeded) + { + this.BuildResult2(polytree); + } + } + finally + { + this.DisposeAllPolyPts(); + this.executeLocked = false; + } + + if (succeeded) + { + return polytree; + } + + return null; + } + + private static float Round(double value) + { + return value < 0 ? (float)(value - 0.5) : (float)(value + 0.5); + } + + private static float TopX(TEdge edge, float currentY) + { + if (currentY == edge.Top.Y) + { + return edge.Top.X; + } + + return edge.Bot.X + Round(edge.Dx * (currentY - edge.Bot.Y)); + } + + private static void AddPolyNodeToPaths(PolyNode polynode, NodeType nt, List> paths) + { + bool match = true; + switch (nt) + { + case NodeType.Open: return; + case NodeType.Closed: match = !polynode.IsOpen; break; + default: break; + } + + if (polynode.Polygon.Count > 0 && match) + { + paths.Add(polynode.Polygon); + } + + foreach (PolyNode pn in polynode.Children) + { + AddPolyNodeToPaths(pn, nt, paths); + } + } + + private static double DistanceFromLineSqrd(Vector2 pt, Vector2 ln1, Vector2 ln2) + { + // The equation of a line in general form (Ax + By + C = 0) + // given 2 points (x¹,y¹) & (x²,y²) is ... + // (y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0 + // A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹ + // perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) + // see http://en.wikipedia.org/wiki/Perpendicular_distance + double a = ln1.Y - ln2.Y; + double b = ln2.X - ln1.X; + double c = (a * ln1.X) + (b * ln1.Y); + c = (a * pt.X) + (b * pt.Y) - c; + return (c * c) / ((a * a) + (b * b)); + } + + private static bool SlopesNearCollinear(Vector2 pt1, Vector2 pt2, Vector2 pt3, double distSqrd) + { + // this function is more accurate when the point that's GEOMETRICALLY + // between the other 2 points is the one that's tested for distance. + // nb: with 'spikes', either pt1 or pt3 is geometrically between the other pts + if (Math.Abs(pt1.X - pt2.X) > Math.Abs(pt1.Y - pt2.Y)) + { + if ((pt1.X > pt2.X) == (pt1.X < pt3.X)) + { + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + } + else if ((pt2.X > pt1.X) == (pt2.X < pt3.X)) + { + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + } + else + { + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } + } + else + { + if ((pt1.Y > pt2.Y) == (pt1.Y < pt3.Y)) + { + return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; + } + else if ((pt2.Y > pt1.Y) == (pt2.Y < pt3.Y)) + { + return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + } + else + { + return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; + } + } + } + + private static bool PointsAreClose(Vector2 pt1, Vector2 pt2, double distSqrd) + { + return Vector2.DistanceSquared(pt1, pt2) <= distSqrd; + } + + private static OutPt ExcludeOp(OutPt op) + { + OutPt result = op.Prev; + result.Next = op.Next; + op.Next.Prev = result; + result.Idx = 0; + return result; + } + + private static void FixHoleLinkage(OutRec outRec) + { + // skip if an outermost polygon or + // already already points to the correct FirstLeft ... + if (outRec.FirstLeft == null || + (outRec.IsHole != outRec.FirstLeft.IsHole && + outRec.FirstLeft.Pts != null)) + { + return; + } + + OutRec orfl = outRec.FirstLeft; + while (orfl != null && ((orfl.IsHole == outRec.IsHole) || orfl.Pts == null)) + { + orfl = orfl.FirstLeft; + } + + outRec.FirstLeft = orfl; + } + + // See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos + // http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf + private static int PointInPolygon(Vector2 pt, OutPt op) + { + // returns 0 if false, +1 if true, -1 if pt ON polygon boundary + int result = 0; + OutPt startOp = op; + float ptx = pt.X, pty = pt.Y; + float poly0x = op.Pt.X, poly0y = op.Pt.Y; + do + { + op = op.Next; + float poly1x = op.Pt.X, poly1y = op.Pt.Y; + + if (poly1y == pty) + { + if ((poly1x == ptx) || (poly0y == pty && + ((poly1x > ptx) == (poly0x < ptx)))) + { + return -1; + } + } + + if ((poly0y < pty) != (poly1y < pty)) + { + if (poly0x >= ptx) + { + if (poly1x > ptx) + { + result = 1 - result; + } + else + { + double d = (double)((poly0x - ptx) * (poly1y - pty)) - + (double)((poly1x - ptx) * (poly0y - pty)); + if (d == 0) + { + return -1; + } + + if ((d > 0) == (poly1y > poly0y)) + { + result = 1 - result; + } + } + } + else + { + if (poly1x > ptx) + { + double d = (double)((poly0x - ptx) * (poly1y - pty)) - (double)((poly1x - ptx) * (poly0y - pty)); + if (d == 0) + { + return -1; + } + + if ((d > 0) == (poly1y > poly0y)) + { + result = 1 - result; + } + } + } + } + + poly0x = poly1x; + poly0y = poly1y; + } + while (startOp != op); + + return result; + } + + private static bool Poly2ContainsPoly1(OutPt outPt1, OutPt outPt2) + { + OutPt op = outPt1; + do + { + // nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon + int res = PointInPolygon(op.Pt, outPt2); + if (res >= 0) + { + return res > 0; + } + + op = op.Next; + } + while (op != outPt1); + return true; + } + + private static void SwapSides(TEdge edge1, TEdge edge2) + { + EdgeSide side = edge1.Side; + edge1.Side = edge2.Side; + edge2.Side = side; + } + + private static void SwapPolyIndexes(TEdge edge1, TEdge edge2) + { + int outIdx = edge1.OutIdx; + edge1.OutIdx = edge2.OutIdx; + edge2.OutIdx = outIdx; + } + + private static double GetDx(Vector2 pt1, Vector2 pt2) + { + if (pt1.Y == pt2.Y) + { + return HorizontalDeltaLimit; + } + else + { + return (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y); + } + } + + private static bool HorzSegmentsOverlap(float seg1a, float seg1b, float seg2a, float seg2b) + { + if (seg1a > seg1b) + { + Swap(ref seg1a, ref seg1b); + } + + if (seg2a > seg2b) + { + Swap(ref seg2a, ref seg2b); + } + + return (seg1a < seg2b) && (seg2a < seg1b); + } + + private static TEdge FindNextLocMin(TEdge edge) + { + TEdge edge2; + while (true) + { + while (edge.Bot != edge.Prev.Bot || edge.Curr == edge.Top) + { + edge = edge.Next; + } + + if (edge.Dx != HorizontalDeltaLimit && edge.Prev.Dx != HorizontalDeltaLimit) + { + break; + } + + while (edge.Prev.Dx == HorizontalDeltaLimit) + { + edge = edge.Prev; + } + + edge2 = edge; + while (edge.Dx == HorizontalDeltaLimit) + { + edge = edge.Next; + } + + if (edge.Top.Y == edge.Prev.Bot.Y) + { + continue; // ie just an intermediate horz. + } + + if (edge2.Prev.Bot.X < edge.Bot.X) + { + edge = edge2; + } + + break; + } + + return edge; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Swap(ref float val1, ref float val2) + { + float tmp = val1; + val1 = val2; + val2 = tmp; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHorizontal(TEdge e) + { + return e.Delta.Y == 0; + } + + private static bool SlopesEqual(TEdge e1, TEdge e2) + { + return e1.Delta.Y * e2.Delta.X == e1.Delta.X * e2.Delta.Y; + } + + private static bool SlopesEqual(Vector2 pt1, Vector2 pt2, Vector2 pt3) + { + var dif12 = pt1 - pt2; + var dif23 = pt2 - pt3; + return (dif12.Y * dif23.X) - (dif12.X * dif23.Y) == 0; + } + + private static bool SlopesEqual(Vector2 pt1, Vector2 pt2, Vector2 pt3, Vector2 pt4) + { + var dif12 = pt1 - pt2; + var dif34 = pt3 - pt4; + + return (dif12.Y * dif34.X) - (dif12.X * dif34.Y) == 0; + } + + private static void InitEdge(TEdge e, TEdge eNext, TEdge ePrev, Vector2 pt) + { + e.Next = eNext; + e.Prev = ePrev; + e.Curr = pt; + e.OutIdx = Unassigned; + } + + private static OutRec ParseFirstLeft(OutRec firstLeft) + { + while (firstLeft != null && firstLeft.Pts == null) + { + firstLeft = firstLeft.FirstLeft; + } + + return firstLeft; + } + + private static bool Pt2IsBetweenPt1AndPt3(Vector2 pt1, Vector2 pt2, Vector2 pt3) + { + if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2)) + { + return false; + } + else if (pt1.X != pt3.X) + { + return (pt2.X > pt1.X) == (pt2.X < pt3.X); + } + else + { + return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); + } + } + + private static TEdge RemoveEdge(TEdge e) + { + // removes e from double_linked_list (but without removing from memory) + e.Prev.Next = e.Next; + e.Next.Prev = e.Prev; + TEdge result = e.Next; + e.Prev = null; // flag as removed (see ClipperBase.Clear) + return result; + } + + private static void ReverseHorizontal(TEdge e) + { + // swap horizontal edges' top and bottom x's so they follow the natural + // progression of the bounds - ie so their xbots will align with the + // adjoining lower edge. [Helpful in the ProcessHorizontal() method.] + Swap(ref e.Top.X, ref e.Bot.X); + } + + private void InsertMaxima(float x) + { + // double-linked list: sorted ascending, ignoring dups. + Maxima newMax = new Maxima(); + newMax.X = x; + if (this.maxima == null) + { + this.maxima = newMax; + this.maxima.Next = null; + this.maxima.Prev = null; + } + else if (x < this.maxima.X) + { + newMax.Next = this.maxima; + newMax.Prev = null; + this.maxima = newMax; + } + else + { + Maxima m = this.maxima; + while (m.Next != null && (x >= m.Next.X)) + { + m = m.Next; + } + + if (x == m.X) + { + return; // ie ignores duplicates (& CG to clean up newMax) + } + + // insert newMax between m and m.Next ... + newMax.Next = m.Next; + newMax.Prev = m; + if (m.Next != null) + { + m.Next.Prev = newMax; + } + + m.Next = newMax; + } + } + + private bool ExecuteInternal() + { + try + { + this.Reset(); + this.sortedEdges = null; + this.maxima = null; + + float botY, topY; + if (!this.PopScanbeam(out botY)) + { + return false; + } + + this.InsertLocalMinimaIntoAEL(botY); + while (this.PopScanbeam(out topY) || this.LocalMinimaPending()) + { + this.ProcessHorizontals(); + this.ghostJoins.Clear(); + if (!this.ProcessIntersections(topY)) + { + return false; + } + + this.ProcessEdgesAtTopOfScanbeam(topY); + botY = topY; + this.InsertLocalMinimaIntoAEL(botY); + } + + // fix orientations ... + foreach (OutRec outRec in this.polyOuts) + { + if (outRec.Pts == null || outRec.IsOpen) + { + continue; + } + } + + this.JoinCommonEdges(); + + foreach (OutRec outRec in this.polyOuts) + { + if (outRec.Pts == null) + { + continue; + } + else if (outRec.IsOpen) + { + this.FixupOutPolyline(outRec); + } + else + { + this.FixupOutPolygon(outRec); + } + } + + return true; + } + finally + { + this.joins.Clear(); + this.ghostJoins.Clear(); + } + } + + private void DisposeAllPolyPts() + { + for (int i = 0; i < this.polyOuts.Count; ++i) + { + this.DisposeOutRec(i); + } + + this.polyOuts.Clear(); + } + + private void AddJoin(OutPt op1, OutPt op2, Vector2 offPt) + { + Join j = new Join(); + j.OutPt1 = op1; + j.OutPt2 = op2; + j.OffPt = offPt; + this.joins.Add(j); + } + + private void AddGhostJoin(OutPt op, Vector2 offPt) + { + Join j = new Join(); + j.OutPt1 = op; + j.OffPt = offPt; + this.ghostJoins.Add(j); + } + + private void InsertLocalMinimaIntoAEL(float botY) + { + LocalMinima lm; + while (this.PopLocalMinima(botY, out lm)) + { + TEdge lb = lm.LeftBound; + TEdge rb = lm.RightBound; + + OutPt op1 = null; + if (lb == null) + { + this.InsertEdgeIntoAEL(rb, null); + this.SetWindingCount(rb); + if (this.IsContributing(rb)) + { + op1 = this.AddOutPt(rb, rb.Bot); + } + } + else if (rb == null) + { + this.InsertEdgeIntoAEL(lb, null); + this.SetWindingCount(lb); + if (this.IsContributing(lb)) + { + op1 = this.AddOutPt(lb, lb.Bot); + } + + this.InsertScanbeam(lb.Top.Y); + } + else + { + this.InsertEdgeIntoAEL(lb, null); + this.InsertEdgeIntoAEL(rb, lb); + this.SetWindingCount(lb); + rb.WindCnt = lb.WindCnt; + rb.WindCnt2 = lb.WindCnt2; + if (this.IsContributing(lb)) + { + op1 = this.AddLocalMinPoly(lb, rb, lb.Bot); + } + + this.InsertScanbeam(lb.Top.Y); + } + + if (rb != null) + { + if (IsHorizontal(rb)) + { + if (rb.NextInLML != null) + { + this.InsertScanbeam(rb.NextInLML.Top.Y); + } + + this.AddEdgeToSEL(rb); + } + else + { + this.InsertScanbeam(rb.Top.Y); + } + } + + if (lb == null || rb == null) + { + continue; + } + + // if output polygons share an Edge with a horizontal rb, they'll need joining later ... + if (op1 != null && IsHorizontal(rb) && + this.ghostJoins.Count > 0 && rb.WindDelta != 0) + { + for (int i = 0; i < this.ghostJoins.Count; i++) + { + // if the horizontal Rb and a 'ghost' horizontal overlap, then convert + // the 'ghost' join to a real join ready for later ... + Join j = this.ghostJoins[i]; + if (HorzSegmentsOverlap(j.OutPt1.Pt.X, j.OffPt.X, rb.Bot.X, rb.Top.X)) + { + this.AddJoin(j.OutPt1, op1, j.OffPt); + } + } + } + + if (lb.OutIdx >= 0 && lb.PrevInAEL != null && + lb.PrevInAEL.Curr.X == lb.Bot.X && + lb.PrevInAEL.OutIdx >= 0 && + SlopesEqual(lb.PrevInAEL.Curr, lb.PrevInAEL.Top, lb.Curr, lb.Top) && + lb.WindDelta != 0 && lb.PrevInAEL.WindDelta != 0) + { + OutPt op2 = this.AddOutPt(lb.PrevInAEL, lb.Bot); + this.AddJoin(op1, op2, lb.Top); + } + + if (lb.NextInAEL != rb) + { + if (rb.OutIdx >= 0 && rb.PrevInAEL.OutIdx >= 0 && + SlopesEqual(rb.PrevInAEL.Curr, rb.PrevInAEL.Top, rb.Curr, rb.Top) && + rb.WindDelta != 0 && rb.PrevInAEL.WindDelta != 0) + { + OutPt op2 = this.AddOutPt(rb.PrevInAEL, rb.Bot); + this.AddJoin(op1, op2, rb.Top); + } + + TEdge e = lb.NextInAEL; + if (e != null) + { + while (e != rb) + { + // nb: For calculating winding counts etc, IntersectEdges() assumes + // that param1 will be to the right of param2 ABOVE the intersection ... + this.IntersectEdges(rb, e, lb.Curr); // order important here + e = e.NextInAEL; + } + } + } + } + } + + private void InsertEdgeIntoAEL(TEdge edge, TEdge startEdge) + { + if (this.activeEdges == null) + { + edge.PrevInAEL = null; + edge.NextInAEL = null; + this.activeEdges = edge; + } + else if (startEdge == null && this.E2InsertsBeforeE1(this.activeEdges, edge)) + { + edge.PrevInAEL = null; + edge.NextInAEL = this.activeEdges; + this.activeEdges.PrevInAEL = edge; + this.activeEdges = edge; + } + else + { + if (startEdge == null) + { + startEdge = this.activeEdges; + } + + while (startEdge.NextInAEL != null && + !this.E2InsertsBeforeE1(startEdge.NextInAEL, edge)) + { + startEdge = startEdge.NextInAEL; + } + + edge.NextInAEL = startEdge.NextInAEL; + if (startEdge.NextInAEL != null) + { + startEdge.NextInAEL.PrevInAEL = edge; + } + + edge.PrevInAEL = startEdge; + startEdge.NextInAEL = edge; + } + } + + private bool E2InsertsBeforeE1(TEdge e1, TEdge e2) + { + if (e2.Curr.X == e1.Curr.X) + { + if (e2.Top.Y > e1.Top.Y) + { + return e2.Top.X < TopX(e1, e2.Top.Y); + } + else + { + return e1.Top.X > TopX(e2, e1.Top.Y); + } + } + else + { + return e2.Curr.X < e1.Curr.X; + } + } + + private bool IsContributing(TEdge edge) + { + // return false if a subj line has been flagged as inside a subj polygon + if (edge.WindDelta == 0 && edge.WindCnt != 1) + { + return false; + } + + if (edge.PolyTyp == PolyType.Subject) + { + return edge.WindCnt2 == 0; + } + else + { + return edge.WindCnt2 != 0; + } + } + + private void SetWindingCount(TEdge edge) + { + TEdge e = edge.PrevInAEL; + + // find the edge of the same polytype that immediately preceeds 'edge' in AEL + while (e != null && ((e.PolyTyp != edge.PolyTyp) || (e.WindDelta == 0))) + { + e = e.PrevInAEL; + } + + if (e == null) + { + if (edge.WindDelta == 0) + { + edge.WindCnt = 1; + } + else + { + edge.WindCnt = edge.WindDelta; + } + + edge.WindCnt2 = 0; + e = this.activeEdges; // ie get ready to calc WindCnt2 + } + else if (edge.WindDelta == 0) + { + edge.WindCnt = 1; + edge.WindCnt2 = e.WindCnt2; + e = e.NextInAEL; // ie get ready to calc WindCnt2 + } + else + { + // EvenOdd filling ... + if (edge.WindDelta == 0) + { + // are we inside a subj polygon ... + bool inside = true; + TEdge e2 = e.PrevInAEL; + while (e2 != null) + { + if (e2.PolyTyp == e.PolyTyp && e2.WindDelta != 0) + { + inside = !inside; + } + + e2 = e2.PrevInAEL; + } + + edge.WindCnt = inside ? 0 : 1; + } + else + { + edge.WindCnt = edge.WindDelta; + } + + edge.WindCnt2 = e.WindCnt2; + e = e.NextInAEL; // ie get ready to calc WindCnt2 + } + + // update WindCnt2 ... + // EvenOdd filling ... + while (e != edge) + { + if (e.WindDelta != 0) + { + edge.WindCnt2 = edge.WindCnt2 == 0 ? 1 : 0; + } + + e = e.NextInAEL; + } + } + + private void AddEdgeToSEL(TEdge edge) + { + // SEL pointers in PEdge are use to build transient lists of horizontal edges. + // However, since we don't need to worry about processing order, all additions + // are made to the front of the list ... + if (this.sortedEdges == null) + { + this.sortedEdges = edge; + edge.PrevInSEL = null; + edge.NextInSEL = null; + } + else + { + edge.NextInSEL = this.sortedEdges; + edge.PrevInSEL = null; + this.sortedEdges.PrevInSEL = edge; + this.sortedEdges = edge; + } + } + + private bool PopEdgeFromSEL(out TEdge e) + { + // Pop edge from front of SEL (ie SEL is a FILO list) + e = this.sortedEdges; + if (e == null) + { + return false; + } + + TEdge oldE = e; + this.sortedEdges = e.NextInSEL; + if (this.sortedEdges != null) + { + this.sortedEdges.PrevInSEL = null; + } + + oldE.NextInSEL = null; + oldE.PrevInSEL = null; + return true; + } + + private void CopyAELToSEL() + { + TEdge e = this.activeEdges; + this.sortedEdges = e; + while (e != null) + { + e.PrevInSEL = e.PrevInAEL; + e.NextInSEL = e.NextInAEL; + e = e.NextInAEL; + } + } + + private void SwapPositionsInSEL(TEdge edge1, TEdge edge2) + { + if (edge1.NextInSEL == null && edge1.PrevInSEL == null) + { + return; + } + + if (edge2.NextInSEL == null && edge2.PrevInSEL == null) + { + return; + } + + if (edge1.NextInSEL == edge2) + { + TEdge next = edge2.NextInSEL; + if (next != null) + { + next.PrevInSEL = edge1; + } + + TEdge prev = edge1.PrevInSEL; + if (prev != null) + { + prev.NextInSEL = edge2; + } + + edge2.PrevInSEL = prev; + edge2.NextInSEL = edge1; + edge1.PrevInSEL = edge2; + edge1.NextInSEL = next; + } + else if (edge2.NextInSEL == edge1) + { + TEdge next = edge1.NextInSEL; + if (next != null) + { + next.PrevInSEL = edge2; + } + + TEdge prev = edge2.PrevInSEL; + if (prev != null) + { + prev.NextInSEL = edge1; + } + + edge1.PrevInSEL = prev; + edge1.NextInSEL = edge2; + edge2.PrevInSEL = edge1; + edge2.NextInSEL = next; + } + else + { + TEdge next = edge1.NextInSEL; + TEdge prev = edge1.PrevInSEL; + edge1.NextInSEL = edge2.NextInSEL; + if (edge1.NextInSEL != null) + { + edge1.NextInSEL.PrevInSEL = edge1; + } + + edge1.PrevInSEL = edge2.PrevInSEL; + if (edge1.PrevInSEL != null) + { + edge1.PrevInSEL.NextInSEL = edge1; + } + + edge2.NextInSEL = next; + if (edge2.NextInSEL != null) + { + edge2.NextInSEL.PrevInSEL = edge2; + } + + edge2.PrevInSEL = prev; + if (edge2.PrevInSEL != null) + { + edge2.PrevInSEL.NextInSEL = edge2; + } + } + + if (edge1.PrevInSEL == null) + { + this.sortedEdges = edge1; + } + else if (edge2.PrevInSEL == null) + { + this.sortedEdges = edge2; + } + } + + private void AddLocalMaxPoly(TEdge e1, TEdge e2, Vector2 pt) + { + this.AddOutPt(e1, pt); + if (e2.WindDelta == 0) + { + this.AddOutPt(e2, pt); + } + + if (e1.OutIdx == e2.OutIdx) + { + e1.OutIdx = Unassigned; + e2.OutIdx = Unassigned; + } + else if (e1.OutIdx < e2.OutIdx) + { + this.AppendPolygon(e1, e2); + } + else + { + this.AppendPolygon(e2, e1); + } + } + + private OutPt AddLocalMinPoly(TEdge e1, TEdge e2, Vector2 pt) + { + OutPt result; + TEdge e, prevE; + if (IsHorizontal(e2) || (e1.Dx > e2.Dx)) + { + result = this.AddOutPt(e1, pt); + e2.OutIdx = e1.OutIdx; + e1.Side = EdgeSide.Left; + e2.Side = EdgeSide.Right; + e = e1; + if (e.PrevInAEL == e2) + { + prevE = e2.PrevInAEL; + } + else + { + prevE = e.PrevInAEL; + } + } + else + { + result = this.AddOutPt(e2, pt); + e1.OutIdx = e2.OutIdx; + e1.Side = EdgeSide.Right; + e2.Side = EdgeSide.Left; + e = e2; + if (e.PrevInAEL == e1) + { + prevE = e1.PrevInAEL; + } + else + { + prevE = e.PrevInAEL; + } + } + + if (prevE != null && prevE.OutIdx >= 0) + { + float xPrev = TopX(prevE, pt.Y); + float xE = TopX(e, pt.Y); + if ((xPrev == xE) && + (e.WindDelta != 0) && + (prevE.WindDelta != 0) && + SlopesEqual(new Vector2(xPrev, pt.Y), prevE.Top, new Vector2(xE, pt.Y), e.Top)) + { + OutPt outPt = this.AddOutPt(prevE, pt); + this.AddJoin(result, outPt, e.Top); + } + } + + return result; + } + + private OutPt AddOutPt(TEdge e, Vector2 pt) + { + if (e.OutIdx < 0) + { + OutRec outRec = this.CreateOutRec(); + outRec.SourcePath = e.SourcePath; // copy source from edge to outrec + outRec.IsOpen = e.WindDelta == 0; + OutPt newOp = new OutPt(); + outRec.Pts = newOp; + newOp.Idx = outRec.Idx; + newOp.Pt = pt; + newOp.Next = newOp; + newOp.Prev = newOp; + if (!outRec.IsOpen) + { + this.SetHoleState(e, outRec); + } + + e.OutIdx = outRec.Idx; // nb: do this after SetZ ! + return newOp; + } + else + { + OutRec outRec = this.polyOuts[e.OutIdx]; + + if (outRec.SourcePath != e.SourcePath) + { + // this edge was from a different/unknown source + outRec.SourcePath = null; // drop source form output + } + + // OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' + OutPt op = outRec.Pts; + bool toFront = e.Side == EdgeSide.Left; + if (toFront && pt == op.Pt) + { + return op; + } + else if (!toFront && pt == op.Prev.Pt) + { + return op.Prev; + } + + // do we need to move the source to the point??? + OutPt newOp = new OutPt(); + newOp.Idx = outRec.Idx; + newOp.Pt = pt; + newOp.Next = op; + newOp.Prev = op.Prev; + newOp.Prev.Next = newOp; + op.Prev = newOp; + if (toFront) + { + outRec.Pts = newOp; + } + + return newOp; + } + } + + private OutPt GetLastOutPt(TEdge e) + { + OutRec outRec = this.polyOuts[e.OutIdx]; + if (e.Side == EdgeSide.Left) + { + return outRec.Pts; + } + else + { + return outRec.Pts.Prev; + } + } + + private void SetHoleState(TEdge e, OutRec outRec) + { + TEdge e2 = e.PrevInAEL; + TEdge eTmp = null; + while (e2 != null) + { + if (e2.OutIdx >= 0 && e2.WindDelta != 0) + { + if (eTmp == null) + { + eTmp = e2; + } + else if (eTmp.OutIdx == e2.OutIdx) + { + eTmp = null; // paired + } + } + + e2 = e2.PrevInAEL; + } + + if (eTmp == null) + { + outRec.FirstLeft = null; + outRec.IsHole = false; + } + else + { + outRec.FirstLeft = this.polyOuts[eTmp.OutIdx]; + outRec.IsHole = !outRec.FirstLeft.IsHole; + } + } + + private bool FirstIsBottomPt(OutPt btmPt1, OutPt btmPt2) + { + OutPt p = btmPt1.Prev; + while ((p.Pt == btmPt1.Pt) && (p != btmPt1)) + { + p = p.Prev; + } + + double dx1p = Math.Abs(GetDx(btmPt1.Pt, p.Pt)); + p = btmPt1.Next; + while ((p.Pt == btmPt1.Pt) && (p != btmPt1)) + { + p = p.Next; + } + + double dx1n = Math.Abs(GetDx(btmPt1.Pt, p.Pt)); + + p = btmPt2.Prev; + while ((p.Pt == btmPt2.Pt) && (p != btmPt2)) + { + p = p.Prev; + } + + double dx2p = Math.Abs(GetDx(btmPt2.Pt, p.Pt)); + p = btmPt2.Next; + while ((p.Pt == btmPt2.Pt) && (p != btmPt2)) + { + p = p.Next; + } + + double dx2n = Math.Abs(GetDx(btmPt2.Pt, p.Pt)); + + if (Math.Max(dx1p, dx1n) == Math.Max(dx2p, dx2n) && + Math.Min(dx1p, dx1n) == Math.Min(dx2p, dx2n)) + { + return this.Area(btmPt1) > 0; // if otherwise identical use orientation + } + else + { + return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); + } + } + + private OutPt GetBottomPt(OutPt pp) + { + OutPt dups = null; + OutPt p = pp.Next; + while (p != pp) + { + if (p.Pt.Y > pp.Pt.Y) + { + pp = p; + dups = null; + } + else if (p.Pt.Y == pp.Pt.Y && p.Pt.X <= pp.Pt.X) + { + if (p.Pt.X < pp.Pt.X) + { + dups = null; + pp = p; + } + else + { + if (p.Next != pp && p.Prev != pp) + { + dups = p; + } + } + } + + p = p.Next; + } + + if (dups != null) + { + // there appears to be at least 2 vertices at bottomPt so ... + while (dups != p) + { + if (!this.FirstIsBottomPt(p, dups)) + { + pp = dups; + } + + dups = dups.Next; + while (dups.Pt != pp.Pt) + { + dups = dups.Next; + } + } + } + + return pp; + } + + private OutRec GetLowermostRec(OutRec outRec1, OutRec outRec2) + { + // work out which polygon fragment has the correct hole state ... + if (outRec1.BottomPt == null) + { + outRec1.BottomPt = this.GetBottomPt(outRec1.Pts); + } + + if (outRec2.BottomPt == null) + { + outRec2.BottomPt = this.GetBottomPt(outRec2.Pts); + } + + OutPt bPt1 = outRec1.BottomPt; + OutPt bPt2 = outRec2.BottomPt; + if (bPt1.Pt.Y > bPt2.Pt.Y) + { + return outRec1; + } + else if (bPt1.Pt.Y < bPt2.Pt.Y) + { + return outRec2; + } + else if (bPt1.Pt.X < bPt2.Pt.X) + { + return outRec1; + } + else if (bPt1.Pt.X > bPt2.Pt.X) + { + return outRec2; + } + else if (bPt1.Next == bPt1) + { + return outRec2; + } + else if (bPt2.Next == bPt2) + { + return outRec1; + } + else if (this.FirstIsBottomPt(bPt1, bPt2)) + { + return outRec1; + } + else + { + return outRec2; + } + } + + private bool OutRec1RightOfOutRec2(OutRec outRec1, OutRec outRec2) + { + do + { + outRec1 = outRec1.FirstLeft; + if (outRec1 == outRec2) + { + return true; + } + } + while (outRec1 != null); + + return false; + } + + private OutRec GetOutRec(int idx) + { + OutRec outrec = this.polyOuts[idx]; + while (outrec != this.polyOuts[outrec.Idx]) + { + outrec = this.polyOuts[outrec.Idx]; + } + + return outrec; + } + + private void AppendPolygon(TEdge e1, TEdge e2) + { + OutRec outRec1 = this.polyOuts[e1.OutIdx]; + OutRec outRec2 = this.polyOuts[e2.OutIdx]; + + OutRec holeStateRec; + if (this.OutRec1RightOfOutRec2(outRec1, outRec2)) + { + holeStateRec = outRec2; + } + else if (this.OutRec1RightOfOutRec2(outRec2, outRec1)) + { + holeStateRec = outRec1; + } + else + { + holeStateRec = this.GetLowermostRec(outRec1, outRec2); + } + + // get the start and ends of both output polygons and + // join E2 poly onto E1 poly and delete pointers to E2 ... + OutPt p1_lft = outRec1.Pts; + OutPt p1_rt = p1_lft.Prev; + OutPt p2_lft = outRec2.Pts; + OutPt p2_rt = p2_lft.Prev; + + // join e2 poly onto e1 poly and delete pointers to e2 ... + if (e1.Side == EdgeSide.Left) + { + if (e2.Side == EdgeSide.Left) + { + // z y x a b c + this.ReversePolyPtLinks(p2_lft); + p2_lft.Next = p1_lft; + p1_lft.Prev = p2_lft; + p1_rt.Next = p2_rt; + p2_rt.Prev = p1_rt; + outRec1.Pts = p2_rt; + } + else + { + // x y z a b c + p2_rt.Next = p1_lft; + p1_lft.Prev = p2_rt; + p2_lft.Prev = p1_rt; + p1_rt.Next = p2_lft; + outRec1.Pts = p2_lft; + } + } + else + { + if (e2.Side == EdgeSide.Right) + { + // a b c z y x + this.ReversePolyPtLinks(p2_lft); + p1_rt.Next = p2_rt; + p2_rt.Prev = p1_rt; + p2_lft.Next = p1_lft; + p1_lft.Prev = p2_lft; + } + else + { + // a b c x y z + p1_rt.Next = p2_lft; + p2_lft.Prev = p1_rt; + p1_lft.Prev = p2_rt; + p2_rt.Next = p1_lft; + } + } + + outRec1.BottomPt = null; + if (holeStateRec == outRec2) + { + if (outRec2.FirstLeft != outRec1) + { + outRec1.FirstLeft = outRec2.FirstLeft; + } + + outRec1.IsHole = outRec2.IsHole; + } + + outRec2.Pts = null; + outRec2.BottomPt = null; + + outRec2.FirstLeft = outRec1; + + int okIdx = e1.OutIdx; + int obsoleteIdx = e2.OutIdx; + + e1.OutIdx = Unassigned; // nb: safe because we only get here via AddLocalMaxPoly + e2.OutIdx = Unassigned; + + TEdge e = this.activeEdges; + while (e != null) + { + if (e.OutIdx == obsoleteIdx) + { + e.OutIdx = okIdx; + e.Side = e1.Side; + break; + } + + e = e.NextInAEL; + } + + outRec2.Idx = outRec1.Idx; + } + + private void ReversePolyPtLinks(OutPt pp) + { + if (pp == null) + { + return; + } + + OutPt pp1; + OutPt pp2; + pp1 = pp; + do + { + pp2 = pp1.Next; + pp1.Next = pp1.Prev; + pp1.Prev = pp2; + pp1 = pp2; + } + while (pp1 != pp); + } + + private void IntersectEdges(TEdge e1, TEdge e2, Vector2 pt) + { + // e1 will be to the left of e2 BELOW the intersection. Therefore e1 is before + // e2 in AEL except when e1 is being inserted at the intersection point ... + bool e1Contributing = e1.OutIdx >= 0; + bool e2Contributing = e2.OutIdx >= 0; + + // update winding counts... + // assumes that e1 will be to the Right of e2 ABOVE the intersection + if (e1.PolyTyp == e2.PolyTyp) + { + int oldE1WindCnt = e1.WindCnt; + e1.WindCnt = e2.WindCnt; + e2.WindCnt = oldE1WindCnt; + } + else + { + e1.WindCnt2 = (e1.WindCnt2 == 0) ? 1 : 0; + e2.WindCnt2 = (e2.WindCnt2 == 0) ? 1 : 0; + } + + int e1Wc, e2Wc; + e1Wc = Math.Abs(e1.WindCnt); + e2Wc = Math.Abs(e2.WindCnt); + + if (e1Contributing && e2Contributing) + { + if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || + (e1.PolyTyp != e2.PolyTyp)) + { + this.AddLocalMaxPoly(e1, e2, pt); + } + else + { + this.AddOutPt(e1, pt); + this.AddOutPt(e2, pt); + SwapSides(e1, e2); + SwapPolyIndexes(e1, e2); + } + } + else if (e1Contributing) + { + if (e2Wc == 0 || e2Wc == 1) + { + this.AddOutPt(e1, pt); + SwapSides(e1, e2); + SwapPolyIndexes(e1, e2); + } + } + else if (e2Contributing) + { + if (e1Wc == 0 || e1Wc == 1) + { + this.AddOutPt(e2, pt); + SwapSides(e1, e2); + SwapPolyIndexes(e1, e2); + } + } + else if ((e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) + { + // neither edge is currently contributing ... + float e1Wc2, e2Wc2; + + e1Wc2 = Math.Abs(e1.WindCnt2); + e2Wc2 = Math.Abs(e2.WindCnt2); + + if (e1.PolyTyp != e2.PolyTyp) + { + this.AddLocalMinPoly(e1, e2, pt); + } + else if (e1Wc == 1 && e2Wc == 1) + { + if (((e1.PolyTyp == PolyType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1.PolyTyp == PolyType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + { + this.AddLocalMinPoly(e1, e2, pt); + } + } + else + { + SwapSides(e1, e2); + } + } + } + + private void ProcessHorizontals() + { + TEdge horzEdge; // m_SortedEdges; + while (this.PopEdgeFromSEL(out horzEdge)) + { + this.ProcessHorizontal(horzEdge); + } + } + + private void GetHorzDirection(TEdge horzEdge, out Direction dir, out float left, out float right) + { + if (horzEdge.Bot.X < horzEdge.Top.X) + { + left = horzEdge.Bot.X; + right = horzEdge.Top.X; + dir = Direction.LeftToRight; + } + else + { + left = horzEdge.Top.X; + right = horzEdge.Bot.X; + dir = Direction.RightToLeft; + } + } + + private void ProcessHorizontal(TEdge horzEdge) + { + Direction dir; + float horzLeft, horzRight; + bool isOpen = horzEdge.WindDelta == 0; + + this.GetHorzDirection(horzEdge, out dir, out horzLeft, out horzRight); + + TEdge eLastHorz = horzEdge, eMaxPair = null; + while (eLastHorz.NextInLML != null && IsHorizontal(eLastHorz.NextInLML)) + { + eLastHorz = eLastHorz.NextInLML; + } + + if (eLastHorz.NextInLML == null) + { + eMaxPair = this.GetMaximaPair(eLastHorz); + } + + Maxima currMax = this.maxima; + if (currMax != null) + { + // get the first maxima in range (X) ... + if (dir == Direction.LeftToRight) + { + while (currMax != null && currMax.X <= horzEdge.Bot.X) + { + currMax = currMax.Next; + } + + if (currMax != null && currMax.X >= eLastHorz.Top.X) + { + currMax = null; + } + } + else + { + while (currMax.Next != null && currMax.Next.X < horzEdge.Bot.X) + { + currMax = currMax.Next; + } + + if (currMax.X <= eLastHorz.Top.X) + { + currMax = null; + } + } + } + + OutPt op1 = null; + + // loop through consec. horizontal edges + while (true) + { + bool isLastHorz = horzEdge == eLastHorz; + TEdge e = this.GetNextInAEL(horzEdge, dir); + while (e != null) + { + // this code block inserts extra coords into horizontal edges (in output + // polygons) whereever maxima touch these horizontal edges. This helps + // 'simplifying' polygons (ie if the Simplify property is set). + if (currMax != null) + { + if (dir == Direction.LeftToRight) + { + while (currMax != null && currMax.X < e.Curr.X) + { + if (horzEdge.OutIdx >= 0 && !isOpen) + { + this.AddOutPt(horzEdge, new Vector2(currMax.X, horzEdge.Bot.Y)); + } + + currMax = currMax.Next; + } + } + else + { + while (currMax != null && currMax.X > e.Curr.X) + { + if (horzEdge.OutIdx >= 0 && !isOpen) + { + this.AddOutPt(horzEdge, new Vector2(currMax.X, horzEdge.Bot.Y)); + } + + currMax = currMax.Prev; + } + } + } + + if ((dir == Direction.LeftToRight && e.Curr.X > horzRight) || + (dir == Direction.RightToLeft && e.Curr.X < horzLeft)) + { + break; + } + + // Also break if we've got to the end of an intermediate horizontal edge ... + // nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. + if (e.Curr.X == horzEdge.Top.X && horzEdge.NextInLML != null && + e.Dx < horzEdge.NextInLML.Dx) + { + break; + } + + // note: may be done multiple times + if (horzEdge.OutIdx >= 0 && !isOpen) + { + op1 = this.AddOutPt(horzEdge, e.Curr); + TEdge eNextHorz = this.sortedEdges; + while (eNextHorz != null) + { + if (eNextHorz.OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge.Bot.X, horzEdge.Top.X, eNextHorz.Bot.X, eNextHorz.Top.X)) + { + OutPt op2 = this.GetLastOutPt(eNextHorz); + this.AddJoin(op2, op1, eNextHorz.Top); + } + + eNextHorz = eNextHorz.NextInSEL; + } + + this.AddGhostJoin(op1, horzEdge.Bot); + } + + // OK, so far we're still in range of the horizontal Edge but make sure + // we're at the last of consec. horizontals when matching with eMaxPair + if (e == eMaxPair && isLastHorz) + { + if (horzEdge.OutIdx >= 0) + { + this.AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge.Top); + } + + this.DeleteFromAEL(horzEdge); + this.DeleteFromAEL(eMaxPair); + return; + } + + if (dir == Direction.LeftToRight) + { + Vector2 pt = new Vector2(e.Curr.X, horzEdge.Curr.Y); + this.IntersectEdges(horzEdge, e, pt); + } + else + { + Vector2 pt = new Vector2(e.Curr.X, horzEdge.Curr.Y); + this.IntersectEdges(e, horzEdge, pt); + } + + TEdge eNext = this.GetNextInAEL(e, dir); + this.SwapPositionsInAEL(horzEdge, e); + e = eNext; + } // end while(e != null) + + // Break out of loop if HorzEdge.NextInLML is not also horizontal ... + if (horzEdge.NextInLML == null || !IsHorizontal(horzEdge.NextInLML)) + { + break; + } + + this.UpdateEdgeIntoAEL(ref horzEdge); + if (horzEdge.OutIdx >= 0) + { + this.AddOutPt(horzEdge, horzEdge.Bot); + } + + this.GetHorzDirection(horzEdge, out dir, out horzLeft, out horzRight); + } + + if (horzEdge.OutIdx >= 0 && op1 == null) + { + op1 = this.GetLastOutPt(horzEdge); + TEdge eNextHorz = this.sortedEdges; + while (eNextHorz != null) + { + if (eNextHorz.OutIdx >= 0 && + HorzSegmentsOverlap(horzEdge.Bot.X, horzEdge.Top.X, eNextHorz.Bot.X, eNextHorz.Top.X)) + { + OutPt op2 = this.GetLastOutPt(eNextHorz); + this.AddJoin(op2, op1, eNextHorz.Top); + } + + eNextHorz = eNextHorz.NextInSEL; + } + + this.AddGhostJoin(op1, horzEdge.Top); + } + + if (horzEdge.NextInLML != null) + { + if (horzEdge.OutIdx >= 0) + { + op1 = this.AddOutPt(horzEdge, horzEdge.Top); + + this.UpdateEdgeIntoAEL(ref horzEdge); + if (horzEdge.WindDelta == 0) + { + return; + } + + // nb: HorzEdge is no longer horizontal here + TEdge ePrev = horzEdge.PrevInAEL; + TEdge eNext = horzEdge.NextInAEL; + if (ePrev != null && ePrev.Curr.X == horzEdge.Bot.X && + ePrev.Curr.Y == horzEdge.Bot.Y && ePrev.WindDelta != 0 && + (ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y && + SlopesEqual(horzEdge, ePrev))) + { + OutPt op2 = this.AddOutPt(ePrev, horzEdge.Bot); + this.AddJoin(op1, op2, horzEdge.Top); + } + else if (eNext != null && eNext.Curr.X == horzEdge.Bot.X && + eNext.Curr.Y == horzEdge.Bot.Y && eNext.WindDelta != 0 && + eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y && + SlopesEqual(horzEdge, eNext)) + { + OutPt op2 = this.AddOutPt(eNext, horzEdge.Bot); + this.AddJoin(op1, op2, horzEdge.Top); + } + } + else + { + this.UpdateEdgeIntoAEL(ref horzEdge); + } + } + else + { + if (horzEdge.OutIdx >= 0) + { + this.AddOutPt(horzEdge, horzEdge.Top); + } + + this.DeleteFromAEL(horzEdge); + } + } + + private TEdge GetNextInAEL(TEdge e, Direction direction) + { + return direction == Direction.LeftToRight ? e.NextInAEL : e.PrevInAEL; + } + + private bool IsMaxima(TEdge e, double y) + { + return e != null && e.Top.Y == y && e.NextInLML == null; + } + + private bool IsIntermediate(TEdge e, double y) + { + return e.Top.Y == y && e.NextInLML != null; + } + + private TEdge GetMaximaPair(TEdge e) + { + if ((e.Next.Top == e.Top) && e.Next.NextInLML == null) + { + return e.Next; + } + else if ((e.Prev.Top == e.Top) && e.Prev.NextInLML == null) + { + return e.Prev; + } + else + { + return null; + } + } + + private TEdge GetMaximaPairEx(TEdge e) + { + // as above but returns null if MaxPair isn't in AEL (unless it's horizontal) + TEdge result = this.GetMaximaPair(e); + if (result == null || result.OutIdx == Skip || + ((result.NextInAEL == result.PrevInAEL) && !IsHorizontal(result))) + { + return null; + } + + return result; + } + + private bool ProcessIntersections(float topY) + { + if (this.activeEdges == null) + { + return true; + } + + try + { + this.BuildIntersectList(topY); + if (this.intersectList.Count == 0) + { + return true; + } + + if (this.intersectList.Count == 1 || this.FixupIntersectionOrder()) + { + this.ProcessIntersectList(); + } + else + { + return false; + } + } + catch + { + this.sortedEdges = null; + this.intersectList.Clear(); + throw new ClipperException("ProcessIntersections error"); + } + + this.sortedEdges = null; + return true; + } + + private void BuildIntersectList(float topY) + { + if (this.activeEdges == null) + { + return; + } + + // prepare for sorting ... + TEdge e = this.activeEdges; + this.sortedEdges = e; + while (e != null) + { + e.PrevInSEL = e.PrevInAEL; + e.NextInSEL = e.NextInAEL; + e.Curr.X = TopX(e, topY); + e = e.NextInAEL; + } + + // bubblesort ... + bool isModified = true; + while (isModified && this.sortedEdges != null) + { + isModified = false; + e = this.sortedEdges; + while (e.NextInSEL != null) + { + TEdge eNext = e.NextInSEL; + Vector2 pt; + if (e.Curr.X > eNext.Curr.X) + { + this.IntersectPoint(e, eNext, out pt); + if (pt.Y < topY) + { + pt = new Vector2(TopX(e, topY), topY); + } + + IntersectNode newNode = new IntersectNode(); + newNode.Edge1 = e; + newNode.Edge2 = eNext; + newNode.Pt = pt; + this.intersectList.Add(newNode); + + this.SwapPositionsInSEL(e, eNext); + isModified = true; + } + else + { + e = eNext; + } + } + + if (e.PrevInSEL != null) + { + e.PrevInSEL.NextInSEL = null; + } + else + { + break; + } + } + + this.sortedEdges = null; + } + + private bool EdgesAdjacent(IntersectNode inode) + { + return (inode.Edge1.NextInSEL == inode.Edge2) || + (inode.Edge1.PrevInSEL == inode.Edge2); + } + + private bool FixupIntersectionOrder() + { + // pre-condition: intersections are sorted bottom-most first. + // Now it's crucial that intersections are made only between adjacent edges, + // so to ensure this the order of intersections may need adjusting ... + this.intersectList.Sort(this.intersectNodeComparer); + + this.CopyAELToSEL(); + int cnt = this.intersectList.Count; + for (int i = 0; i < cnt; i++) + { + if (!this.EdgesAdjacent(this.intersectList[i])) + { + int j = i + 1; + while (j < cnt && !this.EdgesAdjacent(this.intersectList[j])) + { + j++; + } + + if (j == cnt) + { + return false; + } + + IntersectNode tmp = this.intersectList[i]; + this.intersectList[i] = this.intersectList[j]; + this.intersectList[j] = tmp; + } + + this.SwapPositionsInSEL(this.intersectList[i].Edge1, this.intersectList[i].Edge2); + } + + return true; + } + + private void ProcessIntersectList() + { + for (int i = 0; i < this.intersectList.Count; i++) + { + IntersectNode iNode = this.intersectList[i]; + { + this.IntersectEdges(iNode.Edge1, iNode.Edge2, iNode.Pt); + this.SwapPositionsInAEL(iNode.Edge1, iNode.Edge2); + } + } + + this.intersectList.Clear(); + } + + private void IntersectPoint(TEdge edge1, TEdge edge2, out Vector2 ip) + { + ip = default(Vector2); + double b1, b2; + + // nb: with very large coordinate values, it's possible for SlopesEqual() to + // return false but for the edge.Dx value be equal due to double precision rounding. + if (edge1.Dx == edge2.Dx) + { + ip.Y = edge1.Curr.Y; + ip.X = TopX(edge1, ip.Y); + return; + } + + if (edge1.Delta.X == 0) + { + ip.X = edge1.Bot.X; + if (IsHorizontal(edge2)) + { + ip.Y = edge2.Bot.Y; + } + else + { + b2 = edge2.Bot.Y - (edge2.Bot.X / edge2.Dx); + ip.Y = Round((ip.X / edge2.Dx) + b2); + } + } + else if (edge2.Delta.X == 0) + { + ip.X = edge2.Bot.X; + if (IsHorizontal(edge1)) + { + ip.Y = edge1.Bot.Y; + } + else + { + b1 = edge1.Bot.Y - (edge1.Bot.X / edge1.Dx); + ip.Y = Round((ip.X / edge1.Dx) + b1); + } + } + else + { + b1 = edge1.Bot.X - (edge1.Bot.Y * edge1.Dx); + b2 = edge2.Bot.X - (edge2.Bot.Y * edge2.Dx); + double q = (b2 - b1) / (edge1.Dx - edge2.Dx); + ip.Y = Round(q); + if (Math.Abs(edge1.Dx) < Math.Abs(edge2.Dx)) + { + ip.X = Round((edge1.Dx * q) + b1); + } + else + { + ip.X = Round((edge2.Dx * q) + b2); + } + } + + if (ip.Y < edge1.Top.Y || ip.Y < edge2.Top.Y) + { + if (edge1.Top.Y > edge2.Top.Y) + { + ip.Y = edge1.Top.Y; + } + else + { + ip.Y = edge2.Top.Y; + } + + if (Math.Abs(edge1.Dx) < Math.Abs(edge2.Dx)) + { + ip.X = TopX(edge1, ip.Y); + } + else + { + ip.X = TopX(edge2, ip.Y); + } + } + + // finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ... + if (ip.Y > edge1.Curr.Y) + { + ip.Y = edge1.Curr.Y; + + // better to use the more vertical edge to derive X ... + if (Math.Abs(edge1.Dx) > Math.Abs(edge2.Dx)) + { + ip.X = TopX(edge2, ip.Y); + } + else + { + ip.X = TopX(edge1, ip.Y); + } + } + } + + private void ProcessEdgesAtTopOfScanbeam(float topY) + { + TEdge e = this.activeEdges; + while (e != null) + { + // 1. process maxima, treating them as if they're 'bent' horizontal edges, + // but exclude maxima with horizontal edges. nb: e can't be a horizontal. + bool isMaximaEdge = this.IsMaxima(e, topY); + + if (isMaximaEdge) + { + TEdge eMaxPair = this.GetMaximaPairEx(e); + isMaximaEdge = eMaxPair == null || !IsHorizontal(eMaxPair); + } + + if (isMaximaEdge) + { + TEdge ePrev = e.PrevInAEL; + this.DoMaxima(e); + if (ePrev == null) + { + e = this.activeEdges; + } + else + { + e = ePrev.NextInAEL; + } + } + else + { + // 2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... + if (this.IsIntermediate(e, topY) && IsHorizontal(e.NextInLML)) + { + this.UpdateEdgeIntoAEL(ref e); + if (e.OutIdx >= 0) + { + this.AddOutPt(e, e.Bot); + } + + this.AddEdgeToSEL(e); + } + else + { + e.Curr.X = TopX(e, topY); + e.Curr.Y = topY; + } + + e = e.NextInAEL; + } + } + + // 3. Process horizontals at the Top of the scanbeam ... + this.ProcessHorizontals(); + this.maxima = null; + + // 4. Promote intermediate vertices ... + e = this.activeEdges; + while (e != null) + { + if (this.IsIntermediate(e, topY)) + { + OutPt op = null; + if (e.OutIdx >= 0) + { + op = this.AddOutPt(e, e.Top); + } + + this.UpdateEdgeIntoAEL(ref e); + + // if output polygons share an edge, they'll need joining later ... + TEdge ePrev = e.PrevInAEL; + TEdge eNext = e.NextInAEL; + if (ePrev != null && ePrev.Curr.X == e.Bot.X && + ePrev.Curr.Y == e.Bot.Y && op != null && + ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y && + SlopesEqual(e.Curr, e.Top, ePrev.Curr, ePrev.Top) && + (e.WindDelta != 0) && (ePrev.WindDelta != 0)) + { + OutPt op2 = this.AddOutPt(ePrev, e.Bot); + this.AddJoin(op, op2, e.Top); + } + else if (eNext != null && eNext.Curr.X == e.Bot.X && + eNext.Curr.Y == e.Bot.Y && op != null && + eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y && + SlopesEqual(e.Curr, e.Top, eNext.Curr, eNext.Top) && + (e.WindDelta != 0) && (eNext.WindDelta != 0)) + { + OutPt op2 = this.AddOutPt(eNext, e.Bot); + this.AddJoin(op, op2, e.Top); + } + } + + e = e.NextInAEL; + } + } + + private void DoMaxima(TEdge e) + { + TEdge eMaxPair = this.GetMaximaPairEx(e); + if (eMaxPair == null) + { + if (e.OutIdx >= 0) + { + this.AddOutPt(e, e.Top); + } + + this.DeleteFromAEL(e); + return; + } + + TEdge eNext = e.NextInAEL; + while (eNext != null && eNext != eMaxPair) + { + this.IntersectEdges(e, eNext, e.Top); + this.SwapPositionsInAEL(e, eNext); + eNext = e.NextInAEL; + } + + if (e.OutIdx == Unassigned && eMaxPair.OutIdx == Unassigned) + { + this.DeleteFromAEL(e); + this.DeleteFromAEL(eMaxPair); + } + else if (e.OutIdx >= 0 && eMaxPair.OutIdx >= 0) + { + if (e.OutIdx >= 0) + { + this.AddLocalMaxPoly(e, eMaxPair, e.Top); + } + + this.DeleteFromAEL(e); + this.DeleteFromAEL(eMaxPair); + } + else + { + throw new ClipperException("DoMaxima error"); + } + } + + private int PointCount(OutPt pts) + { + if (pts == null) + { + return 0; + } + + int result = 0; + OutPt p = pts; + do + { + result++; + p = p.Next; + } + while (p != pts); + return result; + } + + private void BuildResult2(PolyTree polytree) + { + polytree.Clear(); + + // add each output polygon/contour to polytree ... + polytree.AllPolys.Capacity = this.polyOuts.Count; + for (int i = 0; i < this.polyOuts.Count; i++) + { + OutRec outRec = this.polyOuts[i]; + int cnt = this.PointCount(outRec.Pts); + if ((outRec.IsOpen && cnt < 2) || + (!outRec.IsOpen && cnt < 3)) + { + continue; + } + + FixHoleLinkage(outRec); + PolyNode pn = new PolyNode(); + pn.SourcePath = outRec.SourcePath; + polytree.AllPolys.Add(pn); + outRec.PolyNode = pn; + pn.Polygon.Capacity = cnt; + OutPt op = outRec.Pts.Prev; + for (int j = 0; j < cnt; j++) + { + pn.Polygon.Add(op.Pt); + op = op.Prev; + } + } + + // 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) + { + outRec.PolyNode.IsOpen = true; + polytree.AddChild(outRec.PolyNode); + } + else if (outRec.FirstLeft != null && + outRec.FirstLeft.PolyNode != null) + { + outRec.FirstLeft.PolyNode.AddChild(outRec.PolyNode); + } + else + { + polytree.AddChild(outRec.PolyNode); + } + } + } + + private void FixupOutPolyline(OutRec outrec) + { + OutPt pp = outrec.Pts; + OutPt lastPP = pp.Prev; + while (pp != lastPP) + { + pp = pp.Next; + if (pp.Pt == pp.Prev.Pt) + { + if (pp == lastPP) + { + lastPP = pp.Prev; + } + + OutPt tmpPP = pp.Prev; + tmpPP.Next = pp.Next; + pp.Next.Prev = tmpPP; + pp = tmpPP; + } + } + + if (pp == pp.Prev) + { + outrec.Pts = null; + } + } + + private void FixupOutPolygon(OutRec outRec) + { + // FixupOutPolygon() - removes duplicate points and simplifies consecutive + // parallel edges by removing the middle vertex. + OutPt lastOK = null; + outRec.BottomPt = null; + OutPt pp = outRec.Pts; + while (true) + { + if (pp.Prev == pp || pp.Prev == pp.Next) + { + outRec.Pts = null; + return; + } + + // test for duplicate points and collinear edges ... + if ((pp.Pt == pp.Next.Pt) || (pp.Pt == pp.Prev.Pt) || + SlopesEqual(pp.Prev.Pt, pp.Pt, pp.Next.Pt)) + { + lastOK = null; + pp.Prev.Next = pp.Next; + pp.Next.Prev = pp.Prev; + pp = pp.Prev; + } + else if (pp == lastOK) + { + break; + } + else + { + if (lastOK == null) + { + lastOK = pp; + } + + pp = pp.Next; + } + } + + outRec.Pts = pp; + } + + private OutPt DupOutPt(OutPt outPt, bool insertAfter) + { + OutPt result = new OutPt(); + result.Pt = outPt.Pt; + result.Idx = outPt.Idx; + if (insertAfter) + { + result.Next = outPt.Next; + result.Prev = outPt; + outPt.Next.Prev = result; + outPt.Next = result; + } + else + { + result.Prev = outPt.Prev; + result.Next = outPt; + outPt.Prev.Next = result; + outPt.Prev = result; + } + + return result; + } + + private bool GetOverlap(float a1, float a2, float b1, float b2, out float left, out float right) + { + if (a1 < a2) + { + if (b1 < b2) + { + left = Math.Max(a1, b1); + right = Math.Min(a2, b2); + } + else + { + left = Math.Max(a1, b2); + right = Math.Min(a2, b1); + } + } + else + { + if (b1 < b2) + { + left = Math.Max(a2, b1); + right = Math.Min(a1, b2); + } + else + { + left = Math.Max(a2, b2); + right = Math.Min(a1, b1); + } + } + + return left < right; + } + + private bool JoinHorz(OutPt op1, OutPt op1b, OutPt op2, OutPt op2b, Vector2 pt, bool discardLeft) + { + Direction dir1 = op1.Pt.X > op1b.Pt.X ? Direction.RightToLeft : Direction.LeftToRight; + Direction dir2 = op2.Pt.X > op2b.Pt.X ? Direction.RightToLeft : Direction.LeftToRight; + if (dir1 == dir2) + { + return false; + } + + // When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we + // want Op1b to be on the Right. (And likewise with Op2 and Op2b.) + // So, to facilitate this while inserting Op1b and Op2b ... + // when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, + // otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) + if (dir1 == Direction.LeftToRight) + { + while (op1.Next.Pt.X <= pt.X && + op1.Next.Pt.X >= op1.Pt.X && op1.Next.Pt.Y == pt.Y) + { + op1 = op1.Next; + } + + if (discardLeft && (op1.Pt.X != pt.X)) + { + op1 = op1.Next; + } + + op1b = this.DupOutPt(op1, !discardLeft); + if (op1b.Pt != pt) + { + op1 = op1b; + op1.Pt = pt; + op1b = this.DupOutPt(op1, !discardLeft); + } + } + else + { + while (op1.Next.Pt.X >= pt.X && + op1.Next.Pt.X <= op1.Pt.X && + op1.Next.Pt.Y == pt.Y) + { + op1 = op1.Next; + } + + if (!discardLeft && (op1.Pt.X != pt.X)) + { + op1 = op1.Next; + } + + op1b = this.DupOutPt(op1, discardLeft); + if (op1b.Pt != pt) + { + op1 = op1b; + op1.Pt = pt; + op1b = this.DupOutPt(op1, discardLeft); + } + } + + if (dir2 == Direction.LeftToRight) + { + while (op2.Next.Pt.X <= pt.X && + op2.Next.Pt.X >= op2.Pt.X && + op2.Next.Pt.Y == pt.Y) + { + op2 = op2.Next; + } + + if (discardLeft && (op2.Pt.X != pt.X)) + { + op2 = op2.Next; + } + + op2b = this.DupOutPt(op2, !discardLeft); + if (op2b.Pt != pt) + { + op2 = op2b; + op2.Pt = pt; + op2b = this.DupOutPt(op2, !discardLeft); + } + } + else + { + while (op2.Next.Pt.X >= pt.X && + op2.Next.Pt.X <= op2.Pt.X && + op2.Next.Pt.Y == pt.Y) + { + op2 = op2.Next; + } + + if (!discardLeft && (op2.Pt.X != pt.X)) + { + op2 = op2.Next; + } + + op2b = this.DupOutPt(op2, discardLeft); + if (op2b.Pt != pt) + { + op2 = op2b; + op2.Pt = pt; + op2b = this.DupOutPt(op2, discardLeft); + } + } + + if ((dir1 == Direction.LeftToRight) == discardLeft) + { + op1.Prev = op2; + op2.Next = op1; + op1b.Next = op2b; + op2b.Prev = op1b; + } + else + { + op1.Next = op2; + op2.Prev = op1; + op1b.Prev = op2b; + op2b.Next = op1b; + } + + return true; + } + + private bool JoinPoints(Join j, OutRec outRec1, OutRec outRec2) + { + OutPt op1 = j.OutPt1, op1b; + OutPt op2 = j.OutPt2, op2b; + + // There are 3 kinds of joins for output polygons ... + // 1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere + // along (horizontal) collinear edges (& Join.OffPt is on the same horizontal). + // 2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same + // location at the Bottom of the overlapping segment (& Join.OffPt is above). + // 3. StrictlySimple joins where edges touch but are not collinear and where + // Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. + bool isHorizontal = j.OutPt1.Pt.Y == j.OffPt.Y; + + if (isHorizontal && (j.OffPt == j.OutPt1.Pt) && (j.OffPt == j.OutPt2.Pt)) + { + // Strictly Simple join ... + if (outRec1 != outRec2) + { + return false; + } + + op1b = j.OutPt1.Next; + while (op1b != op1 && (op1b.Pt == j.OffPt)) + { + op1b = op1b.Next; + } + + bool reverse1 = op1b.Pt.Y > j.OffPt.Y; + op2b = j.OutPt2.Next; + while (op2b != op2 && (op2b.Pt == j.OffPt)) + { + op2b = op2b.Next; + } + + bool reverse2 = op2b.Pt.Y > j.OffPt.Y; + if (reverse1 == reverse2) + { + return false; + } + + if (reverse1) + { + op1b = this.DupOutPt(op1, false); + op2b = this.DupOutPt(op2, true); + op1.Prev = op2; + op2.Next = op1; + op1b.Next = op2b; + op2b.Prev = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + else + { + op1b = this.DupOutPt(op1, true); + op2b = this.DupOutPt(op2, false); + op1.Next = op2; + op2.Prev = op1; + op1b.Prev = op2b; + op2b.Next = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + } + else if (isHorizontal) + { + // treat horizontal joins differently to non-horizontal joins since with + // them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt + // may be anywhere along the horizontal edge. + op1b = op1; + while (op1.Prev.Pt.Y == op1.Pt.Y && op1.Prev != op1b && op1.Prev != op2) + { + op1 = op1.Prev; + } + + while (op1b.Next.Pt.Y == op1b.Pt.Y && op1b.Next != op1 && op1b.Next != op2) + { + op1b = op1b.Next; + } + + if (op1b.Next == op1 || op1b.Next == op2) + { + return false; // a flat 'polygon' + } + + op2b = op2; + while (op2.Prev.Pt.Y == op2.Pt.Y && op2.Prev != op2b && op2.Prev != op1b) + { + op2 = op2.Prev; + } + + while (op2b.Next.Pt.Y == op2b.Pt.Y && op2b.Next != op2 && op2b.Next != op1) + { + op2b = op2b.Next; + } + + if (op2b.Next == op2 || op2b.Next == op1) + { + return false; // a flat 'polygon' + } + + float left, right; + + // Op1 -. Op1b & Op2 -. Op2b are the extremites of the horizontal edges + if (!this.GetOverlap(op1.Pt.X, op1b.Pt.X, op2.Pt.X, op2b.Pt.X, out left, out right)) + { + return false; + } + + // DiscardLeftSide: when overlapping edges are joined, a spike will created + // which needs to be cleaned up. However, we don't want Op1 or Op2 caught up + // on the discard Side as either may still be needed for other joins ... + Vector2 pt; + bool discardLeftSide; + if (op1.Pt.X >= left && op1.Pt.X <= right) + { + pt = op1.Pt; + discardLeftSide = op1.Pt.X > op1b.Pt.X; + } + else if (op2.Pt.X >= left && op2.Pt.X <= right) + { + pt = op2.Pt; + discardLeftSide = op2.Pt.X > op2b.Pt.X; + } + else if (op1b.Pt.X >= left && op1b.Pt.X <= right) + { + pt = op1b.Pt; + discardLeftSide = op1b.Pt.X > op1.Pt.X; + } + else + { + pt = op2b.Pt; + discardLeftSide = op2b.Pt.X > op2.Pt.X; + } + + j.OutPt1 = op1; + j.OutPt2 = op2; + return this.JoinHorz(op1, op1b, op2, op2b, pt, discardLeftSide); + } + else + { + // nb: For non-horizontal joins ... + // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y + // 2. Jr.OutPt1.Pt > Jr.OffPt.Y + + // make sure the polygons are correctly oriented ... + op1b = op1.Next; + while ((op1b.Pt == op1.Pt) && (op1b != op1)) + { + op1b = op1b.Next; + } + + bool reverse1 = (op1b.Pt.Y > op1.Pt.Y) || !SlopesEqual(op1.Pt, op1b.Pt, j.OffPt); + if (reverse1) + { + op1b = op1.Prev; + while ((op1b.Pt == op1.Pt) && (op1b != op1)) + { + op1b = op1b.Prev; + } + + if ((op1b.Pt.Y > op1.Pt.Y) || + !SlopesEqual(op1.Pt, op1b.Pt, j.OffPt)) + { + return false; + } + } + + op2b = op2.Next; + while ((op2b.Pt == op2.Pt) && (op2b != op2)) + { + op2b = op2b.Next; + } + + bool reverse2 = (op2b.Pt.Y > op2.Pt.Y) || !SlopesEqual(op2.Pt, op2b.Pt, j.OffPt); + if (reverse2) + { + op2b = op2.Prev; + while ((op2b.Pt == op2.Pt) && (op2b != op2)) + { + op2b = op2b.Prev; + } + + if ((op2b.Pt.Y > op2.Pt.Y) || + !SlopesEqual(op2.Pt, op2b.Pt, j.OffPt)) + { + return false; + } + } + + if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || + ((outRec1 == outRec2) && (reverse1 == reverse2))) + { + return false; + } + + if (reverse1) + { + op1b = this.DupOutPt(op1, false); + op2b = this.DupOutPt(op2, true); + op1.Prev = op2; + op2.Next = op1; + op1b.Next = op2b; + op2b.Prev = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + else + { + op1b = this.DupOutPt(op1, true); + op2b = this.DupOutPt(op2, false); + op1.Next = op2; + op2.Prev = op1; + op1b.Prev = op2b; + op2b.Next = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + } + } + + private void FixupFirstLefts1(OutRec oldOutRec, OutRec newOutRec) + { + foreach (OutRec outRec in this.polyOuts) + { + OutRec firstLeft = ParseFirstLeft(outRec.FirstLeft); + if (outRec.Pts != null && firstLeft == oldOutRec) + { + if (Poly2ContainsPoly1(outRec.Pts, newOutRec.Pts)) + { + outRec.FirstLeft = newOutRec; + } + } + } + } + + private void FixupFirstLefts2(OutRec innerOutRec, OutRec outerOutRec) + { + // A polygon has split into two such that one is now the inner of the other. + // It's possible that these polygons now wrap around other polygons, so check + // every polygon that's also contained by OuterOutRec's FirstLeft container + // (including nil) to see if they've become inner to the new inner polygon ... + OutRec orfl = outerOutRec.FirstLeft; + foreach (OutRec outRec in this.polyOuts) + { + if (outRec.Pts == null || outRec == outerOutRec || outRec == innerOutRec) + { + continue; + } + + OutRec firstLeft = ParseFirstLeft(outRec.FirstLeft); + if (firstLeft != orfl && firstLeft != innerOutRec && firstLeft != outerOutRec) + { + continue; + } + + if (Poly2ContainsPoly1(outRec.Pts, innerOutRec.Pts)) + { + outRec.FirstLeft = innerOutRec; + } + else if (Poly2ContainsPoly1(outRec.Pts, outerOutRec.Pts)) + { + outRec.FirstLeft = outerOutRec; + } + else if (outRec.FirstLeft == innerOutRec || outRec.FirstLeft == outerOutRec) + { + outRec.FirstLeft = orfl; + } + } + } + + private void FixupFirstLefts3(OutRec oldOutRec, OutRec newOutRec) + { + // same as FixupFirstLefts1 but doesn't call Poly2ContainsPoly1() + foreach (OutRec outRec in this.polyOuts) + { + OutRec firstLeft = ParseFirstLeft(outRec.FirstLeft); + if (outRec.Pts != null && outRec.FirstLeft == oldOutRec) + { + outRec.FirstLeft = newOutRec; + } + } + } + + private void JoinCommonEdges() + { + for (int i = 0; i < this.joins.Count; i++) + { + Join join = this.joins[i]; + + OutRec outRec1 = this.GetOutRec(join.OutPt1.Idx); + OutRec outRec2 = this.GetOutRec(join.OutPt2.Idx); + + if (outRec1.Pts == null || outRec2.Pts == null) + { + continue; + } + + if (outRec1.IsOpen || outRec2.IsOpen) + { + continue; + } + + // get the polygon fragment with the correct hole state (FirstLeft) + // before calling JoinPoints() ... + OutRec holeStateRec; + if (outRec1 == outRec2) + { + holeStateRec = outRec1; + } + else if (this.OutRec1RightOfOutRec2(outRec1, outRec2)) + { + holeStateRec = outRec2; + } + else if (this.OutRec1RightOfOutRec2(outRec2, outRec1)) + { + holeStateRec = outRec1; + } + else + { + holeStateRec = this.GetLowermostRec(outRec1, outRec2); + } + + if (!this.JoinPoints(join, outRec1, outRec2)) + { + continue; + } + + if (outRec1 == outRec2) + { + // instead of joining two polygons, we've just created a new one by + // splitting one polygon into two. + outRec1.Pts = join.OutPt1; + outRec1.BottomPt = null; + outRec2 = this.CreateOutRec(); + outRec2.Pts = join.OutPt2; + + // update all OutRec2.Pts Idx's ... + this.UpdateOutPtIdxs(outRec2); + + if (Poly2ContainsPoly1(outRec2.Pts, outRec1.Pts)) + { + // outRec1 contains outRec2 ... + outRec2.IsHole = !outRec1.IsHole; + outRec2.FirstLeft = outRec1; + + if (this.usingPolyTree) + { + this.FixupFirstLefts2(outRec2, outRec1); + } + } + else if (Poly2ContainsPoly1(outRec1.Pts, outRec2.Pts)) + { + // outRec2 contains outRec1 ... + outRec2.IsHole = outRec1.IsHole; + outRec1.IsHole = !outRec2.IsHole; + outRec2.FirstLeft = outRec1.FirstLeft; + outRec1.FirstLeft = outRec2; + + if (this.usingPolyTree) + { + this.FixupFirstLefts2(outRec1, outRec2); + } + } + else + { + // the 2 polygons are completely separate ... + outRec2.IsHole = outRec1.IsHole; + outRec2.FirstLeft = outRec1.FirstLeft; + + // fixup FirstLeft pointers that may need reassigning to OutRec2 + if (this.usingPolyTree) + { + this.FixupFirstLefts1(outRec1, outRec2); + } + } + } + else + { + // joined 2 polygons together ... + outRec2.Pts = null; + outRec2.BottomPt = null; + outRec2.Idx = outRec1.Idx; + + outRec1.IsHole = holeStateRec.IsHole; + if (holeStateRec == outRec2) + { + outRec1.FirstLeft = outRec2.FirstLeft; + } + + outRec2.FirstLeft = outRec1; + + // fixup FirstLeft pointers that may need reassigning to OutRec1 + if (this.usingPolyTree) + { + this.FixupFirstLefts3(outRec2, outRec1); + } + } + } + } + + private void UpdateOutPtIdxs(OutRec outrec) + { + OutPt op = outrec.Pts; + do + { + op.Idx = outrec.Idx; + op = op.Prev; + } + while (op != outrec.Pts); + } + + private void DoSimplePolygons() + { + int i = 0; + while (i < this.polyOuts.Count) + { + OutRec outrec = this.polyOuts[i++]; + OutPt op = outrec.Pts; + if (op == null || outrec.IsOpen) + { + continue; + } + + do + { + // for each Pt in Polygon until duplicate found do ... + OutPt op2 = op.Next; + while (op2 != outrec.Pts) + { + if ((op.Pt == op2.Pt) && op2.Next != op && op2.Prev != op) + { + // split the polygon into two ... + OutPt op3 = op.Prev; + OutPt op4 = op2.Prev; + op.Prev = op4; + op4.Next = op; + op2.Prev = op3; + op3.Next = op2; + + outrec.Pts = op; + OutRec outrec2 = this.CreateOutRec(); + outrec2.Pts = op2; + this.UpdateOutPtIdxs(outrec2); + if (Poly2ContainsPoly1(outrec2.Pts, outrec.Pts)) + { + // OutRec2 is contained by OutRec1 ... + outrec2.IsHole = !outrec.IsHole; + outrec2.FirstLeft = outrec; + if (this.usingPolyTree) + { + this.FixupFirstLefts2(outrec2, outrec); + } + } + else + if (Poly2ContainsPoly1(outrec.Pts, outrec2.Pts)) + { + // OutRec1 is contained by OutRec2 ... + outrec2.IsHole = outrec.IsHole; + outrec.IsHole = !outrec2.IsHole; + outrec2.FirstLeft = outrec.FirstLeft; + outrec.FirstLeft = outrec2; + if (this.usingPolyTree) + { + this.FixupFirstLefts2(outrec, outrec2); + } + } + else + { + // the 2 polygons are separate ... + outrec2.IsHole = outrec.IsHole; + outrec2.FirstLeft = outrec.FirstLeft; + if (this.usingPolyTree) + { + this.FixupFirstLefts1(outrec, outrec2); + } + } + + op2 = op; // ie get ready for the next iteration + } + + op2 = op2.Next; + } + + op = op.Next; + } + while (op != outrec.Pts); + } + } + + private double Area(OutRec outRec) + { + return this.Area(outRec.Pts); + } + + private double Area(OutPt op) + { + OutPt opFirst = op; + if (op == null) + { + return 0; + } + + double a = 0; + do + { + a = a + ((op.Prev.Pt.X + op.Pt.X) * (op.Prev.Pt.Y - op.Pt.Y)); + op = op.Next; + } + while (op != opFirst); + + return a * 0.5; + } + + private void SetDx(TEdge e) + { + e.Delta.X = e.Top.X - e.Bot.X; + e.Delta.Y = e.Top.Y - e.Bot.Y; + if (e.Delta.Y == 0) + { + e.Dx = HorizontalDeltaLimit; + } + else + { + e.Dx = e.Delta.X / e.Delta.Y; + } + } + + private void InsertLocalMinima(LocalMinima newLm) + { + if (this.minimaList == null) + { + this.minimaList = newLm; + } + else if (newLm.Y >= this.minimaList.Y) + { + newLm.Next = this.minimaList; + this.minimaList = newLm; + } + else + { + LocalMinima tmpLm = this.minimaList; + while (tmpLm.Next != null && (newLm.Y < tmpLm.Next.Y)) + { + tmpLm = tmpLm.Next; + } + + newLm.Next = tmpLm.Next; + tmpLm.Next = newLm; + } + } + + private bool PopLocalMinima(float y, out LocalMinima current) + { + current = this.currentLM; + if (this.currentLM != null && this.currentLM.Y == y) + { + this.currentLM = this.currentLM.Next; + return true; + } + + return false; + } + + private void Reset() + { + this.currentLM = this.minimaList; + if (this.currentLM == null) + { + return; // ie nothing to process + } + + // reset all edges ... + this.scanbeam = null; + LocalMinima lm = this.minimaList; + while (lm != null) + { + this.InsertScanbeam(lm.Y); + TEdge e = lm.LeftBound; + if (e != null) + { + e.Curr = e.Bot; + e.OutIdx = Unassigned; + } + + e = lm.RightBound; + if (e != null) + { + e.Curr = e.Bot; + e.OutIdx = Unassigned; + } + + lm = lm.Next; + } + + this.activeEdges = null; + } + + private void InsertScanbeam(float y) + { + // single-linked list: sorted descending, ignoring dups. + if (this.scanbeam == null) + { + this.scanbeam = new Scanbeam(); + this.scanbeam.Next = null; + this.scanbeam.Y = y; + } + else if (y > this.scanbeam.Y) + { + Scanbeam newSb = new Scanbeam(); + newSb.Y = y; + newSb.Next = this.scanbeam; + this.scanbeam = newSb; + } + else + { + Scanbeam sb2 = this.scanbeam; + while (sb2.Next != null && (y <= sb2.Next.Y)) + { + sb2 = sb2.Next; + } + + if (y == sb2.Y) + { + return; // ie ignores duplicates + } + + Scanbeam newSb = new Scanbeam(); + newSb.Y = y; + newSb.Next = sb2.Next; + sb2.Next = newSb; + } + } + + private bool PopScanbeam(out float y) + { + if (this.scanbeam == null) + { + y = 0; + return false; + } + + y = this.scanbeam.Y; + this.scanbeam = this.scanbeam.Next; + return true; + } + + private bool LocalMinimaPending() + { + return this.currentLM != null; + } + + private OutRec CreateOutRec() + { + OutRec result = new OutRec(); + result.Idx = Unassigned; + result.IsHole = false; + result.IsOpen = false; + result.FirstLeft = null; + result.Pts = null; + result.BottomPt = null; + result.PolyNode = null; + this.polyOuts.Add(result); + result.Idx = this.polyOuts.Count - 1; + return result; + } + + private void DisposeOutRec(int index) + { + OutRec outRec = this.polyOuts[index]; + outRec.Pts = null; + outRec = null; + this.polyOuts[index] = null; + } + + private void UpdateEdgeIntoAEL(ref TEdge e) + { + if (e.NextInLML == null) + { + throw new ClipperException("UpdateEdgeIntoAEL: invalid call"); + } + + TEdge aelPrev = e.PrevInAEL; + TEdge aelNext = e.NextInAEL; + e.NextInLML.OutIdx = e.OutIdx; + if (aelPrev != null) + { + aelPrev.NextInAEL = e.NextInLML; + } + else + { + this.activeEdges = e.NextInLML; + } + + if (aelNext != null) + { + aelNext.PrevInAEL = e.NextInLML; + } + + e.NextInLML.Side = e.Side; + e.NextInLML.WindDelta = e.WindDelta; + e.NextInLML.WindCnt = e.WindCnt; + e.NextInLML.WindCnt2 = e.WindCnt2; + e = e.NextInLML; + e.Curr = e.Bot; + e.PrevInAEL = aelPrev; + e.NextInAEL = aelNext; + if (!IsHorizontal(e)) + { + this.InsertScanbeam(e.Top.Y); + } + } + + private void SwapPositionsInAEL(TEdge edge1, TEdge edge2) + { + // check that one or other edge hasn't already been removed from AEL ... + if (edge1.NextInAEL == edge1.PrevInAEL || + edge2.NextInAEL == edge2.PrevInAEL) + { + return; + } + + if (edge1.NextInAEL == edge2) + { + TEdge next = edge2.NextInAEL; + if (next != null) + { + next.PrevInAEL = edge1; + } + + TEdge prev = edge1.PrevInAEL; + if (prev != null) + { + prev.NextInAEL = edge2; + } + + edge2.PrevInAEL = prev; + edge2.NextInAEL = edge1; + edge1.PrevInAEL = edge2; + edge1.NextInAEL = next; + } + else if (edge2.NextInAEL == edge1) + { + TEdge next = edge1.NextInAEL; + if (next != null) + { + next.PrevInAEL = edge2; + } + + TEdge prev = edge2.PrevInAEL; + if (prev != null) + { + prev.NextInAEL = edge1; + } + + edge1.PrevInAEL = prev; + edge1.NextInAEL = edge2; + edge2.PrevInAEL = edge1; + edge2.NextInAEL = next; + } + else + { + TEdge next = edge1.NextInAEL; + TEdge prev = edge1.PrevInAEL; + edge1.NextInAEL = edge2.NextInAEL; + if (edge1.NextInAEL != null) + { + edge1.NextInAEL.PrevInAEL = edge1; + } + + edge1.PrevInAEL = edge2.PrevInAEL; + if (edge1.PrevInAEL != null) + { + edge1.PrevInAEL.NextInAEL = edge1; + } + + edge2.NextInAEL = next; + if (edge2.NextInAEL != null) + { + edge2.NextInAEL.PrevInAEL = edge2; + } + + edge2.PrevInAEL = prev; + if (edge2.PrevInAEL != null) + { + edge2.PrevInAEL.NextInAEL = edge2; + } + } + + if (edge1.PrevInAEL == null) + { + this.activeEdges = edge1; + } + else if (edge2.PrevInAEL == null) + { + this.activeEdges = edge2; + } + } + + private void DeleteFromAEL(TEdge e) + { + TEdge aelPrev = e.PrevInAEL; + TEdge aelNext = e.NextInAEL; + if (aelPrev == null && aelNext == null && (e != this.activeEdges)) + { + return; // already deleted + } + + if (aelPrev != null) + { + aelPrev.NextInAEL = aelNext; + } + else + { + this.activeEdges = aelNext; + } + + if (aelNext != null) + { + aelNext.PrevInAEL = aelPrev; + } + + e.NextInAEL = null; + e.PrevInAEL = null; + } + + private void InitEdge2(TEdge e, PolyType polyType) + { + if (e.Curr.Y >= e.Next.Curr.Y) + { + e.Bot = e.Curr; + e.Top = e.Next.Curr; + } + else + { + e.Top = e.Curr; + e.Bot = e.Next.Curr; + } + + this.SetDx(e); + e.PolyTyp = polyType; + } + + private TEdge ProcessBound(TEdge edge, bool leftBoundIsForward) + { + TEdge eStart, result = edge; + TEdge horz; + + if (result.OutIdx == Skip) + { + // check if there are edges beyond the skip edge in the bound and if so + // create another LocMin and calling ProcessBound once more ... + edge = result; + if (leftBoundIsForward) + { + while (edge.Top.Y == edge.Next.Bot.Y) + { + edge = edge.Next; + } + + while (edge != result && edge.Dx == HorizontalDeltaLimit) + { + edge = edge.Prev; + } + } + else + { + while (edge.Top.Y == edge.Prev.Bot.Y) + { + edge = edge.Prev; + } + + while (edge != result && edge.Dx == HorizontalDeltaLimit) + { + edge = edge.Next; + } + } + + if (edge == result) + { + if (leftBoundIsForward) + { + result = edge.Next; + } + else + { + result = edge.Prev; + } + } + else + { + // there are more edges in the bound beyond result starting with E + if (leftBoundIsForward) + { + edge = result.Next; + } + else + { + edge = result.Prev; + } + + LocalMinima locMin = new LocalMinima(); + locMin.Next = null; + locMin.Y = edge.Bot.Y; + locMin.LeftBound = null; + locMin.RightBound = edge; + edge.WindDelta = 0; + result = this.ProcessBound(edge, leftBoundIsForward); + this.InsertLocalMinima(locMin); + } + + return result; + } + + if (edge.Dx == HorizontalDeltaLimit) + { + // We need to be careful with open paths because this may not be a + // true local minima (ie E may be following a skip edge). + // Also, consecutive horz. edges may start heading left before going right. + if (leftBoundIsForward) + { + eStart = edge.Prev; + } + else + { + eStart = edge.Next; + } + + // ie an adjoining horizontal skip edge + if (eStart.Dx == HorizontalDeltaLimit) + { + if (eStart.Bot.X != edge.Bot.X && eStart.Top.X != edge.Bot.X) + { + ReverseHorizontal(edge); + } + } + else if (eStart.Bot.X != edge.Bot.X) + { + ReverseHorizontal(edge); + } + } + + eStart = edge; + if (leftBoundIsForward) + { + while (result.Top.Y == result.Next.Bot.Y && result.Next.OutIdx != Skip) + { + result = result.Next; + } + + if (result.Dx == HorizontalDeltaLimit && result.Next.OutIdx != Skip) + { + // nb: at the top of a bound, horizontals are added to the bound + // only when the preceding edge attaches to the horizontal's left vertex + // unless a Skip edge is encountered when that becomes the top divide + horz = result; + while (horz.Prev.Dx == HorizontalDeltaLimit) + { + horz = horz.Prev; + } + + if (horz.Prev.Top.X > result.Next.Top.X) + { + result = horz.Prev; + } + } + + while (edge != result) + { + edge.NextInLML = edge.Next; + if (edge.Dx == HorizontalDeltaLimit && edge != eStart && edge.Bot.X != edge.Prev.Top.X) + { + ReverseHorizontal(edge); + } + + edge = edge.Next; + } + + if (edge.Dx == HorizontalDeltaLimit && edge != eStart && edge.Bot.X != edge.Prev.Top.X) + { + ReverseHorizontal(edge); + } + + result = result.Next; // move to the edge just beyond current bound + } + else + { + while (result.Top.Y == result.Prev.Bot.Y && result.Prev.OutIdx != Skip) + { + result = result.Prev; + } + + if (result.Dx == HorizontalDeltaLimit && result.Prev.OutIdx != Skip) + { + horz = result; + while (horz.Next.Dx == HorizontalDeltaLimit) + { + horz = horz.Next; + } + + if (horz.Next.Top.X == result.Prev.Top.X || horz.Next.Top.X > result.Prev.Top.X) + { + result = horz.Next; + } + } + + while (edge != result) + { + edge.NextInLML = edge.Prev; + if (edge.Dx == HorizontalDeltaLimit && edge != eStart && edge.Bot.X != edge.Next.Top.X) + { + ReverseHorizontal(edge); + } + + edge = edge.Prev; + } + + if (edge.Dx == HorizontalDeltaLimit && edge != eStart && edge.Bot.X != edge.Next.Top.X) + { + ReverseHorizontal(edge); + } + + result = result.Prev; // move to the edge just beyond current bound + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/ClipperException.cs b/src/Shaper2D/PolygonClipper/ClipperException.cs new file mode 100644 index 0000000..e5c67ed --- /dev/null +++ b/src/Shaper2D/PolygonClipper/ClipperException.cs @@ -0,0 +1,29 @@ +// +// 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; + + /// + /// Clipper Exception + /// + /// + internal class ClipperException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The description. + public ClipperException(string description) + : base(description) + { + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/Direction.cs b/src/Shaper2D/PolygonClipper/Direction.cs new file mode 100644 index 0000000..08bef5c --- /dev/null +++ b/src/Shaper2D/PolygonClipper/Direction.cs @@ -0,0 +1,29 @@ +// +// 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; + + /// + /// ??? + /// + internal enum Direction + { + /// + /// The right to left + /// + RightToLeft, + + /// + /// The left to right + /// + LeftToRight + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/EdgeSide.cs b/src/Shaper2D/PolygonClipper/EdgeSide.cs new file mode 100644 index 0000000..36152ab --- /dev/null +++ b/src/Shaper2D/PolygonClipper/EdgeSide.cs @@ -0,0 +1,29 @@ +// +// 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; + + /// + /// ?? + /// + internal enum EdgeSide + { + /// + /// The left + /// + Left, + + /// + /// The right + /// + Right + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/IntersectNode.cs b/src/Shaper2D/PolygonClipper/IntersectNode.cs new file mode 100644 index 0000000..2040338 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/IntersectNode.cs @@ -0,0 +1,36 @@ +// +// 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; + + /// + /// ?? + /// + internal class IntersectNode + { +#pragma warning disable SA1401 // Field must be private + /// + /// The edge1 + /// + internal TEdge Edge1; + + /// + /// The edge2 + /// + internal TEdge Edge2; + + /// + /// The pt + /// + internal System.Numerics.Vector2 Pt; +#pragma warning restore SA1401 // Field must be private + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/IntersectNodeSort.cs b/src/Shaper2D/PolygonClipper/IntersectNodeSort.cs new file mode 100644 index 0000000..5912778 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/IntersectNodeSort.cs @@ -0,0 +1,46 @@ +// +// 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; + + /// + /// Compares s + /// + internal class IntersectNodeSort : IComparer + { + /// + /// Compares the specified node1. + /// + /// The node1. + /// The node2. + /// + /// 1 if node2 %gt; node1 + /// -1 if node2 $lt; node1 + /// 0 if same + /// + public int Compare(IntersectNode node1, IntersectNode node2) + { + float i = node2.Pt.Y - node1.Pt.Y; + if (i > 0) + { + return 1; + } + else if (i < 0) + { + return -1; + } + else + { + return 0; + } + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/Join.cs b/src/Shaper2D/PolygonClipper/Join.cs new file mode 100644 index 0000000..6d8f1d8 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/Join.cs @@ -0,0 +1,36 @@ +// +// 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; + + /// + /// ?? + /// + internal class Join + { +#pragma warning disable SA1401 // Field must be private + /// + /// The out PT1 + /// + internal OutPt OutPt1; + + /// + /// The out PT2 + /// + internal OutPt OutPt2; + + /// + /// The off pt + /// + internal System.Numerics.Vector2 OffPt; +#pragma warning restore SA1401 // Field must be private + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/LocalMinima.cs b/src/Shaper2D/PolygonClipper/LocalMinima.cs new file mode 100644 index 0000000..0f8c53d --- /dev/null +++ b/src/Shaper2D/PolygonClipper/LocalMinima.cs @@ -0,0 +1,42 @@ +// +// 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; + + /// + /// ?? + /// + internal class LocalMinima + { +#pragma warning disable SA1401 // Field must be private + /// + /// The y + /// + internal float Y; + + /// + /// The left bound + /// + internal TEdge LeftBound; + + /// + /// The right bound + /// + internal TEdge RightBound; + + /// + /// The next + /// + internal LocalMinima Next; + +#pragma warning restore SA1401 // Field must be private + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/Maxima.cs b/src/Shaper2D/PolygonClipper/Maxima.cs new file mode 100644 index 0000000..06fae8b --- /dev/null +++ b/src/Shaper2D/PolygonClipper/Maxima.cs @@ -0,0 +1,36 @@ +// +// 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; + + /// + /// ?? + /// + internal class Maxima + { +#pragma warning disable SA1401 // Field must be private + /// + /// The x + /// + internal float X; + + /// + /// The next + /// + internal Maxima Next; + + /// + /// The previous + /// + internal Maxima Prev; +#pragma warning restore SA1401 // Field must be private + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/OutPt.cs b/src/Shaper2D/PolygonClipper/OutPt.cs new file mode 100644 index 0000000..dede3cd --- /dev/null +++ b/src/Shaper2D/PolygonClipper/OutPt.cs @@ -0,0 +1,41 @@ +// +// 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; + + /// + /// ?? + /// + internal class OutPt + { +#pragma warning disable SA1401 // Field must be private + /// + /// The index + /// + internal int Idx; + + /// + /// The pt + /// + internal System.Numerics.Vector2 Pt; + + /// + /// The next + /// + internal OutPt Next; + + /// + /// The previous + /// + internal OutPt Prev; +#pragma warning restore SA1401 // Field must be private + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/OutRec.cs b/src/Shaper2D/PolygonClipper/OutRec.cs new file mode 100644 index 0000000..f04332e --- /dev/null +++ b/src/Shaper2D/PolygonClipper/OutRec.cs @@ -0,0 +1,62 @@ +// +// 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; + + /// + /// OutRec: contains a path in the clipping solution. Edges in the AEL will + /// carry a pointer to an OutRec when they are part of the clipping solution. + /// + internal class OutRec + { +#pragma warning disable SA1401 // Field must be private + /// + /// The source path + /// + internal IPath SourcePath; + + /// + /// The index + /// + internal int Idx; + + /// + /// The is hole + /// + internal bool IsHole; + + /// + /// The is open + /// + internal bool IsOpen; + + /// + /// The first left + /// + internal OutRec FirstLeft; + + /// + /// The PTS + /// + internal OutPt Pts; + + /// + /// The bottom pt + /// + internal OutPt BottomPt; + + /// + /// The poly node + /// + internal PolyNode PolyNode; +#pragma warning restore SA1401 // Field must be private + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/PolyNode.cs b/src/Shaper2D/PolygonClipper/PolyNode.cs new file mode 100644 index 0000000..5c0b7f0 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/PolyNode.cs @@ -0,0 +1,177 @@ +// +// 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 + { +#pragma warning disable SA1401 // Field must be private + /// + /// The polygon + /// + internal List Polygon = new List(); + + /// + /// The index + /// + internal int Index; + + /// + /// The childs + /// + protected List children = new List(); + + private PolyNode parent; +#pragma warning restore SA1401 // Field must be private + + /// + /// Gets the child count. + /// + /// + /// The child count. + /// + public int ChildCount + { + get { return this.children.Count; } + } + + /// + /// Gets the contour. + /// + /// + /// The contour. + /// + public List Contour + { + get { return this.Polygon; } + } + + /// + /// Gets the childs. + /// + /// + /// The childs. + /// + public List Children + { + get { return this.children; } + } + + /// + /// Gets or sets the parent. + /// + /// + /// The parent. + /// + public PolyNode Parent + { + get { return this.parent; } + internal set { this.parent = value; } + } + + /// + /// Gets a value indicating whether this instance is hole. + /// + /// + /// true if this instance is hole; otherwise, false. + /// + public bool IsHole + { + get { return this.IsHoleNode(); } + } + + /// + /// Gets or sets a value indicating whether this instance is open. + /// + /// + /// true if this instance is open; otherwise, false. + /// + public bool IsOpen { get; set; } + + /// + /// Gets or sets the source path. + /// + /// + /// The source path. + /// + public IPath SourcePath { get; internal set; } + + /// + /// Gets the next. + /// + /// The next node + public PolyNode GetNext() + { + if (this.children.Count > 0) + { + return this.children[0]; + } + else + { + return this.GetNextSiblingUp(); + } + } + + /// + /// Adds the child. + /// + /// The child. + internal void AddChild(PolyNode child) + { + int cnt = this.children.Count; + this.children.Add(child); + child.parent = this; + child.Index = cnt; + } + + /// + /// Gets the next sibling up. + /// + /// The next sibling up + internal PolyNode GetNextSiblingUp() + { + if (this.parent == null) + { + return null; + } + else if (this.Index == this.parent.children.Count - 1) + { + return this.parent.GetNextSiblingUp(); + } + else + { + return this.parent.Children[this.Index + 1]; + } + } + + /// + /// Determines whether [is hole node]. + /// + /// + /// true if [is hole node]; otherwise, false. + /// + private bool IsHoleNode() + { + bool result = true; + PolyNode node = this.parent; + while (node != null) + { + result = !result; + node = node.parent; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/PolyTree.cs b/src/Shaper2D/PolygonClipper/PolyTree.cs new file mode 100644 index 0000000..d4f1aeb --- /dev/null +++ b/src/Shaper2D/PolygonClipper/PolyTree.cs @@ -0,0 +1,79 @@ +// +// 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 + { +#pragma warning disable SA1401 // Field must be private + /// + /// All polys + /// + internal List AllPolys = new List(); +#pragma warning restore SA1401 // Field must be private + + /// + /// Gets the total. + /// + /// + /// The total. + /// + public int Total + { + get + { + int result = this.AllPolys.Count; + + // with negative offsets, ignore the hidden outer polygon ... + if (result > 0 && this.Children[0] != this.AllPolys[0]) + { + result--; + } + + return result; + } + } + + /// + /// Clears this instance. + /// + public void Clear() + { + for (int i = 0; i < this.AllPolys.Count; i++) + { + this.AllPolys[i] = null; + } + + this.AllPolys.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/PolygonClipper/PolyType.cs b/src/Shaper2D/PolygonClipper/PolyType.cs new file mode 100644 index 0000000..5019571 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/PolyType.cs @@ -0,0 +1,29 @@ +// +// 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 Type + /// + internal enum PolyType + { + /// + /// The subject + /// + Subject, + + /// + /// The clip + /// + Clip + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/README.md b/src/Shaper2D/PolygonClipper/README.md new file mode 100644 index 0000000..c0f2ff6 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/README.md @@ -0,0 +1,40 @@ +# Clipper + +License details for code in this folder, this is code original written by **Angus Johnson** + +The license header onthe original file which has now be split across multiple files in this folder. + +``` +/******************************************************************************* +* * +* Author : Angus Johnson * +* Version : 6.4.0 * +* Date : 2 July 2015 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2015 * +* * +* License: * +* Use, modification & distribution is subject to Boost Software License Ver 1. * +* http://www.boost.org/LICENSE_1_0.txt * +* * +* Attributions: * +* The code in this library is an extension of Bala Vatti's clipping algorithm: * +* "A generic solution to polygon clipping" * +* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * +* http://portal.acm.org/citation.cfm?id=129906 * +* * +* Computer graphics and geometric modeling: implementation and algorithms * +* By Max K. Agoston * +* Springer; 1 edition (January 4, 2005) * +* http://books.google.com/books?q=vatti+clipping+agoston * +* * +* See also: * +* "Polygon Offsetting by Computing Winding Numbers" * +* Paper no. DETC2005-85513 pp. 565-575 * +* ASME 2005 International Design Engineering Technical Conferences * +* and Computers and Information in Engineering Conference (IDETC/CIE2005) * +* September 24-28, 2005 , Long Beach, California, USA * +* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * +* * +*******************************************************************************/ +``` \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/Scanbeam.cs b/src/Shaper2D/PolygonClipper/Scanbeam.cs new file mode 100644 index 0000000..f73af68 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/Scanbeam.cs @@ -0,0 +1,31 @@ +// +// 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; + + /// + /// Scanbeam + /// + internal class Scanbeam // would this work as a struct? + { +#pragma warning disable SA1401 // Field must be private + /// + /// The y + /// + internal float Y; + + /// + /// The next + /// + internal Scanbeam Next; +#pragma warning restore SA1401 // Field must be private + } +} \ No newline at end of file diff --git a/src/Shaper2D/PolygonClipper/TEdge.cs b/src/Shaper2D/PolygonClipper/TEdge.cs new file mode 100644 index 0000000..74f7200 --- /dev/null +++ b/src/Shaper2D/PolygonClipper/TEdge.cs @@ -0,0 +1,116 @@ +// +// 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; + + /// + /// TEdge + /// + internal class TEdge + { +#pragma warning disable SA1401 // Field must be private + /// + /// The source path, see if we can link this back later + /// + internal IPath SourcePath; + + /// + /// The bot + /// + internal System.Numerics.Vector2 Bot; + + /// + /// The current (updated for every new scanbeam) + /// + internal System.Numerics.Vector2 Curr; + + /// + /// The top + /// + internal System.Numerics.Vector2 Top; + + /// + /// The delta + /// + internal System.Numerics.Vector2 Delta; + + /// + /// The dx + /// + internal double Dx; + + /// + /// The poly type + /// + internal PolyType PolyTyp; + + /// + /// Side only refers to current side of solution poly + /// + internal EdgeSide Side; + + /// + /// 1 or -1 depending on winding direction + /// + internal int WindDelta; + + /// + /// The winding count + /// + internal int WindCnt; + + /// + /// The winding count of the opposite polytype + /// + internal int WindCnt2; + + /// + /// The out index + /// + internal int OutIdx; + + /// + /// The next + /// + internal TEdge Next; + + /// + /// The previous + /// + internal TEdge Prev; + + /// + /// The next in LML + /// + internal TEdge NextInLML; + + /// + /// The next in ael + /// + internal TEdge NextInAEL; + + /// + /// The previous in ael + /// + internal TEdge PrevInAEL; + + /// + /// The next in sel + /// + internal TEdge NextInSEL; + + /// + /// The previous in sel + /// + internal TEdge PrevInSEL; +#pragma warning restore SA1401 // Field must be + } +} \ No newline at end of file diff --git a/src/Shaper2D/Properties/AssemblyInfo.cs b/src/Shaper2D/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..be46731 --- /dev/null +++ b/src/Shaper2D/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// Common values read from `AssemblyInfo.Common.cs` diff --git a/src/Shaper2D/Rectangle.cs b/src/Shaper2D/Rectangle.cs new file mode 100644 index 0000000..6360a8f --- /dev/null +++ b/src/Shaper2D/Rectangle.cs @@ -0,0 +1,345 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Numerics; + using System.Threading.Tasks; + + /// + /// A way of optermising drawing rectangles. + /// + /// + public class Rectangle : IShape, IPath + { + private readonly Vector2 topLeft; + private readonly Vector2 bottomRight; + private readonly ImmutableArray points; + private readonly IEnumerable pathCollection; + private readonly float halfLength; + private readonly float length; + + /// + /// Initializes a new instance of the class. + /// + /// The x. + /// The y. + /// The width. + /// The height. + public Rectangle(float x, float y, float width, float height) + : this(new Point(x, y), new Size(width, height)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The location. + /// The size. + public Rectangle(Point location, Size size) + { + this.Location = location; + this.topLeft = location; + this.bottomRight = location.Offset(size); + this.Size = size; + + this.points = ImmutableArray.Create(new Point[4] + { + this.topLeft, + new Vector2(this.bottomRight.X, this.topLeft.Y), + this.bottomRight, + new Vector2(this.topLeft.X, this.bottomRight.Y) + }); + + this.halfLength = size.Width + size.Height; + this.length = this.halfLength * 2; + this.pathCollection = new[] { this }; + } + + /// + /// Gets the location. + /// + /// + /// The location. + /// + public Point Location { get; } + + /// + /// Gets the left. + /// + /// + /// The left. + /// + public float Left => this.topLeft.X; + + /// + /// Gets the right. + /// + /// + /// The right. + /// + public float Right => this.bottomRight.X; + + /// + /// Gets the top. + /// + /// + /// The top. + /// + public float Top => this.topLeft.Y; + + /// + /// Gets the bottom. + /// + /// + /// The bottom. + /// + public float Bottom => this.bottomRight.Y; + + /// + /// Gets the bounding box of this shape. + /// + /// + /// The bounds. + /// + Rectangle IShape.Bounds => this; + + /// + /// Gets the bounding box of this shape. + /// + /// + /// The bounds. + /// + Rectangle IPath.Bounds => this; + + /// + /// Gets the paths that make up this shape + /// + /// + /// The paths. + /// + IEnumerable IShape.Paths => this.pathCollection; + + /// + /// Gets a value indicating whether this instance is closed. + /// + /// + /// true if this instance is closed; otherwise, false. + /// + bool IPath.IsClosed => true; + + /// + /// Gets the length of the path + /// + /// + /// The length. + /// + float IPath.Length => this.length; + + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + int IShape.MaxIntersections => 4; + + /// + /// Gets the size. + /// + /// + /// The size. + /// + public Size Size { get; private set; } + + /// + /// Determines if the specfied point is contained within the rectangular region defined by + /// this . + /// + /// The point. + /// + /// The + /// + public bool Contains(Point point) + { + var v = point.ToVector2(); + return Vector2.Clamp(v, this.topLeft, this.bottomRight) == v; + } + + /// + /// Calculates the distance along and away from the path for a specified point. + /// + /// The point along the path. + /// + /// Returns details about the point and its distance away from the path. + /// + PointInfo IPath.Distance(Point point) + { + bool inside; // dont care about inside/outside for paths just distance + return this.Distance(point, false, out inside); + } + + /// + /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds + /// + /// The point. + /// + /// Returns the distance from the shape to the point + /// + public float Distance(Point point) + { + bool insidePoly; + PointInfo result = this.Distance(point, true, out insidePoly); + + // invert the distance from path when inside + return insidePoly ? -result.DistanceFromPath : result.DistanceFromPath; + } + + /// + /// Based on a line described by and + /// populate a buffer for all points on the edges of the + /// that the line intersects. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Point start, Point end, Point[] buffer, int count, int offset) + { + int discovered = 0; + Vector2 startPoint = Vector2.Clamp(start, this.topLeft, this.bottomRight); + Vector2 endPoint = Vector2.Clamp(end, this.topLeft, this.bottomRight); + + if (startPoint == Vector2.Clamp(startPoint, start, end)) + { + // if start closest is within line then its a valid point + discovered++; + buffer[offset++] = startPoint; + } + + if (endPoint == Vector2.Clamp(endPoint, start, end)) + { + // if start closest is within line then its a valid point + discovered++; + buffer[offset++] = endPoint; + } + + return discovered; + } + + /// + /// Converts the into a simple linear path.. + /// + /// + /// Returns the current as simple linear path. + /// + ImmutableArray ILineSegment.AsSimpleLinearPath() + { + return this.points; + } + + private PointInfo Distance(Vector2 point, bool getDistanceAwayOnly, out bool isInside) + { + // point in rectangle + // if after its clamped by the extreams its still the same then it must be inside :) + Vector2 clamped = Vector2.Clamp(point, this.topLeft, this.bottomRight); + isInside = clamped == point; + + float distanceFromEdge = float.MaxValue; + float distanceAlongEdge = 0f; + + if (isInside) + { + // get the absolute distances from the extreams + Vector2 topLeftDist = Vector2.Abs(point - this.topLeft); + Vector2 bottomRightDist = Vector2.Abs(point - this.bottomRight); + + // get the min components + Vector2 minDists = Vector2.Min(topLeftDist, bottomRightDist); + + // and then the single smallest (dont have to worry about direction) + distanceFromEdge = Math.Min(minDists.X, minDists.Y); + + if (!getDistanceAwayOnly) + { + // we need to make clamped the closest point + if (this.topLeft.X + distanceFromEdge == point.X) + { + // closer to lhf + clamped.X = this.topLeft.X; // y is already the same + + // distance along edge is length minus the amout down we are from the top of the rect + distanceAlongEdge = this.length - (clamped.Y - this.topLeft.Y); + } + else if (this.topLeft.Y + distanceFromEdge == point.Y) + { + // closer to top + clamped.Y = this.topLeft.Y; // x is already the same + + distanceAlongEdge = clamped.X - this.topLeft.X; + } + else if (this.bottomRight.Y - distanceFromEdge == point.Y) + { + // closer to bottom + clamped.Y = this.bottomRight.Y; // x is already the same + + distanceAlongEdge = (this.bottomRight.X - clamped.X) + this.halfLength; + } + else if (this.bottomRight.X - distanceFromEdge == point.X) + { + // closer to rhs + clamped.X = this.bottomRight.X; // x is already the same + + distanceAlongEdge = (this.bottomRight.Y - clamped.Y) + this.Size.Width; + } + } + } + else + { + // clamped is the point on the path thats closest no matter what + distanceFromEdge = (clamped - point).Length(); + + if (!getDistanceAwayOnly) + { + // we need to figure out whats the cloests edge now and thus what distance/poitn is closest + if (this.topLeft.X == clamped.X) + { + // distance along edge is length minus the amout down we are from the top of the rect + distanceAlongEdge = this.length - (clamped.Y - this.topLeft.Y); + } + else if (this.topLeft.Y == clamped.Y) + { + distanceAlongEdge = clamped.X - this.topLeft.X; + } + else if (this.bottomRight.Y == clamped.Y) + { + distanceAlongEdge = (this.bottomRight.X - clamped.X) + this.halfLength; + } + else if (this.bottomRight.X == clamped.X) + { + distanceAlongEdge = (this.bottomRight.Y - clamped.Y) + this.Size.Width; + } + } + } + + return new PointInfo + { + SearchPoint = point, + DistanceFromPath = distanceFromEdge, + ClosestPointOnPath = clamped, + DistanceAlongPath = distanceAlongEdge + }; + } + } +} diff --git a/src/Shaper2D/Shaper2D.xproj b/src/Shaper2D/Shaper2D.xproj new file mode 100644 index 0000000..d4260c4 --- /dev/null +++ b/src/Shaper2D/Shaper2D.xproj @@ -0,0 +1,25 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2e33181e-6e28-4662-a801-e2e7dc206029 + Shaper2D + .\obj + .\bin\ + v4.5.1 + + + 2.0 + + + True + + + + + + \ No newline at end of file diff --git a/src/Shaper2D/Size.cs b/src/Shaper2D/Size.cs new file mode 100644 index 0000000..7753b98 --- /dev/null +++ b/src/Shaper2D/Size.cs @@ -0,0 +1,163 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace Shaper2D +{ + using System; + using System.ComponentModel; + using System.Numerics; + using System.Runtime.CompilerServices; + + /// + /// Stores an ordered pair of integers, which specify a height and width. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Size : IEquatable + { + /// + /// Represents a that has Width and Height values set to zero. + /// + public static readonly Size Empty = default(Size); + + private readonly Vector2 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The width of the size. + /// The height of the size. + public Size(float width, float height) + : this(new Vector2(width, height)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector. + public Size(Vector2 vector) + { + this.backingVector = vector; + } + + /// + /// Gets the width of this . + /// + public float Width => this.backingVector.X; + + /// + /// Gets the height of this . + /// + public float Height => this.backingVector.Y; + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Computes the sum of adding two sizes. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size operator +(Size left, Size right) + { + return new Size(left.backingVector + right.backingVector); + } + + /// + /// Computes the difference left by subtracting one size from another. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size operator -(Size left, Size right) + { + return new Size(left.backingVector - right.backingVector); + } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Size left, Size right) + { + return left.backingVector == right.backingVector; + } + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Size left, Size right) + { + return left.backingVector != right.backingVector; + } + + /// + public override int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "Size [ Empty ]"; + } + + return $"Size [ Width={this.Width}, Height={this.Height} ]"; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object obj) + { + if (obj is Size) + { + return this.Equals((Size)obj); + } + + return false; + } + + /// + public bool Equals(Size other) + { + return this.backingVector == other.backingVector; + } + } +} diff --git a/src/Shaper2D/project.json b/src/Shaper2D/project.json new file mode 100644 index 0000000..cb286f2 --- /dev/null +++ b/src/Shaper2D/project.json @@ -0,0 +1,73 @@ +{ + "version": "0.0.1-alpha1-*", + "title": "Shaper2D", + "description": "Polygon manipulation on mergin library", + "authors": [ + "Scott Williams and contributors" + ], + "packOptions": { + "owners": [ + "Scott Williams and contributors" + ], + "projectUrl": "https://github.com/tocsoft/Shaper2D", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", + "iconUrl": "https://raw.githubusercontent.com/tocsoft/Shaper2D/master/build/icons/shaper2d-logo-128.png", + "requireLicenseAcceptance": false, + "repository": { + "type": "git", + "url": "https://github.com/tocsoft/Shaper2D" + }, + "tags": [ + "polygon", + "rectangle", + "point in polygon", + "complex polygons", + "shape", + "2D" + ] + }, + "buildOptions": { + "allowUnsafe": true, + "xmlDoc": true, + "additionalArguments": [ "/additionalfile:../Shared/stylecop.json", "/ruleset:../../Shaper2D.ruleset" ], + "compile": [ + "../Shared/*.cs" + ] + }, + "configurations": { + "Release": { + "buildOptions": { + "warningsAsErrors": true, + "optimize": true + } + } + }, + "dependencies": { + "StyleCop.Analyzers": { + "version": "1.0.0", + "type": "build" + }, + "System.Collections.Immutable": "1.2.0", + "System.Numerics.Vectors": "4.1.1" + }, + "frameworks": { + "netstandard1.1": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Diagnostics.Tools": "4.0.1", + "System.Linq": "4.1.0", + "System.ObjectModel": "4.0.12", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1" + } + }, + "net45": { + "dependencies": { + "System.Runtime": "4.0.0" + } + } + } +} \ No newline at end of file diff --git a/src/Shared/AssemblyInfo.Common.cs b/src/Shared/AssemblyInfo.Common.cs new file mode 100644 index 0000000..4a13f01 --- /dev/null +++ b/src/Shared/AssemblyInfo.Common.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Scott Williams and contributors. +// Licensed under the Apache License, Version 2.0. +// + +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyDescription("A cross-platform library for processing of image files; written in C#")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Scott Williams")] +[assembly: AssemblyProduct("ImageSharp")] +[assembly: AssemblyCopyright("Copyright (c) Scott Williams and contributors.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] + +// Ensure the internals can be tested. +[assembly: InternalsVisibleTo("Shaper2D.Tests")] \ No newline at end of file diff --git a/src/Shared/stylecop.json b/src/Shared/stylecop.json new file mode 100644 index 0000000..df3c8c9 --- /dev/null +++ b/src/Shared/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Scott Williams", + "copyrightText": "Copyright (c) Scott Williams and contributors.\nLicensed under the Apache License, Version 2.0." + } + } +} \ No newline at end of file diff --git a/tests/Shaper2D.Tests/Properties/AssemblyInfo.cs b/tests/Shaper2D.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ef0680a --- /dev/null +++ b/tests/Shaper2D.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ImageSharp.Tests")] +[assembly: AssemblyDescription("A cross-platform library for processing of image files written in C#")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ImageSharp.Tests")] +[assembly: AssemblyCopyright("Copyright © Scott Williams and contributors.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f836e8e6-b4d9-4208-8346-140c74678b91")] diff --git a/tests/Shaper2D.Tests/Shaper2D.Tests.xproj b/tests/Shaper2D.Tests/Shaper2D.Tests.xproj new file mode 100644 index 0000000..d2c2d15 --- /dev/null +++ b/tests/Shaper2D.Tests/Shaper2D.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + f836e8e6-b4d9-4208-8346-140c74678b91 + ImageSharp.Tests + .\obj + .\bin\ + v4.5.1 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/tests/Shaper2D.Tests/project.json b/tests/Shaper2D.Tests/project.json new file mode 100644 index 0000000..4a007f5 --- /dev/null +++ b/tests/Shaper2D.Tests/project.json @@ -0,0 +1,34 @@ +{ + "version": "0.0.0-*", + "configurations": { + "Release": { + "buildOptions": { + "warningsAsErrors": true + } + } + }, + "dependencies": { + "Shaper2D": { + "target": "project", + "version": "0.0.1-*" + }, + "xunit": "2.2.0-*", + "dotnet-test-xunit": "2.2.0-*" + }, + "frameworks": { + "netcoreapp1.1": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0-*" + }, + "Microsoft.CodeCoverage": "1.0.2" + } + }, + "net451": { + "dependencies": { + } + } + }, + "testRunner": "xunit" +} \ No newline at end of file