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); } }