fix(engine): make canonical-frame fills orientation-invariant
Part.Rotation is cumulative, so rebinding canonical parts with CreateAtOrigin(original, p.Rotation) double-counted the drawing's own rotation, and FromCanonical rotated each part about its Location, which moved it off its slot and out of the work area. Add CanonicalFrame.RebindToOriginal (rotation = part - original program rotation, footprint aligned to the canonical part) and use it in the three places that duplicated the old logic. The MBR only fixes the frame modulo 90 degrees and nest results are not 90-degree symmetric (an L gave 56/43/42/42 parts by orientation). CanonicalAngle.Compute now picks one of the four orientations from the centroid offset; symmetric shapes keep the MBR orientation. Fixes the three NestInvarianceTests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,12 @@ namespace OpenNest
|
||||
/// <summary>Angles with |v| below this (radians) are snapped to 0.</summary>
|
||||
public const double SnapToZero = 0.001;
|
||||
|
||||
/// <summary>Centroid offsets below this fraction of the MBR extent count as symmetric.</summary>
|
||||
private const double SymmetryTolerance = 1e-6;
|
||||
|
||||
/// <summary>Angular margin (radians) keeping axis-aligned centroid offsets off the edge of the preferred quadrant.</summary>
|
||||
private const double PreferenceMargin = 0.001;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
@@ -73,7 +79,89 @@ namespace OpenNest
|
||||
return 0.0;
|
||||
|
||||
var mbr = RotatingCalipers.MinimumBoundingRectangle(hull);
|
||||
return FromMbr(mbr);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user