Qwen3.8-Flash-Next's final version after a 14.5-hour optimization run (its commit 7d7fca3): cost-first sheet trials, largest-area-first demand order, and a cached-triangulation exact gate that brought a 219-part production job from timeout to ~106 s. 13/13 tests pass against OpenNest master. README cleaned for publishing: the model-facing template rules are replaced by a one-line independence statement, the production job is described generically instead of by its PEP job/file name (also in a JobSolver comment), results show both sheet pools as re-measured here (the 9-size claim in its report didn't reproduce: it grabs 96x240 and under-fills them), and the stale StockLadder-crash note is gone now that core leaves etch marks out of nesting. Also drops a stale Aurora plugin reference from Opus55's README and lists the engine in the repo README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
384 lines
12 KiB
C#
384 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Qwen38FlashNext.Engine;
|
|
|
|
using Math = System.Math;
|
|
|
|
/// <summary>
|
|
/// Axis-aligned bounding box with no allocation and inclusive intersection tests.
|
|
/// </summary>
|
|
internal readonly struct Bounds
|
|
{
|
|
public Bounds(double minX, double minY, double maxX, double maxY)
|
|
{
|
|
MinX = minX;
|
|
MinY = minY;
|
|
MaxX = maxX;
|
|
MaxY = maxY;
|
|
}
|
|
|
|
public double MinX { get; }
|
|
public double MinY { get; }
|
|
public double MaxX { get; }
|
|
public double MaxY { get; }
|
|
|
|
public bool Intersects(in Bounds other, double margin = 0) =>
|
|
other.MinX <= MaxX + margin
|
|
&& MinX <= other.MaxX + margin
|
|
&& other.MinY <= MaxY + margin
|
|
&& MinY <= other.MaxY + margin;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A convex contour as flat coordinate arrays (closed: last point != first), with
|
|
/// O(log n) strict-inside and exact vertical/horizontal span queries. This is the
|
|
/// engine's own working representation for No-Fit-Polygon geometry; nothing here is
|
|
/// shared with the built-in nesters.
|
|
/// </summary>
|
|
internal sealed class ConvexContour
|
|
{
|
|
// Numerical inset: points within this depth of the boundary count as outside, so a
|
|
// placement resting on the NFP (hull contact) is accepted.
|
|
public const double Surface = 1e-6;
|
|
|
|
private readonly double[] _x;
|
|
private readonly double[] _y;
|
|
|
|
private ConvexContour(double[] x, double[] y, Bounds bounds)
|
|
{
|
|
_x = x;
|
|
_y = y;
|
|
Bounds = bounds;
|
|
_ = FindStart();
|
|
}
|
|
|
|
public Bounds Bounds { get; }
|
|
public int Count => _x.Length;
|
|
|
|
/// <summary>Index of the lexicographic (Y, X) minimum vertex.</summary>
|
|
public int Start { get; private set; }
|
|
|
|
public double X(int i) => _x[i];
|
|
public double Y(int i) => _y[i];
|
|
|
|
public static ConvexContour FromVertices(IList<Vector> points)
|
|
{
|
|
var n = points.Count;
|
|
if (n > 1 && points[0].Equals(points[n - 1]))
|
|
n--;
|
|
if (n < 3)
|
|
throw new ArgumentException("Convex contour needs at least three vertices.");
|
|
|
|
var x = new double[n];
|
|
var y = new double[n];
|
|
var minX = double.MaxValue;
|
|
var minY = double.MaxValue;
|
|
var maxX = double.MinValue;
|
|
var maxY = double.MinValue;
|
|
for (var i = 0; i < n; i++)
|
|
{
|
|
x[i] = points[i].X;
|
|
y[i] = points[i].Y;
|
|
if (x[i] < minX)
|
|
minX = x[i];
|
|
if (x[i] > maxX)
|
|
maxX = x[i];
|
|
if (y[i] < minY)
|
|
minY = y[i];
|
|
if (y[i] > maxY)
|
|
maxY = y[i];
|
|
}
|
|
return new ConvexContour(x, y, new Bounds(minX, minY, maxX, maxY));
|
|
}
|
|
|
|
/// <summary>Regular 2^k-gon approximating a disk of the given radius (convex CCW).</summary>
|
|
public static ConvexContour Disk(double radius, int segments = 32)
|
|
{
|
|
var x = new double[segments];
|
|
var y = new double[segments];
|
|
for (var i = 0; i < segments; i++)
|
|
{
|
|
var angle = 2 * Math.PI * i / segments;
|
|
x[i] = radius * Math.Cos(angle);
|
|
y[i] = radius * Math.Sin(angle);
|
|
}
|
|
return new ConvexContour(x, y, new Bounds(-radius, -radius, radius, radius));
|
|
}
|
|
|
|
public ConvexContour Translated(double dx, double dy)
|
|
{
|
|
var n = _x.Length;
|
|
var x = new double[n];
|
|
var y = new double[n];
|
|
for (var i = 0; i < n; i++)
|
|
{
|
|
x[i] = _x[i] + dx;
|
|
y[i] = _y[i] + dy;
|
|
}
|
|
return new ConvexContour(x, y, new Bounds(Bounds.MinX + dx, Bounds.MinY + dy, Bounds.MaxX + dx, Bounds.MaxY + dy));
|
|
}
|
|
|
|
public double MinX => Bounds.MinX;
|
|
public double MinY => Bounds.MinY;
|
|
public double MaxX => Bounds.MaxX;
|
|
public double MaxY => Bounds.MaxY;
|
|
|
|
/// <summary>
|
|
/// Containment with a <see cref="Surface"/> band: points strictly outside return
|
|
/// false; points inside - OR within the band of an edge - return true, so anchors
|
|
/// resting on the NFP (the usual corner-candidate case) fall through to the exact
|
|
/// material gate instead of being certified by the fast path. The inset may never
|
|
/// exceed the circumscribed spacing disk's chord slack (Disk radius r/cos(pi/24)),
|
|
/// so a hull contact that still clears the true spacing passes the gate.
|
|
/// </summary>
|
|
public bool ContainsPoint(double px, double py)
|
|
{
|
|
var n = _x.Length;
|
|
var sx = _x[Start];
|
|
var sy = _y[Start];
|
|
|
|
// Polar-angle wedge from the start vertex (CCW order: first -> last).
|
|
var first = Mod(Start + 1, n);
|
|
var last = Mod(Start - 1, n);
|
|
var head = Cross(sx, sy, _x[first], _y[first], px, py);
|
|
if (head < -Surface)
|
|
return false;
|
|
var tail = Cross(sx, sy, _x[last], _y[last], px, py);
|
|
if (tail > Surface)
|
|
return false;
|
|
// Within the band of the two wedge rays: conservative inside.
|
|
if (head <= Surface || tail >= -Surface)
|
|
return true;
|
|
|
|
// Binary search for the fan triangle (start, vk, vk+1) bracketing the ray
|
|
// start->p; vk is CCW-ordered so polar angle rises monotonically first->last.
|
|
var lo = 0; // offset (from first) of the last vertex at-or-before p's angle
|
|
var hi = n - 2; // offset of last
|
|
while (hi - lo > 1)
|
|
{
|
|
var mid = (lo + hi) / 2;
|
|
var index = Mod(Start + 1 + mid, n);
|
|
if (Cross(sx, sy, _x[index], _y[index], px, py) >= -Surface)
|
|
lo = mid;
|
|
else
|
|
hi = mid;
|
|
}
|
|
|
|
var a = Mod(Start + 1 + lo, n);
|
|
var b = Mod(Start + 1 + lo + 1, n);
|
|
var edgeAB = Cross(_x[a], _y[a], _x[b], _y[b], px, py);
|
|
if (edgeAB < -Surface)
|
|
return false;
|
|
// Strictly inside the fan triangle, or inside the band of the far edge.
|
|
return edgeAB <= Surface
|
|
|| Cross(sx, sy, _x[a], _y[a], px, py) >= -Surface
|
|
&& Cross(_x[b], _y[b], sx, sy, px, py) >= -Surface;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The vertical span [lo, hi] of the contour's cross-section at x, when x is
|
|
/// strictly inside its x-range (inset by <see cref="Surface"/>); false otherwise.
|
|
/// </summary>
|
|
public bool VerticalSpanAt(double x, out double lo, out double hi)
|
|
{
|
|
lo = 0;
|
|
hi = 0;
|
|
if (x < MinX + Surface || x > MaxX - Surface)
|
|
return false;
|
|
|
|
lo = double.MaxValue;
|
|
hi = double.MinValue;
|
|
var n = _x.Length;
|
|
var j = n - 1;
|
|
for (var i = 0; i < n; i++)
|
|
{
|
|
var x0 = _x[j];
|
|
var x1 = _x[i];
|
|
if ((x0 <= x && x1 >= x) || (x1 <= x && x0 >= x))
|
|
{
|
|
var y0 = _y[j];
|
|
var y1 = _y[i];
|
|
double y;
|
|
if (x1 == x0)
|
|
y = Math.Min(y0, y1);
|
|
else
|
|
y = y0 + (y1 - y0) * (x - x0) / (x1 - x0);
|
|
if (y < lo)
|
|
lo = y;
|
|
if (y > hi)
|
|
hi = y;
|
|
}
|
|
j = i;
|
|
}
|
|
return lo <= hi;
|
|
}
|
|
|
|
/// <summary>The horizontal span at y, inset like <see cref="VerticalSpanAt"/>.</summary>
|
|
public bool HorizontalSpanAt(double y, out double lo, out double hi)
|
|
{
|
|
lo = 0;
|
|
hi = 0;
|
|
if (y < MinY + Surface || y > MaxY - Surface)
|
|
return false;
|
|
|
|
lo = double.MaxValue;
|
|
hi = double.MinValue;
|
|
var n = _x.Length;
|
|
var j = n - 1;
|
|
for (var i = 0; i < n; i++)
|
|
{
|
|
var y0 = _y[j];
|
|
var y1 = _y[i];
|
|
if ((y0 <= y && y1 >= y) || (y1 <= y && y0 >= y))
|
|
{
|
|
var x0 = _x[j];
|
|
var x1 = _x[i];
|
|
double x;
|
|
if (y1 == y0)
|
|
x = Math.Min(x0, x1);
|
|
else
|
|
x = x0 + (x1 - x0) * (y - y0) / (y1 - y0);
|
|
if (x < lo)
|
|
lo = x;
|
|
if (x > hi)
|
|
hi = x;
|
|
}
|
|
j = i;
|
|
}
|
|
return lo <= hi;
|
|
}
|
|
|
|
private int Mod(int i, int n)
|
|
{
|
|
var m = i % n;
|
|
return m < 0 ? m + n : m;
|
|
}
|
|
|
|
private int FindStart()
|
|
{
|
|
var best = 0;
|
|
for (var i = 1; i < _y.Length; i++)
|
|
if (
|
|
_y[i] < _y[best] - 1e-12
|
|
|| (Math.Abs(_y[i] - _y[best]) <= 1e-12 && _x[i] < _x[best])
|
|
)
|
|
best = i;
|
|
Start = best;
|
|
return best;
|
|
}
|
|
|
|
private static double Cross(double ax, double ay, double bx, double by, double px, double py) =>
|
|
(bx - ax) * (py - ay) - (by - ay) * (px - ax);
|
|
}
|
|
|
|
/// <summary>
|
|
/// No-Fit-Polygon geometry for this engine: the Minkowski sum of two convex contours
|
|
/// (the classic linear edge-merge), used to build convex NFPs as
|
|
/// placedHull (+) disk(spacing) (+) reflect(candidateHull). The engine's placement
|
|
/// search consumes these contours directly; it never tessellates part material or
|
|
/// delegates to the built-in NFP machinery.
|
|
/// </summary>
|
|
internal static class NfpGeometry
|
|
{
|
|
/// <summary>
|
|
/// Point-symmetric reflection (rotation by 180 degrees). Negating every vertex
|
|
/// preserves CCW winding, so the vertex order must NOT be reversed - reversing it
|
|
/// would hand the edge-merge a CW contour and corrupt the NFP.
|
|
/// </summary>
|
|
public static ConvexContour Reflect(ConvexContour contour)
|
|
{
|
|
var n = contour.Count;
|
|
var points = new List<Vector>(n);
|
|
for (var i = 0; i < n; i++)
|
|
points.Add(new Vector(-contour.X(i), -contour.Y(i)));
|
|
return ConvexContour.FromVertices(points);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Minkowski sum of two convex CCW contours via angular edge merge, starting from
|
|
/// the sum of each contour's lexicographic (Y, X) minimum vertex. Edges are chosen
|
|
/// by relative angle (cross product); the invariant that the two frontier edges are
|
|
/// always less than 180 degrees apart holds because both walks start at the lowest
|
|
/// vertex and each convex polygon turns by less than 180 degrees per vertex.
|
|
/// </summary>
|
|
public static ConvexContour Minkowski(ConvexContour a, ConvexContour b)
|
|
{
|
|
var na = a.Count;
|
|
var nb = b.Count;
|
|
var edges = new List<(double x, double y)>(na + nb);
|
|
|
|
// Edge vectors walking CCW from each start vertex.
|
|
var edgeA = new (double x, double y)[na];
|
|
for (var k = 0; k < na; k++)
|
|
{
|
|
var p = (a.Start + k) % na;
|
|
var q = (a.Start + k + 1) % na;
|
|
edgeA[k] = (a.X(q) - a.X(p), a.Y(q) - a.Y(p));
|
|
}
|
|
var edgeB = new (double x, double y)[nb];
|
|
for (var k = 0; k < nb; k++)
|
|
{
|
|
var p = (b.Start + k) % nb;
|
|
var q = (b.Start + k + 1) % nb;
|
|
edgeB[k] = (b.X(q) - b.X(p), b.Y(q) - b.Y(p));
|
|
}
|
|
|
|
var ka = 0;
|
|
var kb = 0;
|
|
while (ka < na || kb < nb)
|
|
{
|
|
if (ka >= na)
|
|
{
|
|
edges.Add(edgeB[kb++]);
|
|
continue;
|
|
}
|
|
if (kb >= nb)
|
|
{
|
|
edges.Add(edgeA[ka++]);
|
|
continue;
|
|
}
|
|
|
|
var ea = edgeA[ka];
|
|
var eb = edgeB[kb];
|
|
var cross = ea.x * eb.y - ea.y * eb.x;
|
|
var scale =
|
|
(ea.x * ea.x + ea.y * ea.y) * (eb.x * eb.x + eb.y * eb.y) + 1e-300;
|
|
if (Math.Abs(cross) <= 1e-9 * Math.Sqrt(scale))
|
|
{
|
|
// Same direction: emit the summed edge.
|
|
edges.Add((ea.x + eb.x, ea.y + eb.y));
|
|
ka++;
|
|
kb++;
|
|
}
|
|
else if (cross > 0)
|
|
{
|
|
// cross(ea, eb) > 0: eb is CCW-after ea, so ea is the more clockwise
|
|
// edge and must be emitted first to keep the merge in angular order.
|
|
edges.Add(ea);
|
|
ka++;
|
|
}
|
|
else
|
|
{
|
|
edges.Add(eb);
|
|
kb++;
|
|
}
|
|
}
|
|
|
|
var result = new List<Vector>(edges.Count + 1);
|
|
var px = a.X(a.Start) + b.X(b.Start);
|
|
var py = a.Y(a.Start) + b.Y(b.Start);
|
|
result.Add(new Vector(px, py));
|
|
foreach (var (ex, ey) in edges)
|
|
{
|
|
px += ex;
|
|
py += ey;
|
|
result.Add(new Vector(px, py));
|
|
}
|
|
if (result.Count > 1 && result[0].Equals(result[^1]))
|
|
result.RemoveAt(result.Count - 1);
|
|
return ConvexContour.FromVertices(result);
|
|
}
|
|
}
|