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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Computes the minimum translation distance along a push direction before
|
||||
/// any edge of movingLines contacts any edge of stationaryLines.
|
||||
|
||||
@@ -39,7 +39,30 @@ namespace OpenNest
|
||||
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 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user