using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace OpenNest.Geometry { public class Shape : Entity { /// /// Entities that make up the shape. /// public List Entities; public Shape() { Entities = new List(); } /// /// Returns true if the shape is closed. /// /// public bool IsClosed() { if (Entities.Count == 0) return false; var tol = Math.Tolerance.ChainTolerance; var first = Entities[0]; Vector firstStartPoint; Vector firstEndPoint; switch (first.Type) { case EntityType.Arc: var arc = (Arc)first; firstStartPoint = arc.StartPoint(); firstEndPoint = arc.EndPoint(); break; case EntityType.Circle: return Entities.Count == 1; case EntityType.Line: var line = (Line)first; firstStartPoint = line.StartPoint; firstEndPoint = line.EndPoint; break; default: Debug.Fail("Unhandled geometry type"); return false; } var endpt = firstEndPoint; Entity geo = null; for (int i = 1; i < Entities.Count; ++i) { geo = Entities[i]; switch (geo.Type) { case EntityType.Arc: var arc = (Arc)geo; if (arc.StartPoint().DistanceTo(endpt) > tol) return false; endpt = arc.EndPoint(); break; case EntityType.Circle: return Entities.Count == 1; case EntityType.Line: var line = (Line)geo; if (line.StartPoint.DistanceTo(endpt) > tol) return false; endpt = line.EndPoint; break; default: Debug.Fail("Unhandled geometry type"); return false; } } if (geo == null) return false; var last = geo; Vector lastEndPoint; switch (last.Type) { case EntityType.Arc: var arc = (Arc)last; lastEndPoint = arc.EndPoint(); break; case EntityType.Line: var line = (Line)last; lastEndPoint = line.EndPoint; break; default: Debug.Fail("Unhandled geometry type"); return false; } return lastEndPoint.DistanceTo(firstStartPoint) <= tol; } /// /// Gets the area. /// /// Returns the area or 0 if the shape is NOT closed. public double Area() { // Check if the shape is closed so we can get the area. if (!IsClosed()) return 0; // If the shape is closed and only one entity in the geometry // then that entity would have to be a circle. if (Entities.Count == 1) { var circle = Entities[0] as Circle; return circle == null ? 0 : circle.Area(); } return ToPolygon().Area(); } /// /// Joins all overlapping lines and arcs. /// public void Optimize() { var lines = new List(); var arcs = new List(); foreach (var geo in Entities) { switch (geo.Type) { case EntityType.Arc: arcs.Add((Arc)geo); break; case EntityType.Line: lines.Add((Line)geo); break; } } Helper.Optimize(lines); Helper.Optimize(arcs); } /// /// Gets the closest point on the shape to the given point. /// /// /// Entity that contains the point. /// public Vector ClosestPointTo(Vector pt, out Entity entity) { if (Entities.Count == 0) { entity = null; return Vector.Invalid; } var first = Entities[0]; Vector closestPt = first.ClosestPointTo(pt); double distance = closestPt.DistanceTo(pt); entity = first; for (int i = 1; i < Entities.Count; i++) { var entity2 = Entities[i]; var closestPt2 = entity2.ClosestPointTo(pt); var distance2 = closestPt2.DistanceTo(pt); if (distance2 < distance) { closestPt = closestPt2; distance = distance2; entity = entity2; } } return closestPt; } /// /// Returns a new shape with entities reordered so that the given point on /// the given entity becomes the new start point of the contour. /// /// The point on the entity to reindex at. /// The entity containing the point. /// A new reindexed shape. public Shape ReindexAt(Vector point, Entity entity) { // Circle case: return a new shape with just the circle if (entity is Circle) { var result = new Shape(); result.Entities.Add(entity); return result; } var i = Entities.IndexOf(entity); if (i < 0) throw new ArgumentException("Entity not found in shape", nameof(entity)); // Split the entity at the point Entity firstHalf = null; Entity secondHalf = null; if (entity is Line line) { var (f, s) = line.SplitAt(point); firstHalf = f; secondHalf = s; } else if (entity is Arc arc) { var (f, s) = arc.SplitAt(point); firstHalf = f; secondHalf = s; } // Build reindexed entity list var entities = new List(); // secondHalf of split entity (if not null) if (secondHalf != null) entities.Add(secondHalf); // Entities after the split index (wrapping) for (var j = i + 1; j < Entities.Count; j++) entities.Add(Entities[j]); // Entities before the split index (wrapping) for (var j = 0; j < i; j++) entities.Add(Entities[j]); // firstHalf of split entity (if not null) if (firstHalf != null) entities.Add(firstHalf); var reindexed = new Shape(); reindexed.Entities.AddRange(entities); return reindexed; } /// /// Converts the shape to a polygon. /// /// public Polygon ToPolygon(int arcSegments = 1000) { var polygon = new Polygon(); foreach (var entity in Entities) { switch (entity.Type) { case EntityType.Arc: var arc = (Arc)entity; polygon.Vertices.AddRange(arc.ToPoints(arcSegments)); break; case EntityType.Line: var line = (Line)entity; polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint }); break; case EntityType.Circle: var circle = (Circle)entity; polygon.Vertices.AddRange(circle.ToPoints(arcSegments)); break; default: Debug.Fail("Unhandled geometry type"); break; } } polygon.Close(); polygon.Cleanup(); return polygon; } /// /// Converts the shape to a polygon using a chord tolerance to determine /// the number of segments per arc/circle. /// public Polygon ToPolygonWithTolerance(double tolerance, bool circumscribe = false) { var polygon = new Polygon(); foreach (var entity in Entities) { switch (entity.Type) { case EntityType.Arc: var arc = (Arc)entity; polygon.Vertices.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe)); break; case EntityType.Line: var line = (Line)entity; polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint }); break; case EntityType.Circle: var circle = (Circle)entity; polygon.Vertices.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe)); break; default: Debug.Fail("Unhandled geometry type"); break; } } polygon.Close(); polygon.Cleanup(); return polygon; } /// /// Reverses the rotation direction of the shape. /// public override void Reverse() { Entities.ForEach(e => e.Reverse()); Entities.Reverse(); } /// /// Linear distance of the shape. /// public override double Length { get { return Entities.Sum(geo => geo.Length); } } /// /// Moves the start point to the given coordinates. /// /// /// public override void MoveTo(double x, double y) { throw new NotImplementedException(); } /// /// Moves the start point to the given point. /// /// public override void MoveTo(Vector pt) { throw new NotImplementedException(); } /// /// Offsets the shape location by the given distances. /// /// /// public override void Offset(double x, double y) { Entities.ForEach(e => e.Offset(x, y)); boundingBox.Offset(x, y); } /// /// Offsets the shape location by the given distances. /// /// public override void Offset(Vector voffset) { Entities.ForEach(e => e.Offset(voffset)); boundingBox.Offset(voffset); } /// /// Scales the shape from the zero point. /// /// public override void Scale(double factor) { Entities.ForEach(e => e.Scale(factor)); UpdateBounds(); } /// /// Scales the shape from the origin. /// /// /// public override void Scale(double factor, Vector origin) { Entities.ForEach(e => e.Scale(factor, origin)); UpdateBounds(); } /// /// Rotates the shape from the zero point. /// /// public override void Rotate(double angle) { Entities.ForEach(e => e.Rotate(angle)); UpdateBounds(); } /// /// Rotates the shape from the origin. /// /// /// public override void Rotate(double angle, Vector origin) { Entities.ForEach(e => e.Rotate(angle, origin)); UpdateBounds(); } /// /// Updates the bounding box. /// public override void UpdateBounds() { boundingBox = Entities.Select(geo => geo.BoundingBox) .ToList() .GetBoundingBox(); } public override Entity OffsetEntity(double distance, OffsetSide side) { var offsetShape = new Shape(); var definedShape = new ShapeProfile(this); Entity firstEntity = null; Entity firstOffsetEntity = null; Entity lastEntity = null; Entity lastOffsetEntity = null; foreach (var entity in definedShape.Perimeter.Entities) { var offsetEntity = entity.OffsetEntity(distance, side); if (offsetEntity == null) continue; if (firstEntity == null) { firstEntity = entity; firstOffsetEntity = offsetEntity; } switch (entity.Type) { case EntityType.Line: { var line = (Line)entity; var offsetLine = (Line)offsetEntity; if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line) { JoinOffsetLines( (Line)lastEntity, (Line)lastOffsetEntity, line, offsetLine, distance, side, offsetShape); } offsetShape.Entities.Add(offsetLine); break; } default: offsetShape.Entities.Add(offsetEntity); break; } lastOffsetEntity = offsetEntity; lastEntity = entity; } // Close the shape: join last offset entity back to first if (lastOffsetEntity != null && firstOffsetEntity != null && lastOffsetEntity != firstOffsetEntity && lastOffsetEntity.Type == EntityType.Line && firstOffsetEntity.Type == EntityType.Line) { JoinOffsetLines( (Line)lastEntity, (Line)lastOffsetEntity, (Line)firstEntity, (Line)firstOffsetEntity, distance, side, offsetShape); } foreach (var cutout in definedShape.Cutouts) offsetShape.Entities.AddRange(((Shape)cutout.OffsetEntity(distance, side)).Entities); return offsetShape; } private static void JoinOffsetLines( Line lastLine, Line lastOffsetLine, Line line, Line offsetLine, double distance, OffsetSide side, Shape offsetShape) { Vector intersection; if (Helper.Intersects(offsetLine, lastOffsetLine, out intersection)) { offsetLine.StartPoint = intersection; lastOffsetLine.EndPoint = intersection; } else { var arc = new Arc( line.StartPoint, distance, line.StartPoint.AngleTo(lastOffsetLine.EndPoint), line.StartPoint.AngleTo(offsetLine.StartPoint), side == OffsetSide.Left ); offsetShape.Entities.Add(arc); } } public override Entity OffsetEntity(double distance, Vector pt) { throw new NotImplementedException(); } /// /// Gets the closest point on the shape to the given point. /// /// /// public override Vector ClosestPointTo(Vector pt) { Entity entity; return ClosestPointTo(pt, out entity); } /// /// Returns true if the given arc is intersecting this. /// /// /// public override bool Intersects(Arc arc) { List pts; return Helper.Intersects(arc, this, out pts); } /// /// Returns true if the given arc is intersecting this. /// /// /// /// public override bool Intersects(Arc arc, out List pts) { return Helper.Intersects(arc, this, out pts); } /// /// Returns true if the given circle is intersecting this. /// /// /// public override bool Intersects(Circle circle) { List pts; return Helper.Intersects(circle, this, out pts); } /// /// Returns true if the given circle is intersecting this. /// /// /// /// public override bool Intersects(Circle circle, out List pts) { return Helper.Intersects(circle, this, out pts); } /// /// Returns true if the given line is intersecting this. /// /// /// public override bool Intersects(Line line) { List pts; return Helper.Intersects(line, this, out pts); } /// /// Returns true if the given line is intersecting this. /// /// /// /// public override bool Intersects(Line line, out List pts) { return Helper.Intersects(line, this, out pts); } /// /// Returns true if the given polygon is intersecting this. /// /// /// public override bool Intersects(Polygon polygon) { List pts; return Helper.Intersects(this, polygon, out pts); } /// /// Returns true if the given polygon is intersecting this. /// /// /// /// public override bool Intersects(Polygon polygon, out List pts) { return Helper.Intersects(this, polygon, out pts); } /// /// Returns true if the given shape is intersecting this. /// /// /// public override bool Intersects(Shape shape) { List pts; return Helper.Intersects(this, shape, out pts); } /// /// Returns true if the given shape is intersecting this. /// /// /// /// public override bool Intersects(Shape shape, out List pts) { return Helper.Intersects(this, shape, out pts); } /// /// Type of entity. /// public override EntityType Type { get { return EntityType.Shape; } } public BoundingRectangleResult FindBestRotation() { return Entities.FindBestRotation(); } public BoundingRectangleResult FindBestRotation(double startAngle, double endAngle) { return Entities.FindBestRotation(startAngle, endAngle); } } }