using System.Linq;
using OpenNest.Engine.Jobs;
using OpenNest.Geometry;
namespace OpenNest.Engine.CirclePacking
{
///
/// A part as the circle packer sees it: its material outline's minimum enclosing circle, and
/// the largest disc inside it that is clear of material. Any shape can be packed this way; a
/// low only means the circle wastes more around the part. Radii are
/// what the layout check sees, so packing is never more lenient than the check: the outline
/// circle covers the check's outward-flattened perimeter, and the hole stays inside its
/// inward-flattened cutouts.
///
internal class Item : Circle
{
/// How far the layout check's flattening may move a contour from its arcs.
public const double FlattenAllowance = NestTolerances.ValidationOutline;
// Optional caller-supplied packing identifier; not assigned automatically; preserved by clones.
public int PackingId { get; set; }
/// Material outline area divided by the enclosing circle's area: 1 for a disc,
/// about 0.85 for a gear, 2/pi for a square. 0 when unknown.
public double FillRatio { get; set; }
/// From to the part's local origin, unrotated.
/// The part's location is Center + PartOffset when it is placed without rotation.
public Vector PartOffset { get; set; }
/// From to the clear disc's center, unrotated.
public Vector HoleOffset { get; set; }
/// Radius of the largest disc clear of material; 0 for a solid part.
public double HoleRadius { get; set; }
/// The clear disc where it currently sits, or null for a solid part.
public Circle Hole => HoleRadius > 0 ? new Circle(Center + HoleOffset, HoleRadius) : null;
public static Item TryCreate(PartGeometrySnapshot snapshot) =>
JobPartGeometry.TryRead(snapshot) is { } geometry ? Create(geometry) : null;
/// Builds the item in the part's unrotated local frame.
public static Item Create(JobPartGeometry geometry)
{
// Every point of the check's outline lies within the allowance of the analytic curve,
// which the circumscribed flattening already contains.
var perimeter = geometry.Perimeter.ToPolygonWithTolerance(FlattenAllowance, circumscribe: true);
var enclosing = MinimumEnclosingCircle.Compute(perimeter.Vertices);
var radius = enclosing.Radius + FlattenAllowance;
var center = enclosing.Center;
var item = new Item
{
Center = center,
Radius = radius,
FillRatio = System.Math.Abs(geometry.Perimeter.Area()) / (System.Math.PI * radius * radius),
PartOffset = -center,
};
foreach (var cutout in geometry.Cutouts)
{
var polygon = cutout.ToPolygonWithTolerance(FlattenAllowance);
var candidates = new[] { PolyLabel.Find(polygon, precision: FlattenAllowance), center };
foreach (var candidate in candidates.Where(polygon.ContainsPoint))
{
// Measured to the analytic boundaries, so it holds however the check flattens
// them; every cutout counts, in case one lies inside another.
var clearance = geometry.Cutouts.Min(c => c.ClosestPointTo(candidate).DistanceTo(candidate))
- FlattenAllowance;
if (clearance > item.HoleRadius)
{
item.HoleRadius = clearance;
item.HoleOffset = candidate - center;
}
}
}
return item;
}
public override Item Clone()
{
var copy = new Item
{
Radius = this.Radius,
Center = this.Center,
Rotation = this.Rotation,
PackingId = this.PackingId,
FillRatio = this.FillRatio,
PartOffset = this.PartOffset,
HoleOffset = this.HoleOffset,
HoleRadius = this.HoleRadius,
};
CopyBaseTo(copy);
return copy;
}
}
}