using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.IO;
namespace OpenNest.Engine.BestFit
{
public class NfpSlideStrategy : IBestFitStrategy
{
private static readonly string LogPath = Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.Desktop),
"nfp-slide-debug.log");
private static readonly object LogLock = new object();
private readonly double _part2Rotation;
private readonly Polygon _stationaryPerimeter;
private readonly Polygon _stationaryHull;
private readonly Vector _correction;
public NfpSlideStrategy(double part2Rotation, int type, string description,
Polygon stationaryPerimeter, Polygon stationaryHull, Vector correction)
{
_part2Rotation = part2Rotation;
StrategyIndex = type;
Description = description;
_stationaryPerimeter = stationaryPerimeter;
_stationaryHull = stationaryHull;
_correction = correction;
}
public int StrategyIndex { get; }
public string Description { get; }
///
/// Creates an NfpSlideStrategy by extracting polygon data from a drawing.
/// Returns null if the drawing has no valid perimeter.
///
public static NfpSlideStrategy Create(Drawing drawing, double part2Rotation,
int type, string description, double spacing)
{
var result = PolygonHelper.ExtractPerimeterPolygon(drawing, spacing / 2);
if (result.Polygon == null)
return null;
var hull = ConvexHull.Compute(result.Polygon.Vertices);
Log($"=== Create: drawing={drawing.Name}, rotation={Angle.ToDegrees(part2Rotation):F1}deg ===");
Log($" Perimeter: {result.Polygon.Vertices.Count} verts, bounds={FormatBounds(result.Polygon)}");
Log($" Hull: {hull.Vertices.Count} verts, bounds={FormatBounds(hull)}");
Log($" Correction: ({result.Correction.X:F4}, {result.Correction.Y:F4})");
Log($" ProgramBBox: {drawing.Program.BoundingBox()}");
return new NfpSlideStrategy(part2Rotation, type, description,
result.Polygon, hull, result.Correction);
}
public List GenerateCandidates(Drawing drawing, double spacing, double stepSize)
{
var candidates = new List();
if (stepSize <= 0)
return candidates;
Log($"--- GenerateCandidates: drawing={drawing.Name}, part2Rot={Angle.ToDegrees(_part2Rotation):F1}deg, spacing={spacing}, stepSize={stepSize} ---");
// Orbiting polygon: same shape rotated to Part2's angle.
var orbitingPerimeter = PolygonHelper.RotatePolygon(_stationaryPerimeter, _part2Rotation, reNormalize: true);
var orbitingPoly = ConvexHull.Compute(orbitingPerimeter.Vertices);
Log($" Stationary hull: {_stationaryHull.Vertices.Count} verts, bounds={FormatBounds(_stationaryHull)}");
Log($" Orbiting perimeter (rotated): {orbitingPerimeter.Vertices.Count} verts, bounds={FormatBounds(orbitingPerimeter)}");
Log($" Orbiting hull: {orbitingPoly.Vertices.Count} verts, bounds={FormatBounds(orbitingPoly)}");
var nfp = NoFitPolygon.ComputeConvex(_stationaryHull, orbitingPoly);
if (nfp == null || nfp.Vertices.Count < 3)
{
Log($" NFP failed or degenerate (verts={nfp?.Vertices.Count ?? 0})");
return candidates;
}
var verts = nfp.Vertices;
var vertCount = nfp.IsClosed() ? verts.Count - 1 : verts.Count;
Log($" NFP: {verts.Count} verts (closed={nfp.IsClosed()}, walking {vertCount}), bounds={FormatBounds(nfp)}");
Log($" Correction: ({_correction.X:F4}, {_correction.Y:F4})");
// Log NFP vertices
for (var v = 0; v < vertCount; v++)
Log($" NFP vert[{v}]: ({verts[v].X:F4}, {verts[v].Y:F4}) -> corrected: ({verts[v].X - _correction.X:F4}, {verts[v].Y - _correction.Y:F4})");
// Compare with what RotationSlideStrategy would produce
var part1 = Part.CreateAtOrigin(drawing);
var part2 = Part.CreateAtOrigin(drawing, _part2Rotation);
Log($" Part1 (rot=0): loc=({part1.Location.X:F4}, {part1.Location.Y:F4}), bbox={part1.BoundingBox}");
Log($" Part2 (rot={Angle.ToDegrees(_part2Rotation):F1}): loc=({part2.Location.X:F4}, {part2.Location.Y:F4}), bbox={part2.BoundingBox}");
var testNumber = 0;
for (var i = 0; i < vertCount; i++)
{
var offset = ApplyCorrection(verts[i], _correction);
candidates.Add(MakeCandidate(drawing, offset, spacing, testNumber++));
// Add edge samples for long edges.
var next = (i + 1) % vertCount;
var dx = verts[next].X - verts[i].X;
var dy = verts[next].Y - verts[i].Y;
var edgeLength = System.Math.Sqrt(dx * dx + dy * dy);
if (edgeLength > stepSize)
{
var steps = (int)(edgeLength / stepSize);
for (var s = 1; s < steps; s++)
{
var t = (double)s / steps;
var sample = new Vector(
verts[i].X + dx * t,
verts[i].Y + dy * t);
var sampleOffset = ApplyCorrection(sample, _correction);
candidates.Add(MakeCandidate(drawing, sampleOffset, spacing, testNumber++));
}
}
}
// Log overlap check for vertex candidates (first few)
var checkCount = System.Math.Min(vertCount, 8);
for (var c = 0; c < checkCount; c++)
{
var cand = candidates[c];
var p2 = Part.CreateAtOrigin(drawing, cand.Part2Rotation);
p2.Location = cand.Part2Offset;
var overlaps = part1.Intersects(p2, out _);
Log($" Candidate[{c}]: offset=({cand.Part2Offset.X:F4}, {cand.Part2Offset.Y:F4}), overlaps={overlaps}");
}
Log($" Total candidates: {candidates.Count}");
Log("");
return candidates;
}
private static Vector ApplyCorrection(Vector nfpVertex, Vector correction)
{
return new Vector(nfpVertex.X - correction.X, nfpVertex.Y - correction.Y);
}
private PairCandidate MakeCandidate(Drawing drawing, Vector offset, double spacing, int testNumber)
{
return new PairCandidate
{
Drawing = drawing,
Part1Rotation = 0,
Part2Rotation = _part2Rotation,
Part2Offset = offset,
StrategyIndex = StrategyIndex,
TestNumber = testNumber,
Spacing = spacing
};
}
private static string FormatBounds(Polygon polygon)
{
polygon.UpdateBounds();
var bb = polygon.BoundingBox;
return $"[({bb.Left:F4}, {bb.Bottom:F4})-({bb.Right:F4}, {bb.Top:F4}), {bb.Width:F2}x{bb.Length:F2}]";
}
private static void Log(string message)
{
lock (LogLock)
{
File.AppendAllText(LogPath, message + "\n");
}
}
}
}