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;
}
}
GeometryOptimizer.Optimize(lines);
GeometryOptimizer.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 (Intersect.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 Intersect.Intersects(arc, this, out pts);
}
///
/// Returns true if the given arc is intersecting this.
///
///
///
///
public override bool Intersects(Arc arc, out List pts)
{
return Intersect.Intersects(arc, this, out pts);
}
///
/// Returns true if the given circle is intersecting this.
///
///
///
public override bool Intersects(Circle circle)
{
List pts;
return Intersect.Intersects(circle, this, out pts);
}
///
/// Returns true if the given circle is intersecting this.
///
///
///
///
public override bool Intersects(Circle circle, out List pts)
{
return Intersect.Intersects(circle, this, out pts);
}
///
/// Returns true if the given line is intersecting this.
///
///
///
public override bool Intersects(Line line)
{
List pts;
return Intersect.Intersects(line, this, out pts);
}
///
/// Returns true if the given line is intersecting this.
///
///
///
///
public override bool Intersects(Line line, out List pts)
{
return Intersect.Intersects(line, this, out pts);
}
///
/// Returns true if the given polygon is intersecting this.
///
///
///
public override bool Intersects(Polygon polygon)
{
List pts;
return Intersect.Intersects(this, polygon, out pts);
}
///
/// Returns true if the given polygon is intersecting this.
///
///
///
///
public override bool Intersects(Polygon polygon, out List pts)
{
return Intersect.Intersects(this, polygon, out pts);
}
///
/// Returns true if the given shape is intersecting this.
///
///
///
public override bool Intersects(Shape shape)
{
List pts;
return Intersect.Intersects(this, shape, out pts);
}
///
/// Returns true if the given shape is intersecting this.
///
///
///
///
public override bool Intersects(Shape shape, out List pts)
{
return Intersect.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);
}
}
}