mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-11 13:21:09 -05:00
Geometry, nesting and import code now call PortableMath instead of System.Math for Sin, Cos, Tan, Atan, Atan2, Asin and Acos (Core, Engine, IO, Data; 269 calls in 64 files). A test fails if those projects call the runtime's trig again. With the runtime's trig, Windows and Linux disagreed in the last bit at non-quarter angles and the same job could nest differently: the learned-angle nests (a cross fixture) placed 23 parts in Strip on Windows and 28 on Linux. Now every Engine.Tests pin gives the same layout on both (3600 passing on Linux and on DesktopPC), Linux layouts are unchanged, and the learned-angle rows are re-pinned to the shared layouts, which pass the overlap and work-area checks, and run on every platform again. Fixes Project Memory 518f3b58.
216 lines
7.4 KiB
C#
216 lines
7.4 KiB
C#
using System.Collections.Generic;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Geometry
|
|
{
|
|
public class BoundingRectangleResult
|
|
{
|
|
public BoundingRectangleResult(double angle, double width, double height)
|
|
{
|
|
Angle = angle;
|
|
Width = width;
|
|
Height = height;
|
|
Area = width * height;
|
|
}
|
|
|
|
public double Angle { get; }
|
|
public double Width { get; }
|
|
public double Height { get; }
|
|
public double Area { get; }
|
|
}
|
|
|
|
public static class RotatingCalipers
|
|
{
|
|
public static BoundingRectangleResult MinimumBoundingRectangle(IList<Vector> points)
|
|
{
|
|
var hull = ConvexHull.Compute(points);
|
|
return MinimumBoundingRectangle(hull);
|
|
}
|
|
|
|
public static BoundingRectangleResult MinimumBoundingRectangle(Polygon hull)
|
|
{
|
|
var vertices = hull.Vertices;
|
|
int n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
|
|
|
|
if (n == 0)
|
|
return new BoundingRectangleResult(0, 0, 0);
|
|
|
|
if (n == 1)
|
|
return new BoundingRectangleResult(0, 0, 0);
|
|
|
|
if (n == 2)
|
|
{
|
|
var dx = vertices[1].X - vertices[0].X;
|
|
var dy = vertices[1].Y - vertices[0].Y;
|
|
var angle = PortableMath.Atan2(dy, dx);
|
|
var length = System.Math.Sqrt(dx * dx + dy * dy);
|
|
return new BoundingRectangleResult(angle, length, 0);
|
|
}
|
|
|
|
// Every edge is a candidate: opposite edges only give the same rectangle
|
|
// when the hull is centrally symmetric. For those hulls (rectangles,
|
|
// best-fit pairs) the second half repeats the first turned 180°, so a
|
|
// second-half edge wins only when smaller beyond rounding; that keeps
|
|
// their orientation from flipping on floating-point noise.
|
|
var half = (n + 1) / 2;
|
|
|
|
BoundingRectangleResult best = null;
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
int next = (i + 1) % n;
|
|
var edgeX = vertices[next].X - vertices[i].X;
|
|
var edgeY = vertices[next].Y - vertices[i].Y;
|
|
var edgeLen = System.Math.Sqrt(edgeX * edgeX + edgeY * edgeY);
|
|
|
|
if (edgeLen.IsEqualTo(0))
|
|
continue;
|
|
|
|
// Unit vectors along and perpendicular to this edge
|
|
var ux = edgeX / edgeLen;
|
|
var uy = edgeY / edgeLen;
|
|
var vx = -uy;
|
|
var vy = ux;
|
|
|
|
// Project all hull vertices onto edge direction (u) and perpendicular (v)
|
|
double minU = double.MaxValue,
|
|
maxU = double.MinValue;
|
|
double minV = double.MaxValue,
|
|
maxV = double.MinValue;
|
|
|
|
for (int j = 0; j < n; j++)
|
|
{
|
|
var projU = vertices[j].X * ux + vertices[j].Y * uy;
|
|
var projV = vertices[j].X * vx + vertices[j].Y * vy;
|
|
|
|
if (projU < minU)
|
|
minU = projU;
|
|
if (projU > maxU)
|
|
maxU = projU;
|
|
if (projV < minV)
|
|
minV = projV;
|
|
if (projV > maxV)
|
|
maxV = projV;
|
|
}
|
|
|
|
var width = maxU - minU;
|
|
var height = maxV - minV;
|
|
var area = width * height;
|
|
var margin = i < half || best == null ? 0 : Tolerance.Epsilon * best.Area;
|
|
|
|
if (best == null || area < best.Area - margin)
|
|
{
|
|
var edgeAngle = PortableMath.Atan2(edgeY, edgeX);
|
|
best = new BoundingRectangleResult(edgeAngle, width, height);
|
|
}
|
|
}
|
|
|
|
return best ?? new BoundingRectangleResult(0, 0, 0);
|
|
}
|
|
|
|
public static BoundingRectangleResult MinimumBoundingRectangle(
|
|
Polygon hull,
|
|
double startAngle,
|
|
double endAngle
|
|
)
|
|
{
|
|
var vertices = hull.Vertices;
|
|
int n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
|
|
|
|
if (n < 3)
|
|
return MinimumBoundingRectangle(hull);
|
|
|
|
startAngle = Angle.NormalizeRad(startAngle);
|
|
if (!endAngle.IsEqualTo(Angle.TwoPI))
|
|
endAngle = Angle.NormalizeRad(endAngle);
|
|
|
|
var candidates = new List<BoundingRectangleResult>();
|
|
|
|
// Evaluate at each hull edge angle
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
int next = (i + 1) % n;
|
|
var edgeX = vertices[next].X - vertices[i].X;
|
|
var edgeY = vertices[next].Y - vertices[i].Y;
|
|
var edgeLen = System.Math.Sqrt(edgeX * edgeX + edgeY * edgeY);
|
|
|
|
if (edgeLen.IsEqualTo(0))
|
|
continue;
|
|
|
|
var edgeAngle = Angle.NormalizeRad(PortableMath.Atan2(edgeY, edgeX));
|
|
|
|
// Each edge produces 4 candidate angles (0, 90, 180, 270 relative to edge)
|
|
for (int k = 0; k < 4; k++)
|
|
{
|
|
var candidateAngle = Angle.NormalizeRad(edgeAngle + k * Angle.HalfPI);
|
|
|
|
if (IsAngleInRange(candidateAngle, startAngle, endAngle))
|
|
candidates.Add(EvaluateAtAngle(vertices, n, candidateAngle));
|
|
}
|
|
}
|
|
|
|
// Also evaluate at range boundaries
|
|
candidates.Add(EvaluateAtAngle(vertices, n, startAngle));
|
|
|
|
if (!endAngle.IsEqualTo(Angle.TwoPI))
|
|
candidates.Add(EvaluateAtAngle(vertices, n, endAngle));
|
|
|
|
BoundingRectangleResult best = null;
|
|
|
|
foreach (var candidate in candidates)
|
|
{
|
|
if (best == null || candidate.Area < best.Area)
|
|
best = candidate;
|
|
}
|
|
|
|
return best ?? new BoundingRectangleResult(startAngle, 0, 0);
|
|
}
|
|
|
|
private static BoundingRectangleResult EvaluateAtAngle(
|
|
IList<Vector> vertices,
|
|
int n,
|
|
double angle
|
|
)
|
|
{
|
|
var cos = PortableMath.Cos(angle);
|
|
var sin = PortableMath.Sin(angle);
|
|
|
|
double minU = double.MaxValue,
|
|
maxU = double.MinValue;
|
|
double minV = double.MaxValue,
|
|
maxV = double.MinValue;
|
|
|
|
for (int j = 0; j < n; j++)
|
|
{
|
|
var projU = vertices[j].X * cos + vertices[j].Y * sin;
|
|
var projV = -vertices[j].X * sin + vertices[j].Y * cos;
|
|
|
|
if (projU < minU)
|
|
minU = projU;
|
|
if (projU > maxU)
|
|
maxU = projU;
|
|
if (projV < minV)
|
|
minV = projV;
|
|
if (projV > maxV)
|
|
maxV = projV;
|
|
}
|
|
|
|
var width = maxU - minU;
|
|
var height = maxV - minV;
|
|
return new BoundingRectangleResult(angle, width, height);
|
|
}
|
|
|
|
private static bool IsAngleInRange(double angle, double start, double end)
|
|
{
|
|
if (end.IsEqualTo(Angle.TwoPI) && start.IsEqualTo(0))
|
|
return true;
|
|
|
|
if (start <= end)
|
|
return angle >= start - Tolerance.Epsilon && angle <= end + Tolerance.Epsilon;
|
|
|
|
// Range wraps around 0 (e.g., 350 to 10 degrees)
|
|
return angle >= start - Tolerance.Epsilon || angle <= end + Tolerance.Epsilon;
|
|
}
|
|
}
|
|
}
|