diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs index 3f852ac..d45fa5d 100644 --- a/OpenNest.Core/Geometry/SpatialQuery.cs +++ b/OpenNest.Core/Geometry/SpatialQuery.cs @@ -104,6 +104,95 @@ namespace OpenNest.Geometry return double.MaxValue; } + /// + /// Computes the distance from a point along a direction to an arc. + /// Solves ray-circle intersection, then constrains hits to the arc's + /// angular span. Returns double.MaxValue if no hit. + /// + [System.Runtime.CompilerServices.MethodImpl( + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static double RayArcDistance( + double vx, double vy, + double cx, double cy, double r, + double startAngle, double endAngle, bool reversed, + double dirX, double dirY) + { + // Ray: P = (vx,vy) + t*(dirX,dirY) + // Circle: (x-cx)^2 + (y-cy)^2 = r^2 + var ox = vx - cx; + var oy = vy - cy; + + // a = dirX^2 + dirY^2 = 1 for unit direction, but handle general case + var a = dirX * dirX + dirY * dirY; + var b = 2.0 * (ox * dirX + oy * dirY); + var c = ox * ox + oy * oy - r * r; + + var discriminant = b * b - 4.0 * a * c; + if (discriminant < 0) + return double.MaxValue; + + var sqrtD = System.Math.Sqrt(discriminant); + var inv2a = 1.0 / (2.0 * a); + var t1 = (-b - sqrtD) * inv2a; + var t2 = (-b + sqrtD) * inv2a; + + var best = double.MaxValue; + + if (t1 > -Tolerance.Epsilon) + { + var hitAngle = Angle.NormalizeRad(System.Math.Atan2( + vy + t1 * dirY - cy, vx + t1 * dirX - cx)); + if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) + best = t1 > Tolerance.Epsilon ? t1 : 0; + } + + if (t2 > -Tolerance.Epsilon && t2 < best) + { + var hitAngle = Angle.NormalizeRad(System.Math.Atan2( + vy + t2 * dirY - cy, vx + t2 * dirX - cx)); + if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) + best = t2 > Tolerance.Epsilon ? t2 : 0; + } + + return best; + } + + /// + /// Computes the distance from a point along a direction to a full circle. + /// Returns double.MaxValue if no hit. + /// + [System.Runtime.CompilerServices.MethodImpl( + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public static double RayCircleDistance( + double vx, double vy, + double cx, double cy, double r, + double dirX, double dirY) + { + var ox = vx - cx; + var oy = vy - cy; + + var a = dirX * dirX + dirY * dirY; + var b = 2.0 * (ox * dirX + oy * dirY); + var c = ox * ox + oy * oy - r * r; + + var discriminant = b * b - 4.0 * a * c; + if (discriminant < 0) + return double.MaxValue; + + var sqrtD = System.Math.Sqrt(discriminant); + var t = (-b - sqrtD) / (2.0 * a); + + if (t > Tolerance.Epsilon) return t; + if (t >= -Tolerance.Epsilon) return 0; + + // First root is behind us, try the second + t = (-b + sqrtD) / (2.0 * a); + if (t > Tolerance.Epsilon) return t; + if (t >= -Tolerance.Epsilon) return 0; + + return double.MaxValue; + } + /// /// Computes the minimum translation distance along a push direction before /// any edge of movingLines contacts any edge of stationaryLines. diff --git a/OpenNest.Core/PartGeometry.cs b/OpenNest.Core/PartGeometry.cs index ce68f53..d6149fe 100644 --- a/OpenNest.Core/PartGeometry.cs +++ b/OpenNest.Core/PartGeometry.cs @@ -39,7 +39,30 @@ namespace OpenNest return lines; } - public static List GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001) + /// + /// Returns the perimeter entities (Line, Arc, Circle) with spacing offset applied, + /// without tessellation. Much faster than GetOffsetPartLines for parts with many arcs. + /// + public static List GetOffsetPerimeterEntities(Part part, double spacing) + { + var geoEntities = ConvertProgram.ToGeometry(part.Program); + var profile = new ShapeProfile( + geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + + var offsetShape = profile.Perimeter.OffsetOutward(spacing); + if (offsetShape == null) + return new List(); + + // Offset the shape's entities to the part's location. + // OffsetOutward creates a new Shape, so mutating is safe. + foreach (var entity in offsetShape.Entities) + entity.Offset(part.Location); + + return offsetShape.Entities; + } + + public static List GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001, + bool perimeterOnly = false) { var entities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( @@ -50,9 +73,12 @@ namespace OpenNest AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing), chordTolerance, part.Location); - foreach (var cutout in profile.Cutouts) - AddOffsetLines(lines, cutout.OffsetInward(totalSpacing), - chordTolerance, part.Location); + if (!perimeterOnly) + { + foreach (var cutout in profile.Cutouts) + AddOffsetLines(lines, cutout.OffsetInward(totalSpacing), + chordTolerance, part.Location); + } return lines; } diff --git a/OpenNest.Engine/BestFit/BestFitFinder.cs b/OpenNest.Engine/BestFit/BestFitFinder.cs index 90bf2da..3332ec8 100644 --- a/OpenNest.Engine/BestFit/BestFitFinder.cs +++ b/OpenNest.Engine/BestFit/BestFitFinder.cs @@ -4,6 +4,7 @@ using OpenNest.Geometry; using OpenNest.Math; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -49,6 +50,8 @@ namespace OpenNest.Engine.BestFit var allCandidates = candidateBags.SelectMany(c => c).ToList(); + Debug.WriteLine($"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates"); + var results = _evaluator.EvaluateAll(allCandidates); _filter.Apply(results); diff --git a/OpenNest.Engine/BestFit/CpuDistanceComputer.cs b/OpenNest.Engine/BestFit/CpuDistanceComputer.cs index 3de81ee..2c89770 100644 --- a/OpenNest.Engine/BestFit/CpuDistanceComputer.cs +++ b/OpenNest.Engine/BestFit/CpuDistanceComputer.cs @@ -1,4 +1,5 @@ using OpenNest.Geometry; +using OpenNest.Math; using System.Collections.Generic; using System.Linq; @@ -17,7 +18,6 @@ namespace OpenNest.Engine.BestFit var allMovingVerts = ExtractUniqueVertices(movingTemplateLines); var allStationaryVerts = ExtractUniqueVertices(stationaryLines); - // Pre-filter vertices per unique direction (typically 4 cardinal directions). var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>(); foreach (var offset in offsets) @@ -43,7 +43,6 @@ namespace OpenNest.Engine.BestFit var minDist = double.MaxValue; - // Case 1: Leading moving vertices → stationary edges for (var v = 0; v < leadingMoving.Length; v++) { var vx = leadingMoving[v].X + offset.Dx; @@ -66,7 +65,6 @@ namespace OpenNest.Engine.BestFit } } - // Case 2: Facing stationary vertices → moving edges (opposite direction) for (var v = 0; v < facingStationary.Length; v++) { var svx = facingStationary[v].X; @@ -95,6 +93,178 @@ namespace OpenNest.Engine.BestFit return results; } + public double[] ComputeDistances( + List stationaryEntities, + List movingEntities, + SlideOffset[] offsets) + { + var count = offsets.Length; + var results = new double[count]; + + var allMovingVerts = ExtractVerticesFromEntities(movingEntities); + var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities); + + var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>(); + + foreach (var offset in offsets) + { + var key = (offset.DirX, offset.DirY); + if (vertexCache.ContainsKey(key)) + continue; + + var leading = FilterVerticesByProjection(allMovingVerts, offset.DirX, offset.DirY, keepHigh: true); + var facing = FilterVerticesByProjection(allStationaryVerts, offset.DirX, offset.DirY, keepHigh: false); + vertexCache[key] = (leading, facing); + } + + System.Threading.Tasks.Parallel.For(0, count, i => + { + var offset = offsets[i]; + var dirX = offset.DirX; + var dirY = offset.DirY; + var oppX = -dirX; + var oppY = -dirY; + + var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)]; + + var minDist = double.MaxValue; + + // Case 1: Leading moving vertices → stationary entities + for (var v = 0; v < leadingMoving.Length; v++) + { + var vx = leadingMoving[v].X + offset.Dx; + var vy = leadingMoving[v].Y + offset.Dy; + + for (var j = 0; j < stationaryEntities.Count; j++) + { + var d = RayEntityDistance(vx, vy, stationaryEntities[j], 0, 0, dirX, dirY); + + if (d < minDist) + { + minDist = d; + if (d <= 0) { results[i] = 0; return; } + } + } + } + + // Case 2: Facing stationary vertices → moving entities (opposite direction) + for (var v = 0; v < facingStationary.Length; v++) + { + var svx = facingStationary[v].X; + var svy = facingStationary[v].Y; + + for (var j = 0; j < movingEntities.Count; j++) + { + var d = RayEntityDistance(svx, svy, movingEntities[j], offset.Dx, offset.Dy, oppX, oppY); + + if (d < minDist) + { + minDist = d; + if (d <= 0) { results[i] = 0; return; } + } + } + } + + results[i] = minDist; + }); + + return results; + } + + private static double RayEntityDistance( + double vx, double vy, Entity entity, + double entityOffsetX, double entityOffsetY, + double dirX, double dirY) + { + if (entity is Line line) + { + return SpatialQuery.RayEdgeDistance( + vx, vy, + line.StartPoint.X + entityOffsetX, line.StartPoint.Y + entityOffsetY, + line.EndPoint.X + entityOffsetX, line.EndPoint.Y + entityOffsetY, + dirX, dirY); + } + + if (entity is Arc arc) + { + return SpatialQuery.RayArcDistance( + vx, vy, + arc.Center.X + entityOffsetX, arc.Center.Y + entityOffsetY, + arc.Radius, + arc.StartAngle, arc.EndAngle, arc.IsReversed, + dirX, dirY); + } + + if (entity is Circle circle) + { + return SpatialQuery.RayCircleDistance( + vx, vy, + circle.Center.X + entityOffsetX, circle.Center.Y + entityOffsetY, + circle.Radius, + dirX, dirY); + } + + return double.MaxValue; + } + + private static Vector[] ExtractVerticesFromEntities(List entities) + { + var vertices = new HashSet(); + + for (var i = 0; i < entities.Count; i++) + { + var entity = entities[i]; + + if (entity is Line line) + { + vertices.Add(line.StartPoint); + vertices.Add(line.EndPoint); + } + else if (entity is Arc arc) + { + vertices.Add(arc.StartPoint()); + vertices.Add(arc.EndPoint()); + AddArcExtremes(vertices, arc); + } + else if (entity is Circle circle) + { + // Four cardinal points + vertices.Add(new Vector(circle.Center.X + circle.Radius, circle.Center.Y)); + vertices.Add(new Vector(circle.Center.X - circle.Radius, circle.Center.Y)); + vertices.Add(new Vector(circle.Center.X, circle.Center.Y + circle.Radius)); + vertices.Add(new Vector(circle.Center.X, circle.Center.Y - circle.Radius)); + } + } + + return vertices.ToArray(); + } + + private static void AddArcExtremes(HashSet points, Arc arc) + { + var a1 = arc.StartAngle; + var a2 = arc.EndAngle; + var reversed = arc.IsReversed; + + if (reversed) + Generic.Swap(ref a1, ref a2); + + // Right (0°) + if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2)) + points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y)); + + // Top (90°) + if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2)) + points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius)); + + // Left (180°) + if (Angle.IsBetweenRad(System.Math.PI, a1, a2)) + points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y)); + + // Bottom (270°) + if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2)) + points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius)); + } + private static Vector[] ExtractUniqueVertices(List lines) { var vertices = new HashSet(); @@ -106,11 +276,6 @@ namespace OpenNest.Engine.BestFit return vertices.ToArray(); } - /// - /// Filters vertices by their projection onto the push direction. - /// keepHigh=true returns the leading half (front face, closest to target). - /// keepHigh=false returns the facing half (side facing the approaching part). - /// private static Vector[] FilterVerticesByProjection( Vector[] vertices, double dirX, double dirY, bool keepHigh) { diff --git a/OpenNest.Engine/BestFit/GpuDistanceComputer.cs b/OpenNest.Engine/BestFit/GpuDistanceComputer.cs index 591bbe2..5c4e05f 100644 --- a/OpenNest.Engine/BestFit/GpuDistanceComputer.cs +++ b/OpenNest.Engine/BestFit/GpuDistanceComputer.cs @@ -36,6 +36,16 @@ namespace OpenNest.Engine.BestFit flatOffsets, count, directions); } + public double[] ComputeDistances( + List stationaryEntities, + List movingEntities, + SlideOffset[] offsets) + { + // GPU path doesn't support native entities yet — fall back to CPU. + var cpu = new CpuDistanceComputer(); + return cpu.ComputeDistances(stationaryEntities, movingEntities, offsets); + } + /// /// Maps a unit direction vector to a PushDirection int for the GPU interface. /// Left=0, Down=1, Right=2, Up=3. diff --git a/OpenNest.Engine/BestFit/IDistanceComputer.cs b/OpenNest.Engine/BestFit/IDistanceComputer.cs index 5c208c7..36e2a9d 100644 --- a/OpenNest.Engine/BestFit/IDistanceComputer.cs +++ b/OpenNest.Engine/BestFit/IDistanceComputer.cs @@ -9,5 +9,10 @@ namespace OpenNest.Engine.BestFit List stationaryLines, List movingTemplateLines, SlideOffset[] offsets); + + double[] ComputeDistances( + List stationaryEntities, + List movingEntities, + SlideOffset[] offsets); } } diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 7d76e16..02541ee 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -36,8 +36,8 @@ namespace OpenNest.Engine.BestFit var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation); var halfSpacing = spacing / 2; - var part1Lines = PartGeometry.GetOffsetPartLines(part1, halfSpacing); - var part2TemplateLines = PartGeometry.GetOffsetPartLines(part2Template, halfSpacing); + var part1Entities = PartGeometry.GetOffsetPerimeterEntities(part1, halfSpacing); + var part2Entities = PartGeometry.GetOffsetPerimeterEntities(part2Template, halfSpacing); var bbox1 = part1.BoundingBox; var bbox2 = part2Template.BoundingBox; @@ -48,7 +48,7 @@ namespace OpenNest.Engine.BestFit return candidates; var distances = _distanceComputer.ComputeDistances( - part1Lines, part2TemplateLines, offsets); + part1Entities, part2Entities, offsets); var testNumber = 0; @@ -90,15 +90,18 @@ namespace OpenNest.Engine.BestFit if (isHorizontalPush) { // Perpendicular sweep along Y → Width; push extent along X → Length - perpMin = -(bbox2.Width + spacing); - perpMax = bbox1.Width + bbox2.Width + spacing; + // Trim to offsets where the parts overlap by at least 50%. + var halfOverlap = bbox2.Width * 0.5; + perpMin = -(halfOverlap - spacing); + perpMax = bbox1.Width + halfOverlap + spacing; pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2; } else { // Perpendicular sweep along X → Length; push extent along Y → Width - perpMin = -(bbox2.Length + spacing); - perpMax = bbox1.Length + bbox2.Length + spacing; + var halfOverlap = bbox2.Length * 0.5; + perpMin = -(halfOverlap - spacing); + perpMax = bbox1.Length + halfOverlap + spacing; pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2; } diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs index 3d570a6..1a2a021 100644 --- a/OpenNest.Engine/DefaultNestEngine.cs +++ b/OpenNest.Engine/DefaultNestEngine.cs @@ -139,24 +139,42 @@ namespace OpenNest var bestFits = BestFitCache.GetOrCompute( drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing); - var best = SelectBestFitPair(bestFits); - if (best == null) - return null; + List bestPlacement = null; - // BuildParts produces landscape orientation (Width >= Height). - // Try both landscape and portrait (90° rotated) and let the - // engine's comparer pick the better orientation. - var landscape = best.BuildParts(drawing); - var portrait = RotatePair90(landscape); + foreach (var fit in bestFits) + { + if (!fit.Keep) + continue; - var lFits = TryOffsetToWorkArea(landscape, workArea); - var pFits = TryOffsetToWorkArea(portrait, workArea); + // Skip pairs that can't possibly fit the work area in either orientation. + if (fit.ShortestSide > System.Math.Min(workArea.Width, workArea.Length) + Tolerance.Epsilon) + continue; + if (fit.LongestSide > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon) + continue; - if (!lFits && !pFits) - return null; - if (lFits && pFits) - return IsBetterFill(portrait, landscape, workArea) ? portrait : landscape; - return lFits ? landscape : portrait; + var landscape = fit.BuildParts(drawing); + var portrait = RotatePair90(landscape); + + var lFits = TryOffsetToWorkArea(landscape, workArea); + var pFits = TryOffsetToWorkArea(portrait, workArea); + + // Pick the better orientation for this pair. + List candidate = null; + if (lFits && pFits) + candidate = IsBetterFill(portrait, landscape, workArea) ? portrait : landscape; + else if (lFits) + candidate = landscape; + else if (pFits) + candidate = portrait; + + if (candidate == null) + continue; + + if (bestPlacement == null || IsBetterFill(candidate, bestPlacement, workArea)) + bestPlacement = candidate; + } + + return bestPlacement; } private static List RotatePair90(List parts) diff --git a/OpenNest.Engine/HorizontalRemnantEngine.cs b/OpenNest.Engine/HorizontalRemnantEngine.cs index 28a34a6..16b31eb 100644 --- a/OpenNest.Engine/HorizontalRemnantEngine.cs +++ b/OpenNest.Engine/HorizontalRemnantEngine.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using OpenNest.Engine; -using OpenNest.Engine.BestFit; using OpenNest.Engine.Fill; using OpenNest.Geometry; using OpenNest.Math; @@ -27,20 +26,6 @@ namespace OpenNest public override ShrinkAxis TrimAxis => ShrinkAxis.Length; - protected override BestFitResult SelectBestFitPair(List results) - { - BestFitResult best = null; - - foreach (var r in results) - { - if (!r.Keep) continue; - if (best == null || r.BoundingHeight < best.BoundingHeight) - best = r; - } - - return best; - } - public override List BuildAngles(NestItem item, ClassificationResult classification, Box workArea) { var baseAngles = new List { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI }; diff --git a/OpenNest.Engine/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs index 226d21f..26b3966 100644 --- a/OpenNest.Engine/NestEngineBase.cs +++ b/OpenNest.Engine/NestEngineBase.cs @@ -56,11 +56,6 @@ namespace OpenNest protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection); - protected virtual BestFitResult SelectBestFitPair(List results) - { - return results.FirstOrDefault(r => r.Keep); - } - // --- Virtual methods (side-effect-free, return parts) --- public virtual List Fill(NestItem item, Box workArea, @@ -338,45 +333,56 @@ namespace OpenNest var bestFits = BestFitCache.GetOrCompute( item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing); - var bestFit = SelectBestFitPair(bestFits); - if (bestFit == null) continue; - var parts = bestFit.BuildParts(item.Drawing); - var pairBbox = ((IEnumerable)parts).GetBoundingBox(); - var pairW = pairBbox.Width; - var pairL = pairBbox.Length; - var minDim = System.Math.Min(pairW, pairL); + List bestPlacement = null; + Box bestTarget = null; - var remnants = finder.FindRemnants(minDim); - Box target = null; - - foreach (var r in remnants) + foreach (var fit in bestFits) { - if (pairW <= r.Width + Tolerance.Epsilon && - pairL <= r.Length + Tolerance.Epsilon) + if (!fit.Keep) + continue; + + var parts = fit.BuildParts(item.Drawing); + var pairBbox = ((IEnumerable)parts).GetBoundingBox(); + var pairW = pairBbox.Width; + var pairL = pairBbox.Length; + var minDim = System.Math.Min(pairW, pairL); + + var remnants = finder.FindRemnants(minDim); + + foreach (var r in remnants) { - target = r; - break; + if (pairW <= r.Width + Tolerance.Epsilon && + pairL <= r.Length + Tolerance.Epsilon) + { + var offset = r.Location - pairBbox.Location; + foreach (var p in parts) + { + p.Offset(offset); + p.UpdateBounds(); + } + + if (bestPlacement == null || IsBetterFill(parts, bestPlacement, r)) + { + bestPlacement = parts; + bestTarget = r; + } + break; + } } } - if (target == null) continue; + if (bestPlacement == null) continue; - var offset = target.Location - pairBbox.Location; - foreach (var p in parts) - { - p.Offset(offset); - p.UpdateBounds(); - } - - result.AddRange(parts); + result.AddRange(bestPlacement); item.Quantity = 0; - var envelope = ((IEnumerable)parts).GetBoundingBox(); + var envelope = ((IEnumerable)bestPlacement).GetBoundingBox(); finder.AddObstacle(envelope.Offset(Plate.PartSpacing)); Debug.WriteLine($"[Nest] Placed best-fit pair for {item.Drawing.Name} " + - $"at ({target.X:F1},{target.Y:F1}), size {pairW:F1}x{pairL:F1}"); + $"at ({bestTarget.X:F1},{bestTarget.Y:F1}), " + + $"size {envelope.Width:F1}x{envelope.Length:F1}"); } return result; diff --git a/OpenNest.Engine/PlateOptimizer.cs b/OpenNest.Engine/PlateOptimizer.cs index ed201bd..2af8553 100644 --- a/OpenNest.Engine/PlateOptimizer.cs +++ b/OpenNest.Engine/PlateOptimizer.cs @@ -1,4 +1,5 @@ using OpenNest.Engine; +using OpenNest.Engine.BestFit; using OpenNest.Geometry; using OpenNest.Math; using System; @@ -44,6 +45,19 @@ namespace OpenNest if (candidates.Count == 0) return null; + // Pre-compute best fits for all candidate plate sizes at once. + // This runs the expensive GPU evaluation once on the largest plate + // and filters the results for each smaller size. + var plateSizes = candidates + .Select(o => (Width: o.Length, Height: o.Width)) + .ToList(); + + foreach (var item in items) + { + if (item.Quantity <= 0) continue; + BestFitCache.ComputeForSizes(item.Drawing, templatePlate.PartSpacing, plateSizes); + } + PlateOptimizerResult best = null; foreach (var option in candidates) @@ -58,9 +72,10 @@ namespace OpenNest if (IsBetter(result, best)) best = result; - // Early exit: when salvage is zero, cheapest plate that fits everything wins. - // With salvage > 0, larger plates may have lower net cost, so keep searching. - if (salvageRate <= 0) + // Early exit: when all items fit, larger plates can only have + // worse utilization and higher cost. With salvage < 100%, the + // remnant credit never offsets the extra plate cost, so skip. + if (salvageRate < 1.0) { var allPlaced = items.All(i => i.Quantity <= 0 || result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) >= i.Quantity); diff --git a/OpenNest.Engine/VerticalRemnantEngine.cs b/OpenNest.Engine/VerticalRemnantEngine.cs index a398761..510f1bb 100644 --- a/OpenNest.Engine/VerticalRemnantEngine.cs +++ b/OpenNest.Engine/VerticalRemnantEngine.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using OpenNest.Engine; -using OpenNest.Engine.BestFit; using OpenNest.Engine.Fill; using OpenNest.Geometry; using OpenNest.Math; @@ -25,20 +24,6 @@ namespace OpenNest public override NestDirection? PreferredDirection => NestDirection.Horizontal; - protected override BestFitResult SelectBestFitPair(List results) - { - BestFitResult best = null; - - foreach (var r in results) - { - if (!r.Keep) continue; - if (best == null || r.BoundingHeight < best.BoundingHeight) - best = r; - } - - return best; - } - public override List BuildAngles(NestItem item, ClassificationResult classification, Box workArea) { var baseAngles = new List { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI }; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 9959dc6..61e4b05 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -64,8 +64,8 @@ namespace OpenNest.Forms //if (GpuEvaluatorFactory.GpuAvailable) // BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing); - if (GpuEvaluatorFactory.GpuAvailable) - BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer(); + //if (GpuEvaluatorFactory.GpuAvailable) + // BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer(); var enginesDir = Path.Combine(Application.StartupPath, "Engines"); NestEngineRegistry.LoadPlugins(enginesDir);