feat(qwen38flashnext): add the finished engine
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>
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user