using System;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Engine.Qwen38FlashNext.Engine;
using Math = System.Math;
///
/// Axis-aligned bounding box with no allocation and inclusive intersection tests.
///
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;
}
///
/// 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.
///
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;
/// Index of the lexicographic (Y, X) minimum vertex.
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 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));
}
/// Regular 2^k-gon approximating a disk of the given radius (convex CCW).
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;
///
/// Containment with a 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.
///
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;
}
///
/// The vertical span [lo, hi] of the contour's cross-section at x, when x is
/// strictly inside its x-range (inset by ); false otherwise.
///
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;
}
/// The horizontal span at y, inset like .
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);
}
///
/// 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.
///
internal static class NfpGeometry
{
///
/// 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.
///
public static ConvexContour Reflect(ConvexContour contour)
{
var n = contour.Count;
var points = new List(n);
for (var i = 0; i < n; i++)
points.Add(new Vector(-contour.X(i), -contour.Y(i)));
return ConvexContour.FromVertices(points);
}
///
/// 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.
///
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(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);
}
}