/// <summary> /// Tests if the supplied Polygon is within this Polygon with or without edge coincident vertices when compared on a shared plane. /// </summary> /// <param name="polygon">The Polygon to compare to this Polygon.</param> /// <returns> /// Returns true if every vertex of the supplied Polygon is within this Polygon or coincident with an edge when compared on a shared plane. Returns false if any vertex of the supplied Polygon is outside this Polygon, or if the supplied Polygon is null. /// </returns> public bool Covers(Polygon polygon) { if (polygon == null) { return(false); } if (this.IsClockWise() != polygon.IsClockWise()) { polygon = polygon.Reversed(); } var clipper = new Clipper(); var solution = new List <List <IntPoint> >(); clipper.AddPath(this.ToClipperPath(), PolyType.ptSubject, true); clipper.AddPath(polygon.ToClipperPath(), PolyType.ptClip, true); clipper.Execute(ClipType.ctUnion, solution); if (solution.Count != 1) { return(false); } return(Math.Abs(solution.First().ToPolygon().Area() - this.ToClipperPath().ToPolygon().Area()) <= 0.0001); }
/// <summary> /// Offset this polyline by the specified amount, only on one side. /// </summary> /// <remarks>This blunts sharp corners to keep widths close to the target.</remarks> /// <param name="offset">The amount to offset.</param> /// <param name="flip">Offset on the opposite of the default side. The default is to draw on the +X side of a polyline that goes up the +Y axis.</param> /// <returns>An array of polygons that are extruded from each segment of the polyline.</returns> public Polygon[] OffsetOnSide(double offset, bool flip) { var polygons = new List <Polygon>(); if (this.Vertices.Count <= 1) { return(polygons.ToArray()); } var isCycle = this.Vertices.Count > 2 && this.Vertices[0].DistanceTo(this.Vertices.Last()) <= offset / 2; var segments = this.Segments(); // Step through each point, collecting info on what its join will look like. var joinInfo = new List <Vector3[]>(); for (var vertexIndex = 0; vertexIndex < this.Vertices.Count; vertexIndex++) { var vertex = this.Vertices[vertexIndex]; // Don't draw both the first and last point if treating as a cycle. if (isCycle && vertexIndex == this.Vertices.Count - 1) { continue; } Line previousSegment = null; Line previousOffsetSegment = null; if (vertexIndex - 1 >= 0 || isCycle) { if (vertexIndex == 0) { previousSegment = new Line(this.Vertices[this.Vertices.Count - 2], vertex); } else { previousSegment = segments[vertexIndex - 1]; } previousOffsetSegment = previousSegment.Offset(offset, flip); } Line nextSegment = null; Line nextOffsetSegment = null; if (vertexIndex + 1 < this.Vertices.Count || isCycle) { if (vertexIndex + 1 == this.Vertices.Count) { nextSegment = new Line(vertex, this.Vertices[0]); } else { nextSegment = segments[vertexIndex]; } nextOffsetSegment = nextSegment.Offset(offset, flip); } var joinPoints = new List <Vector3>(); if (previousOffsetSegment != null && nextOffsetSegment != null) { // Find where the virtual edges would naturally intersect. var intersects = previousOffsetSegment.Intersects(nextOffsetSegment, out Vector3 offsetIntersection, true); // When the end of one of the thickened segments overlaps with the other one, the intersection point may be very far away. // Address this by either picking an intersection point on one of the thickened segments or adding a cap (which happens when offsetIntersection is null) if (intersects) { // Identify if the offset interection lands beyond the previous point or beyond the next point in the polyline. if (previousOffsetSegment.Start.DistanceTo(previousOffsetSegment.End) < previousOffsetSegment.End.DistanceTo(offsetIntersection) && previousOffsetSegment.Start.DistanceTo(offsetIntersection) < previousOffsetSegment.End.DistanceTo(offsetIntersection)) { // The edge at the end of the outgoing segment. var endOfNextSegment = new Line(nextOffsetSegment.End, nextSegment.End); if (previousOffsetSegment.Intersects(endOfNextSegment, out Vector3 endOfNextSegmentIntersection)) { // If the virtual offset point of the next line segment is inside the previous segment, the incoming virtual edge should be inside the outgoing thickened segment. offsetIntersection = endOfNextSegmentIntersection; } else if (previousSegment.Intersects(endOfNextSegment, out _)) { // If the next point is entirely inside the previous segment, add a cap since any accute angle would be too narrow. intersects = false; } else { previousOffsetSegment.Start.DistanceTo(nextOffsetSegment, out offsetIntersection); } } else if (nextOffsetSegment.Start.DistanceTo(nextOffsetSegment.End) < nextOffsetSegment.Start.DistanceTo(offsetIntersection) && nextOffsetSegment.Start.DistanceTo(offsetIntersection) > nextOffsetSegment.End.DistanceTo(offsetIntersection)) { // The edge at the end of the incoming segment. var endOfPreviousSegment = new Line(previousOffsetSegment.Start, previousSegment.Start); if (nextOffsetSegment.Intersects(endOfPreviousSegment, out Vector3 endOfPreviousSegmentIntersection)) { // If the virtual offset point of the previous line segment is inside the next segment, the outgoing virtual edge should be inside the incoming thickened segment. offsetIntersection = endOfPreviousSegmentIntersection; } else if (nextSegment.Intersects(endOfPreviousSegment, out _)) { // If the previous point is entirely inside the next segment, add a cap since any accute angle would be too narrow. intersects = false; } else { nextOffsetSegment.End.DistanceTo(previousOffsetSegment, out offsetIntersection); } } } var isAcuteExteriorAngle = false; if (intersects) { isAcuteExteriorAngle = nextOffsetSegment.Direction().Dot((offsetIntersection - vertex).Unitized()) < Math.Cos(Math.PI * -3 / 4); } // Tight joins should get a cap, to maintain minimum width. if (!intersects || isAcuteExteriorAngle && (previousOffsetSegment.End.DistanceTo(offsetIntersection) > offset || nextOffsetSegment.Start.DistanceTo(offsetIntersection) > offset)) { var offsetPoint1 = previousOffsetSegment.Direction() * offset + previousOffsetSegment.End; var offsetPoint2 = nextOffsetSegment.Direction() * (offset * -1) + nextOffsetSegment.Start; joinPoints.Add(offsetPoint1); if (offsetPoint1.DistanceTo(offsetPoint2) != 0) { joinPoints.Add(offsetPoint2); } } else { joinPoints.Add(offsetIntersection); } } else if (previousOffsetSegment != null) { joinPoints.Add(previousOffsetSegment.End); } else if (nextOffsetSegment != null) { joinPoints.Add(nextOffsetSegment.Start); } else { continue; } joinPoints.Add(vertex); joinInfo.Add(joinPoints.ToArray()); } // Create a polygon for each point's join, connecting back to the end of the previous point's join. for (var joinIndex = 0; joinIndex < joinInfo.Count; joinIndex++) { if (joinIndex == 0 && !isCycle) { continue; } var joinPoints = joinInfo[joinIndex]; var previousJoinPoints = joinIndex > 0 ? joinInfo[joinIndex - 1] : joinInfo.Last(); var vertices = new List <Vector3>(); vertices.Add(previousJoinPoints.Last()); vertices.Add(previousJoinPoints[previousJoinPoints.Length - 2]); vertices.AddRange(joinPoints); var polygon = new Polygon(vertices); if (polygon.IsClockWise()) { polygons.Add(polygon.Reversed()); } else { polygons.Add(polygon); } } return(polygons.ToArray()); }