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