diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index 53bb30f..49ac31d 100644 --- a/OpenNest.Console/Program.cs +++ b/OpenNest.Console/Program.cs @@ -98,8 +98,8 @@ static class NestConsole var parts = args[++i].Split('x'); if (parts.Length == 2) { - o.PlateWidth = double.Parse(parts[0]); - o.PlateHeight = double.Parse(parts[1]); + o.PlateHeight = double.Parse(parts[0]); + o.PlateWidth = double.Parse(parts[1]); } break; case "--check-overlaps": @@ -297,7 +297,8 @@ static class NestConsole static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options) { Console.WriteLine($"Nest: {nest.Name}"); - Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}"); + var wa = plate.WorkArea(); + Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Length:F1} x {plate.Size.Width:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Length:F1}x{wa.Width:F1}"); Console.WriteLine($"Drawing: {drawing.Name}"); Console.WriteLine(options.KeepParts ? $"Keeping {existingCount} existing parts" @@ -386,7 +387,7 @@ static class NestConsole Console.Error.WriteLine(); Console.Error.WriteLine("Modes:"); Console.Error.WriteLine(" Load nest and fill (existing behavior)"); - Console.Error.WriteLine(" --size WxH Import DXF, create plate, and fill"); + Console.Error.WriteLine(" --size LxW Import DXF, create plate, and fill"); Console.Error.WriteLine(" Load nest and add imported DXF drawings"); Console.Error.WriteLine(); Console.Error.WriteLine("Options:"); @@ -394,7 +395,7 @@ static class NestConsole Console.Error.WriteLine(" --plate Plate index to fill (default: 0)"); Console.Error.WriteLine(" --quantity Max parts to place (default: 0 = unlimited)"); Console.Error.WriteLine(" --spacing Override part spacing"); - Console.Error.WriteLine(" --size Override plate size (e.g. 120x60); required for DXF-only mode"); + Console.Error.WriteLine(" --size Override plate size (e.g. 120x60); required for DXF-only mode"); Console.Error.WriteLine(" --output Output nest file path (default: -result.zip)"); Console.Error.WriteLine(" --template Nest template for plate defaults (thickness, quadrant, material, spacing)"); Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill"); diff --git a/OpenNest.Core/Helper.cs b/OpenNest.Core/Helper.cs index ecea400..5b746c1 100644 --- a/OpenNest.Core/Helper.cs +++ b/OpenNest.Core/Helper.cs @@ -1103,7 +1103,7 @@ namespace OpenNest return minDist; } - private static double OneWayDistance( + public static double OneWayDistance( Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset, PushDirection direction) { diff --git a/OpenNest.Engine/BestFit/BestFitCache.cs b/OpenNest.Engine/BestFit/BestFitCache.cs index d7b1fc9..8a12faa 100644 --- a/OpenNest.Engine/BestFit/BestFitCache.cs +++ b/OpenNest.Engine/BestFit/BestFitCache.cs @@ -153,6 +153,9 @@ namespace OpenNest.Engine.BestFit public static void Populate(Drawing drawing, double plateWidth, double plateHeight, double spacing, List results) { + if (results == null || results.Count == 0) + return; + var key = new CacheKey(drawing, plateWidth, plateHeight, spacing); _cache.TryAdd(key, results); } diff --git a/OpenNest.Engine/BestFit/BestFitFinder.cs b/OpenNest.Engine/BestFit/BestFitFinder.cs index b456bf6..b86bc6a 100644 --- a/OpenNest.Engine/BestFit/BestFitFinder.cs +++ b/OpenNest.Engine/BestFit/BestFitFinder.cs @@ -20,10 +20,13 @@ namespace OpenNest.Engine.BestFit { _evaluator = evaluator ?? new PairEvaluator(); _slideComputer = slideComputer; + var plateAspect = System.Math.Max(maxPlateWidth, maxPlateHeight) / + System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001); _filter = new BestFitFilter { MaxPlateWidth = maxPlateWidth, - MaxPlateHeight = maxPlateHeight + MaxPlateHeight = maxPlateHeight, + MaxAspectRatio = System.Math.Max(5.0, plateAspect) }; } diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 7da14dd..395025f 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using OpenNest.Geometry; namespace OpenNest.Engine.BestFit @@ -147,11 +148,82 @@ namespace OpenNest.Engine.BestFit var results = new double[count]; - for (var i = 0; i < count; i++) + // Pre-calculate moving vertices in local space. + var movingVerticesLocal = new HashSet(); + for (var i = 0; i < part2TemplateLines.Count; i++) { - results[i] = Helper.DirectionalDistance( - part2TemplateLines, allDx[i], allDy[i], part1Lines, allDirs[i]); + movingVerticesLocal.Add(part2TemplateLines[i].StartPoint); + movingVerticesLocal.Add(part2TemplateLines[i].EndPoint); } + var movingVerticesArray = movingVerticesLocal.ToArray(); + + // Pre-calculate stationary vertices in local space. + var stationaryVerticesLocal = new HashSet(); + for (var i = 0; i < part1Lines.Count; i++) + { + stationaryVerticesLocal.Add(part1Lines[i].StartPoint); + stationaryVerticesLocal.Add(part1Lines[i].EndPoint); + } + var stationaryVerticesArray = stationaryVerticesLocal.ToArray(); + + // Pre-sort stationary and moving edges for all 4 directions. + var stationaryEdgesByDir = new Dictionary(); + var movingEdgesByDir = new Dictionary(); + + foreach (var dir in AllDirections) + { + var sEdges = new (Vector start, Vector end)[part1Lines.Count]; + for (var i = 0; i < part1Lines.Count; i++) + sEdges[i] = (part1Lines[i].StartPoint, part1Lines[i].EndPoint); + + if (dir == PushDirection.Left || dir == PushDirection.Right) + sEdges = sEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); + else + sEdges = sEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); + stationaryEdgesByDir[dir] = sEdges; + + var opposite = Helper.OppositeDirection(dir); + var mEdges = new (Vector start, Vector end)[part2TemplateLines.Count]; + for (var i = 0; i < part2TemplateLines.Count; i++) + mEdges[i] = (part2TemplateLines[i].StartPoint, part2TemplateLines[i].EndPoint); + + if (opposite == PushDirection.Left || opposite == PushDirection.Right) + mEdges = mEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); + else + mEdges = mEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); + movingEdgesByDir[dir] = mEdges; + } + + // Use Parallel.For for the heavy lifting. + System.Threading.Tasks.Parallel.For(0, count, i => + { + var dx = allDx[i]; + var dy = allDy[i]; + var dir = allDirs[i]; + var movingOffset = new Vector(dx, dy); + + var sEdges = stationaryEdgesByDir[dir]; + var mEdges = movingEdgesByDir[dir]; + var opposite = Helper.OppositeDirection(dir); + + var minDist = double.MaxValue; + + // Case 1: Moving vertices -> Stationary edges + foreach (var mv in movingVerticesArray) + { + var d = Helper.OneWayDistance(mv + movingOffset, sEdges, Vector.Zero, dir); + if (d < minDist) minDist = d; + } + + // Case 2: Stationary vertices -> Moving edges (translated) + foreach (var sv in stationaryVerticesArray) + { + var d = Helper.OneWayDistance(sv, mEdges, movingOffset, opposite); + if (d < minDist) minDist = d; + } + + results[i] = minDist; + }); return results; } diff --git a/OpenNest.Engine/FillLinear.cs b/OpenNest.Engine/FillLinear.cs index deb2deb..09c0ead 100644 --- a/OpenNest.Engine/FillLinear.cs +++ b/OpenNest.Engine/FillLinear.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using OpenNest.Geometry; using OpenNest.Math; @@ -512,7 +513,7 @@ namespace OpenNest { var bag = new System.Collections.Concurrent.ConcurrentBag>(); - foreach (var entry in rotations) + Parallel.ForEach(rotations, entry => { var filler = new FillLinear(strip, PartSpacing); var h = filler.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal); @@ -523,7 +524,7 @@ namespace OpenNest if (v != null && v.Count > 0) bag.Add(v); - } + }); List best = null; diff --git a/OpenNest.Engine/NestEngine.cs b/OpenNest.Engine/NestEngine.cs index 271629a..7344925 100644 --- a/OpenNest.Engine/NestEngine.cs +++ b/OpenNest.Engine/NestEngine.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using OpenNest.Engine.BestFit; using OpenNest.Engine.ML; using OpenNest.Geometry; @@ -215,54 +216,48 @@ namespace OpenNest // Linear phase var linearSw = Stopwatch.StartNew(); - var linearBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>(); - var angleBag = new System.Collections.Concurrent.ConcurrentBag(); - var anglesCompleted = 0; - - System.Threading.Tasks.Parallel.ForEach(angles, - new System.Threading.Tasks.ParallelOptions { CancellationToken = token }, - angle => - { - var localEngine = new FillLinear(workArea, Plate.PartSpacing); - var h = localEngine.Fill(item.Drawing, angle, NestDirection.Horizontal); - var v = localEngine.Fill(item.Drawing, angle, NestDirection.Vertical); - - var angleDeg = Angle.ToDegrees(angle); - if (h != null && h.Count > 0) - { - linearBag.Add((FillScore.Compute(h, workArea), h)); - angleBag.Add(new AngleResult { AngleDeg = angleDeg, Direction = NestDirection.Horizontal, PartCount = h.Count }); - } - if (v != null && v.Count > 0) - { - linearBag.Add((FillScore.Compute(v, workArea), v)); - angleBag.Add(new AngleResult { AngleDeg = angleDeg, Direction = NestDirection.Vertical, PartCount = v.Count }); - } - - var done = Interlocked.Increment(ref anglesCompleted); - var bestCount = System.Math.Max(h?.Count ?? 0, v?.Count ?? 0); - progress?.Report(new NestProgress - { - Phase = NestPhase.Linear, - PlateNumber = PlateNumber, - Description = $"Linear: {done}/{angles.Count} angles, {angleDeg:F0}° = {bestCount} parts" - }); - }); - linearSw.Stop(); - AngleResults.AddRange(angleBag); - var bestLinearCount = 0; - foreach (var (score, parts) in linearBag) + + for (var ai = 0; ai < angles.Count; ai++) { - if (parts.Count > bestLinearCount) - bestLinearCount = parts.Count; - if (score > bestScore) + token.ThrowIfCancellationRequested(); + + var angle = angles[ai]; + var localEngine = new FillLinear(workArea, Plate.PartSpacing); + var h = localEngine.Fill(item.Drawing, angle, NestDirection.Horizontal); + var v = localEngine.Fill(item.Drawing, angle, NestDirection.Vertical); + + var angleDeg = Angle.ToDegrees(angle); + if (h != null && h.Count > 0) { - best = parts; - bestScore = score; - WinnerPhase = NestPhase.Linear; + var scoreH = FillScore.Compute(h, workArea); + AngleResults.Add(new AngleResult { AngleDeg = angleDeg, Direction = NestDirection.Horizontal, PartCount = h.Count }); + if (h.Count > bestLinearCount) bestLinearCount = h.Count; + if (scoreH > bestScore) + { + best = h; + bestScore = scoreH; + WinnerPhase = NestPhase.Linear; + } } + if (v != null && v.Count > 0) + { + var scoreV = FillScore.Compute(v, workArea); + AngleResults.Add(new AngleResult { AngleDeg = angleDeg, Direction = NestDirection.Vertical, PartCount = v.Count }); + if (v.Count > bestLinearCount) bestLinearCount = v.Count; + if (scoreV > bestScore) + { + best = v; + bestScore = scoreV; + WinnerPhase = NestPhase.Linear; + } + } + + ReportProgress(progress, NestPhase.Linear, PlateNumber, best, workArea, + $"Linear: {ai + 1}/{angles.Count} angles, {angleDeg:F0}° best = {bestScore.Count} parts"); } + + linearSw.Stop(); PhaseResults.Add(new PhaseResult(NestPhase.Linear, bestLinearCount, linearSw.ElapsedMilliseconds)); Debug.WriteLine($"[FindBestFill] Linear: {bestScore.Count} parts, density={bestScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Length:F1} | Angles: {angles.Count}"); @@ -369,56 +364,51 @@ namespace OpenNest Plate.PartSpacing); var candidates = SelectPairCandidates(bestFits, workArea); - Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); + var diagMsg = $"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}\n" + + $"[FillWithPairs] Plate: {Plate.Size.Width:F2}x{Plate.Size.Length:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}"; + Debug.WriteLine(diagMsg); + try { System.IO.File.AppendAllText( + System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"), + $"{DateTime.Now:HH:mm:ss} {diagMsg}\n"); } catch { } - var resultBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List parts)>(); + List best = null; + var bestScore = default(FillScore); try { - var pairsCompleted = 0; - var pairsBestCount = 0; + for (var i = 0; i < candidates.Count; i++) + { + token.ThrowIfCancellationRequested(); - System.Threading.Tasks.Parallel.For(0, candidates.Count, - new System.Threading.Tasks.ParallelOptions { CancellationToken = token }, - i => + var result = candidates[i]; + var pairParts = result.BuildParts(item.Drawing); + var angles = result.HullAngles; + var engine = new FillLinear(workArea, Plate.PartSpacing); + var filled = FillPattern(engine, pairParts, angles, workArea); + + if (filled != null && filled.Count > 0) { - var result = candidates[i]; - var pairParts = result.BuildParts(item.Drawing); - var angles = result.HullAngles; - var engine = new FillLinear(workArea, Plate.PartSpacing); - var filled = FillPattern(engine, pairParts, angles, workArea); - - if (filled != null && filled.Count > 0) - resultBag.Add((FillScore.Compute(filled, workArea), filled)); - - var done = Interlocked.Increment(ref pairsCompleted); - InterlockedMax(ref pairsBestCount, filled?.Count ?? 0); - progress?.Report(new NestProgress + var score = FillScore.Compute(filled, workArea); + if (best == null || score > bestScore) { - Phase = NestPhase.Pairs, - PlateNumber = PlateNumber, - Description = $"Pairs: {done}/{candidates.Count} candidates, best = {pairsBestCount} parts" - }); - }); + best = filled; + bestScore = score; + } + } + + ReportProgress(progress, NestPhase.Pairs, PlateNumber, best, workArea, + $"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts"); + } } catch (OperationCanceledException) { Debug.WriteLine("[FillWithPairs] Cancelled mid-phase, using results so far"); } - List best = null; - var bestScore = default(FillScore); - - foreach (var (score, parts) in resultBag) - { - if (best == null || score > bestScore) - { - best = parts; - bestScore = score; - } - } - Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, remnant={bestScore.UsableRemnantArea:F1}, density={bestScore.Density:P1}"); + try { System.IO.File.AppendAllText( + System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"), + $"{DateTime.Now:HH:mm:ss} [FillWithPairs] Best: {bestScore.Count} parts, density={bestScore.Density:P1}\n"); } catch { } return best ?? new List(); } @@ -436,11 +426,14 @@ namespace OpenNest var plateShortSide = System.Math.Min(Plate.Size.Width, Plate.Size.Length); // When the work area is significantly narrower than the plate, - // include all pairs that fit the narrow dimension. + // search ALL candidates (not just kept) for pairs that fit the + // narrow dimension. Pairs rejected by aspect ratio for the full + // plate may be exactly what's needed for a narrow remainder strip. if (workShortSide < plateShortSide * 0.5) { - var stripCandidates = kept - .Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon); + var stripCandidates = bestFits + .Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon + && r.Utilization >= 0.3); var existing = new HashSet(top); @@ -480,32 +473,33 @@ namespace OpenNest private List FillPattern(FillLinear engine, List groupParts, List angles, Box workArea) { - List best = null; - var bestScore = default(FillScore); + var results = new System.Collections.Concurrent.ConcurrentBag<(List Parts, FillScore Score)>(); - foreach (var angle in angles) + Parallel.ForEach(angles, angle => { var pattern = BuildRotatedPattern(groupParts, angle); if (pattern.Parts.Count == 0) - continue; + return; var h = engine.Fill(pattern, NestDirection.Horizontal); - var scoreH = h != null && h.Count > 0 ? FillScore.Compute(h, workArea) : default; - - if (scoreH.Count > 0 && (best == null || scoreH > bestScore)) - { - best = h; - bestScore = scoreH; - } + if (h != null && h.Count > 0) + results.Add((h, FillScore.Compute(h, workArea))); var v = engine.Fill(pattern, NestDirection.Vertical); - var scoreV = v != null && v.Count > 0 ? FillScore.Compute(v, workArea) : default; + if (v != null && v.Count > 0) + results.Add((v, FillScore.Compute(v, workArea))); + }); - if (scoreV.Count > 0 && (best == null || scoreV > bestScore)) + List best = null; + var bestScore = default(FillScore); + + foreach (var res in results) + { + if (best == null || res.Score > bestScore) { - best = v; - bestScore = scoreV; + best = res.Parts; + bestScore = res.Score; } } @@ -766,17 +760,5 @@ namespace OpenNest } } - // --- Utilities --- - - private static void InterlockedMax(ref int location, int value) - { - int current; - do - { - current = location; - if (value <= current) return; - } while (Interlocked.CompareExchange(ref location, value, current) != current); - } - } } diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 0ef8fac..4329cd1 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -862,9 +862,7 @@ namespace OpenNest.Forms var progress = new Progress(p => { progressForm.UpdateProgress(p); - - if (p.BestParts != null) - activeForm.PlateView.SetTemporaryParts(p.BestParts); + activeForm.PlateView.SetTemporaryParts(p.BestParts); }); progressForm.Show(this); @@ -924,9 +922,7 @@ namespace OpenNest.Forms var progress = new Progress(p => { progressForm.UpdateProgress(p); - - if (p.BestParts != null) - activeForm.PlateView.SetTemporaryParts(p.BestParts); + activeForm.PlateView.SetTemporaryParts(p.BestParts); }); Action> onComplete = parts =>