using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest
{
///
/// Computes the rotation that maps a drawing to its canonical (MBR-axis-aligned) frame.
/// Lives in OpenNest.Core so Drawing.Program setter can invoke it directly without
/// a circular dependency on OpenNest.Engine.
///
public static class CanonicalAngle
{
/// Angles with |v| below this (radians) are snapped to 0.
public const double SnapToZero = 0.001;
/// Centroid offsets below this fraction of the MBR extent count as symmetric.
private const double SymmetryTolerance = 1e-6;
/// Angular margin (radians) keeping axis-aligned centroid offsets off the edge of the preferred quadrant.
private const double PreferenceMargin = 0.001;
///
/// Derives the canonical angle from a pre-computed MBR. Used both by Compute (which
/// computes the MBR itself) and by PartClassifier (which already has one). Single formula
/// across both callers.
///
public static double FromMbr(BoundingRectangleResult mbr)
{
if (mbr.Area <= OpenNest.Math.Tolerance.Epsilon)
return 0.0;
// The MBR edge angle can represent any of four equivalent orientations
// (edge-i, edge-i + π/2, edge-i + π, edge-i - π/2) depending on which hull
// edge the algorithm happened to pick. Normalize -mbr.Angle to the
// representative in [-π/4, π/4] so snap-to-zero works for inputs near
// ANY of the equivalent orientations.
var angle = -mbr.Angle;
const double halfPi = System.Math.PI / 2.0;
angle -= halfPi * System.Math.Round(angle / halfPi);
if (System.Math.Abs(angle) < SnapToZero)
return 0.0;
return angle;
}
public static double Compute(Drawing drawing)
{
if (drawing?.Program == null)
return 0.0;
var entities = ConvertProgram
.ToGeometry(drawing.Program)
.Where(e => e.Layer != SpecialLayers.Rapid);
var shapes = ShapeBuilder.GetShapes(entities);
if (shapes.Count == 0)
return 0.0;
var perimeter = shapes[0];
var perimeterArea = perimeter.Area();
for (var i = 1; i < shapes.Count; i++)
{
var area = shapes[i].Area();
if (area > perimeterArea)
{
perimeter = shapes[i];
perimeterArea = area;
}
}
var polygon = perimeter.ToPolygonWithTolerance(0.1);
if (polygon == null || polygon.Vertices.Count < 3)
return 0.0;
var hull = ConvexHull.Compute(polygon.Vertices);
if (hull.Vertices.Count < 3)
return 0.0;
var mbr = RotatingCalipers.MinimumBoundingRectangle(hull);
var angle = FromMbr(mbr);
if (mbr.Area <= OpenNest.Math.Tolerance.Epsilon)
return angle;
var quarterTurns = PreferredQuarterTurns(polygon, hull, angle);
if (quarterTurns == 0)
return angle;
return NormalizeSigned(angle + quarterTurns * System.Math.PI / 2.0);
}
///
/// The MBR only fixes the frame modulo 90°, leaving four equivalent orientations. Nest
/// results are not 90°-symmetric, so pick one deterministically: the quarter-turn count
/// that puts the perimeter's centroid toward the lower-left of its MBR. Shapes with no
/// centroid offset (rectangles, circles) are symmetric and keep the MBR orientation.
///
private static int PreferredQuarterTurns(Polygon polygon, Polygon hull, double angle)
{
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
foreach (var vertex in hull.Vertices)
{
var rotated = vertex.Rotate(angle);
minX = System.Math.Min(minX, rotated.X);
minY = System.Math.Min(minY, rotated.Y);
maxX = System.Math.Max(maxX, rotated.X);
maxY = System.Math.Max(maxY, rotated.Y);
}
var centroid = Centroid(polygon).Rotate(angle);
var dx = centroid.X - (minX + maxX) / 2.0;
var dy = centroid.Y - (minY + maxY) / 2.0;
var extent = System.Math.Max(maxX - minX, maxY - minY);
if (System.Math.Sqrt(dx * dx + dy * dy) <= SymmetryTolerance * extent)
return 0;
// Choose k so the offset direction lands in [PI - margin, 3PI/2 - margin). The margin
// keeps offsets lying exactly on an axis (mirror-symmetric parts) away from the
// interval edge so floating-point noise cannot flip the choice.
var halfPi = System.Math.PI / 2.0;
var direction = System.Math.Atan2(dy, dx);
for (var turns = 0; turns < 4; turns++)
{
var relative = direction + turns * halfPi - (System.Math.PI - PreferenceMargin);
relative -= 2.0 * System.Math.PI * System.Math.Floor(relative / (2.0 * System.Math.PI));
if (relative < halfPi)
return turns;
}
return 0;
}
private static Vector Centroid(Polygon polygon)
{
var vertices = polygon.Vertices;
var doubleArea = 0.0;
var cx = 0.0;
var cy = 0.0;
for (var i = 0; i < vertices.Count; i++)
{
var p = vertices[i];
var q = vertices[(i + 1) % vertices.Count];
var cross = p.X * q.Y - q.X * p.Y;
doubleArea += cross;
cx += (p.X + q.X) * cross;
cy += (p.Y + q.Y) * cross;
}
if (System.Math.Abs(doubleArea) <= OpenNest.Math.Tolerance.Epsilon)
return new Vector(vertices.Average(v => v.X), vertices.Average(v => v.Y));
return new Vector(cx / (3.0 * doubleArea), cy / (3.0 * doubleArea));
}
private static double NormalizeSigned(double angle)
{
var twoPi = 2.0 * System.Math.PI;
angle -= twoPi * System.Math.Floor((angle + System.Math.PI) / twoPi);
return angle;
}
}
}