perf: optimize best fit computation and plate optimizer
- Try all valid best fit pairs instead of only the first when qty=2, picking the best via IsBetterFill comparer (fixes suboptimal plate selection during auto-nesting) - Pre-compute best fits across all plate sizes once via BestFitCache.ComputeForSizes instead of per-size GPU evaluation - Early exit plate optimizer when all items fit (salvage < 100%) - Trim slide offset sweep range to 50% overlap to reduce candidates - Use actual geometry (ray-arc/ray-circle intersection) instead of tessellated polygons for slide distance computation — eliminates the massive line count from circle/arc tessellation - Add RayArcDistance and RayCircleDistance to SpatialQuery - Add PartGeometry.GetOffsetPerimeterEntities for non-tessellated perimeter extraction - Disable GPU slide computer (slower than CPU currently) - Remove dead SelectBestFitPair virtual method and overrides Reduces best fit computation from 7+ minutes to ~4 seconds for a 73x25" part with 30+ holes on a 48x96 plate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,95 @@ namespace OpenNest.Geometry
|
|||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the distance from a point along a direction to a full circle.
|
||||||
|
/// Returns double.MaxValue if no hit.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the minimum translation distance along a push direction before
|
/// Computes the minimum translation distance along a push direction before
|
||||||
/// any edge of movingLines contacts any edge of stationaryLines.
|
/// any edge of movingLines contacts any edge of stationaryLines.
|
||||||
|
|||||||
@@ -39,7 +39,30 @@ namespace OpenNest
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001)
|
/// <summary>
|
||||||
|
/// Returns the perimeter entities (Line, Arc, Circle) with spacing offset applied,
|
||||||
|
/// without tessellation. Much faster than GetOffsetPartLines for parts with many arcs.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> 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<Entity>();
|
||||||
|
|
||||||
|
// 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<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001,
|
||||||
|
bool perimeterOnly = false)
|
||||||
{
|
{
|
||||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
var profile = new ShapeProfile(
|
var profile = new ShapeProfile(
|
||||||
@@ -50,9 +73,12 @@ namespace OpenNest
|
|||||||
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
chordTolerance, part.Location);
|
chordTolerance, part.Location);
|
||||||
|
|
||||||
foreach (var cutout in profile.Cutouts)
|
if (!perimeterOnly)
|
||||||
AddOffsetLines(lines, cutout.OffsetInward(totalSpacing),
|
{
|
||||||
chordTolerance, part.Location);
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
AddOffsetLines(lines, cutout.OffsetInward(totalSpacing),
|
||||||
|
chordTolerance, part.Location);
|
||||||
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using OpenNest.Geometry;
|
|||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
var allCandidates = candidateBags.SelectMany(c => c).ToList();
|
var allCandidates = candidateBags.SelectMany(c => c).ToList();
|
||||||
|
|
||||||
|
Debug.WriteLine($"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates");
|
||||||
|
|
||||||
var results = _evaluator.EvaluateAll(allCandidates);
|
var results = _evaluator.EvaluateAll(allCandidates);
|
||||||
|
|
||||||
_filter.Apply(results);
|
_filter.Apply(results);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ namespace OpenNest.Engine.BestFit
|
|||||||
var allMovingVerts = ExtractUniqueVertices(movingTemplateLines);
|
var allMovingVerts = ExtractUniqueVertices(movingTemplateLines);
|
||||||
var allStationaryVerts = ExtractUniqueVertices(stationaryLines);
|
var allStationaryVerts = ExtractUniqueVertices(stationaryLines);
|
||||||
|
|
||||||
// Pre-filter vertices per unique direction (typically 4 cardinal directions).
|
|
||||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||||
|
|
||||||
foreach (var offset in offsets)
|
foreach (var offset in offsets)
|
||||||
@@ -43,7 +43,6 @@ namespace OpenNest.Engine.BestFit
|
|||||||
|
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
// Case 1: Leading moving vertices → stationary edges
|
|
||||||
for (var v = 0; v < leadingMoving.Length; v++)
|
for (var v = 0; v < leadingMoving.Length; v++)
|
||||||
{
|
{
|
||||||
var vx = leadingMoving[v].X + offset.Dx;
|
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++)
|
for (var v = 0; v < facingStationary.Length; v++)
|
||||||
{
|
{
|
||||||
var svx = facingStationary[v].X;
|
var svx = facingStationary[v].X;
|
||||||
@@ -95,6 +93,178 @@ namespace OpenNest.Engine.BestFit
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double[] ComputeDistances(
|
||||||
|
List<Entity> stationaryEntities,
|
||||||
|
List<Entity> 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<Entity> entities)
|
||||||
|
{
|
||||||
|
var vertices = new HashSet<Vector>();
|
||||||
|
|
||||||
|
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<Vector> 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<Line> lines)
|
private static Vector[] ExtractUniqueVertices(List<Line> lines)
|
||||||
{
|
{
|
||||||
var vertices = new HashSet<Vector>();
|
var vertices = new HashSet<Vector>();
|
||||||
@@ -106,11 +276,6 @@ namespace OpenNest.Engine.BestFit
|
|||||||
return vertices.ToArray();
|
return vertices.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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).
|
|
||||||
/// </summary>
|
|
||||||
private static Vector[] FilterVerticesByProjection(
|
private static Vector[] FilterVerticesByProjection(
|
||||||
Vector[] vertices, double dirX, double dirY, bool keepHigh)
|
Vector[] vertices, double dirX, double dirY, bool keepHigh)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ namespace OpenNest.Engine.BestFit
|
|||||||
flatOffsets, count, directions);
|
flatOffsets, count, directions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double[] ComputeDistances(
|
||||||
|
List<Entity> stationaryEntities,
|
||||||
|
List<Entity> 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps a unit direction vector to a PushDirection int for the GPU interface.
|
/// Maps a unit direction vector to a PushDirection int for the GPU interface.
|
||||||
/// Left=0, Down=1, Right=2, Up=3.
|
/// Left=0, Down=1, Right=2, Up=3.
|
||||||
|
|||||||
@@ -9,5 +9,10 @@ namespace OpenNest.Engine.BestFit
|
|||||||
List<Line> stationaryLines,
|
List<Line> stationaryLines,
|
||||||
List<Line> movingTemplateLines,
|
List<Line> movingTemplateLines,
|
||||||
SlideOffset[] offsets);
|
SlideOffset[] offsets);
|
||||||
|
|
||||||
|
double[] ComputeDistances(
|
||||||
|
List<Entity> stationaryEntities,
|
||||||
|
List<Entity> movingEntities,
|
||||||
|
SlideOffset[] offsets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ namespace OpenNest.Engine.BestFit
|
|||||||
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
||||||
|
|
||||||
var halfSpacing = spacing / 2;
|
var halfSpacing = spacing / 2;
|
||||||
var part1Lines = PartGeometry.GetOffsetPartLines(part1, halfSpacing);
|
var part1Entities = PartGeometry.GetOffsetPerimeterEntities(part1, halfSpacing);
|
||||||
var part2TemplateLines = PartGeometry.GetOffsetPartLines(part2Template, halfSpacing);
|
var part2Entities = PartGeometry.GetOffsetPerimeterEntities(part2Template, halfSpacing);
|
||||||
|
|
||||||
var bbox1 = part1.BoundingBox;
|
var bbox1 = part1.BoundingBox;
|
||||||
var bbox2 = part2Template.BoundingBox;
|
var bbox2 = part2Template.BoundingBox;
|
||||||
@@ -48,7 +48,7 @@ namespace OpenNest.Engine.BestFit
|
|||||||
return candidates;
|
return candidates;
|
||||||
|
|
||||||
var distances = _distanceComputer.ComputeDistances(
|
var distances = _distanceComputer.ComputeDistances(
|
||||||
part1Lines, part2TemplateLines, offsets);
|
part1Entities, part2Entities, offsets);
|
||||||
|
|
||||||
var testNumber = 0;
|
var testNumber = 0;
|
||||||
|
|
||||||
@@ -90,15 +90,18 @@ namespace OpenNest.Engine.BestFit
|
|||||||
if (isHorizontalPush)
|
if (isHorizontalPush)
|
||||||
{
|
{
|
||||||
// Perpendicular sweep along Y → Width; push extent along X → Length
|
// Perpendicular sweep along Y → Width; push extent along X → Length
|
||||||
perpMin = -(bbox2.Width + spacing);
|
// Trim to offsets where the parts overlap by at least 50%.
|
||||||
perpMax = bbox1.Width + bbox2.Width + spacing;
|
var halfOverlap = bbox2.Width * 0.5;
|
||||||
|
perpMin = -(halfOverlap - spacing);
|
||||||
|
perpMax = bbox1.Width + halfOverlap + spacing;
|
||||||
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Perpendicular sweep along X → Length; push extent along Y → Width
|
// Perpendicular sweep along X → Length; push extent along Y → Width
|
||||||
perpMin = -(bbox2.Length + spacing);
|
var halfOverlap = bbox2.Length * 0.5;
|
||||||
perpMax = bbox1.Length + bbox2.Length + spacing;
|
perpMin = -(halfOverlap - spacing);
|
||||||
|
perpMax = bbox1.Length + halfOverlap + spacing;
|
||||||
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,24 +139,42 @@ namespace OpenNest
|
|||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||||
|
|
||||||
var best = SelectBestFitPair(bestFits);
|
List<Part> bestPlacement = null;
|
||||||
if (best == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// BuildParts produces landscape orientation (Width >= Height).
|
foreach (var fit in bestFits)
|
||||||
// Try both landscape and portrait (90° rotated) and let the
|
{
|
||||||
// engine's comparer pick the better orientation.
|
if (!fit.Keep)
|
||||||
var landscape = best.BuildParts(drawing);
|
continue;
|
||||||
var portrait = RotatePair90(landscape);
|
|
||||||
|
|
||||||
var lFits = TryOffsetToWorkArea(landscape, workArea);
|
// Skip pairs that can't possibly fit the work area in either orientation.
|
||||||
var pFits = TryOffsetToWorkArea(portrait, workArea);
|
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)
|
var landscape = fit.BuildParts(drawing);
|
||||||
return null;
|
var portrait = RotatePair90(landscape);
|
||||||
if (lFits && pFits)
|
|
||||||
return IsBetterFill(portrait, landscape, workArea) ? portrait : landscape;
|
var lFits = TryOffsetToWorkArea(landscape, workArea);
|
||||||
return lFits ? landscape : portrait;
|
var pFits = TryOffsetToWorkArea(portrait, workArea);
|
||||||
|
|
||||||
|
// Pick the better orientation for this pair.
|
||||||
|
List<Part> 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<Part> RotatePair90(List<Part> parts)
|
private static List<Part> RotatePair90(List<Part> parts)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.BestFit;
|
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
@@ -27,20 +26,6 @@ namespace OpenNest
|
|||||||
|
|
||||||
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
|
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
|
||||||
|
|
||||||
protected override BestFitResult SelectBestFitPair(List<BestFitResult> 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<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||||
{
|
{
|
||||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||||
|
|||||||
@@ -56,11 +56,6 @@ namespace OpenNest
|
|||||||
|
|
||||||
protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection);
|
protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection);
|
||||||
|
|
||||||
protected virtual BestFitResult SelectBestFitPair(List<BestFitResult> results)
|
|
||||||
{
|
|
||||||
return results.FirstOrDefault(r => r.Keep);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Virtual methods (side-effect-free, return parts) ---
|
// --- Virtual methods (side-effect-free, return parts) ---
|
||||||
|
|
||||||
public virtual List<Part> Fill(NestItem item, Box workArea,
|
public virtual List<Part> Fill(NestItem item, Box workArea,
|
||||||
@@ -338,45 +333,56 @@ namespace OpenNest
|
|||||||
|
|
||||||
var bestFits = BestFitCache.GetOrCompute(
|
var bestFits = BestFitCache.GetOrCompute(
|
||||||
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
|
||||||
var bestFit = SelectBestFitPair(bestFits);
|
|
||||||
if (bestFit == null) continue;
|
|
||||||
|
|
||||||
var parts = bestFit.BuildParts(item.Drawing);
|
List<Part> bestPlacement = null;
|
||||||
var pairBbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
Box bestTarget = null;
|
||||||
var pairW = pairBbox.Width;
|
|
||||||
var pairL = pairBbox.Length;
|
|
||||||
var minDim = System.Math.Min(pairW, pairL);
|
|
||||||
|
|
||||||
var remnants = finder.FindRemnants(minDim);
|
foreach (var fit in bestFits)
|
||||||
Box target = null;
|
|
||||||
|
|
||||||
foreach (var r in remnants)
|
|
||||||
{
|
{
|
||||||
if (pairW <= r.Width + Tolerance.Epsilon &&
|
if (!fit.Keep)
|
||||||
pairL <= r.Length + Tolerance.Epsilon)
|
continue;
|
||||||
|
|
||||||
|
var parts = fit.BuildParts(item.Drawing);
|
||||||
|
var pairBbox = ((IEnumerable<IBoundable>)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;
|
if (pairW <= r.Width + Tolerance.Epsilon &&
|
||||||
break;
|
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;
|
result.AddRange(bestPlacement);
|
||||||
foreach (var p in parts)
|
|
||||||
{
|
|
||||||
p.Offset(offset);
|
|
||||||
p.UpdateBounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
result.AddRange(parts);
|
|
||||||
item.Quantity = 0;
|
item.Quantity = 0;
|
||||||
|
|
||||||
var envelope = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
|
var envelope = ((IEnumerable<IBoundable>)bestPlacement).GetBoundingBox();
|
||||||
finder.AddObstacle(envelope.Offset(Plate.PartSpacing));
|
finder.AddObstacle(envelope.Offset(Plate.PartSpacing));
|
||||||
|
|
||||||
Debug.WriteLine($"[Nest] Placed best-fit pair for {item.Drawing.Name} " +
|
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;
|
return result;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
using System;
|
using System;
|
||||||
@@ -44,6 +45,19 @@ namespace OpenNest
|
|||||||
if (candidates.Count == 0)
|
if (candidates.Count == 0)
|
||||||
return null;
|
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;
|
PlateOptimizerResult best = null;
|
||||||
|
|
||||||
foreach (var option in candidates)
|
foreach (var option in candidates)
|
||||||
@@ -58,9 +72,10 @@ namespace OpenNest
|
|||||||
if (IsBetter(result, best))
|
if (IsBetter(result, best))
|
||||||
best = result;
|
best = result;
|
||||||
|
|
||||||
// Early exit: when salvage is zero, cheapest plate that fits everything wins.
|
// Early exit: when all items fit, larger plates can only have
|
||||||
// With salvage > 0, larger plates may have lower net cost, so keep searching.
|
// worse utilization and higher cost. With salvage < 100%, the
|
||||||
if (salvageRate <= 0)
|
// remnant credit never offsets the extra plate cost, so skip.
|
||||||
|
if (salvageRate < 1.0)
|
||||||
{
|
{
|
||||||
var allPlaced = items.All(i => i.Quantity <= 0 ||
|
var allPlaced = items.All(i => i.Quantity <= 0 ||
|
||||||
result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) >= i.Quantity);
|
result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) >= i.Quantity);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
using OpenNest.Engine.BestFit;
|
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
@@ -25,20 +24,6 @@ namespace OpenNest
|
|||||||
|
|
||||||
public override NestDirection? PreferredDirection => NestDirection.Horizontal;
|
public override NestDirection? PreferredDirection => NestDirection.Horizontal;
|
||||||
|
|
||||||
protected override BestFitResult SelectBestFitPair(List<BestFitResult> 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<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
|
||||||
{
|
{
|
||||||
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ namespace OpenNest.Forms
|
|||||||
//if (GpuEvaluatorFactory.GpuAvailable)
|
//if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
|
||||||
|
|
||||||
if (GpuEvaluatorFactory.GpuAvailable)
|
//if (GpuEvaluatorFactory.GpuAvailable)
|
||||||
BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
// BestFitCache.CreateSlideComputer = () => GpuEvaluatorFactory.CreateSlideComputer();
|
||||||
|
|
||||||
var enginesDir = Path.Combine(Application.StartupPath, "Engines");
|
var enginesDir = Path.Combine(Application.StartupPath, "Engines");
|
||||||
NestEngineRegistry.LoadPlugins(enginesDir);
|
NestEngineRegistry.LoadPlugins(enginesDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user