perf: optimize fill hot path — bbox pre-check and geometry inner loop

- Add bounding box rejection in HasOverlaps to skip expensive
  Part.Intersects (CNC→geometry conversion) for non-adjacent parts.
  Eliminates ~35% CPU in IsBetterValidFill for grid layouts.
- Optimize RayEdgeDistance: access Line fields directly instead of
  property getters (avoids Vector struct copies), inline IsEqualTo
  with direct range comparison (avoids Math.Abs), and precompute
  deltas for reuse in interpolation.
- Cache line endpoints in DirectionalDistance outer loop to avoid
  repeated struct copies in the inner loop.
- Add fill timer to ActionClone.Fill, displayed in PlateView status
  bar as "Fill: N parts in M ms".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 22:10:25 -04:00
parent 35d7248da0
commit 91908c1732
5 changed files with 50 additions and 23 deletions

View File

@@ -6,8 +6,8 @@ namespace OpenNest.Geometry
{
public class Line : Entity
{
private Vector pt1;
private Vector pt2;
internal Vector pt1;
internal Vector pt2;
public Line()
{

View File

@@ -861,22 +861,25 @@ namespace OpenNest
/// </summary>
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
{
var p1 = edge.StartPoint;
var p2 = edge.EndPoint;
var p1x = edge.pt1.X;
var p1y = edge.pt1.Y;
var p2x = edge.pt2.X;
var p2y = edge.pt2.Y;
switch (direction)
{
case PushDirection.Left:
{
// Ray goes in -X direction. Need non-horizontal edge.
if (p1.Y.IsEqualTo(p2.Y))
var dy = p2y - p1y;
if (dy > -Tolerance.Epsilon && dy < Tolerance.Epsilon)
return double.MaxValue; // horizontal edge, parallel to ray
var t = (vertex.Y - p1.Y) / (p2.Y - p1.Y);
var t = (vertex.Y - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1.X + t * (p2.X - p1.X);
var ix = p1x + t * (p2x - p1x);
var dist = vertex.X - ix; // positive if edge is to the left
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0; // touching
@@ -885,14 +888,15 @@ namespace OpenNest
case PushDirection.Right:
{
if (p1.Y.IsEqualTo(p2.Y))
var dy = p2y - p1y;
if (dy > -Tolerance.Epsilon && dy < Tolerance.Epsilon)
return double.MaxValue;
var t = (vertex.Y - p1.Y) / (p2.Y - p1.Y);
var t = (vertex.Y - p1y) / dy;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var ix = p1.X + t * (p2.X - p1.X);
var ix = p1x + t * (p2x - p1x);
var dist = ix - vertex.X;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0; // touching
@@ -902,14 +906,15 @@ namespace OpenNest
case PushDirection.Down:
{
// Ray goes in -Y direction. Need non-vertical edge.
if (p1.X.IsEqualTo(p2.X))
var dx = p2x - p1x;
if (dx > -Tolerance.Epsilon && dx < Tolerance.Epsilon)
return double.MaxValue; // vertical edge, parallel to ray
var t = (vertex.X - p1.X) / (p2.X - p1.X);
var t = (vertex.X - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1.Y + t * (p2.Y - p1.Y);
var iy = p1y + t * (p2y - p1y);
var dist = vertex.Y - iy;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0; // touching
@@ -918,14 +923,15 @@ namespace OpenNest
case PushDirection.Up:
{
if (p1.X.IsEqualTo(p2.X))
var dx = p2x - p1x;
if (dx > -Tolerance.Epsilon && dx < Tolerance.Epsilon)
return double.MaxValue;
var t = (vertex.X - p1.X) / (p2.X - p1.X);
var t = (vertex.X - p1x) / dx;
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
return double.MaxValue;
var iy = p1.Y + t * (p2.Y - p1.Y);
var iy = p1y + t * (p2y - p1y);
var dist = iy - vertex.Y;
if (dist > Tolerance.Epsilon) return dist;
if (dist >= -Tolerance.Epsilon) return 0; // touching
@@ -949,14 +955,15 @@ namespace OpenNest
// Case 1: Each moving vertex → each stationary edge
for (int i = 0; i < movingLines.Count; i++)
{
var movingLine = movingLines[i];
var movingStart = movingLines[i].pt1;
var movingEnd = movingLines[i].pt2;
for (int j = 0; j < stationaryLines.Count; j++)
{
var d = RayEdgeDistance(movingLine.StartPoint, stationaryLines[j], direction);
var d = RayEdgeDistance(movingStart, stationaryLines[j], direction);
if (d < minDist) minDist = d;
d = RayEdgeDistance(movingLine.EndPoint, stationaryLines[j], direction);
d = RayEdgeDistance(movingEnd, stationaryLines[j], direction);
if (d < minDist) minDist = d;
}
}
@@ -966,14 +973,15 @@ namespace OpenNest
for (int i = 0; i < stationaryLines.Count; i++)
{
var stationaryLine = stationaryLines[i];
var stationaryStart = stationaryLines[i].pt1;
var stationaryEnd = stationaryLines[i].pt2;
for (int j = 0; j < movingLines.Count; j++)
{
var d = RayEdgeDistance(stationaryLine.StartPoint, movingLines[j], opposite);
var d = RayEdgeDistance(stationaryStart, movingLines[j], opposite);
if (d < minDist) minDist = d;
d = RayEdgeDistance(stationaryLine.EndPoint, movingLines[j], opposite);
d = RayEdgeDistance(stationaryEnd, movingLines[j], opposite);
if (d < minDist) minDist = d;
}
}

View File

@@ -273,8 +273,19 @@ namespace OpenNest
for (var i = 0; i < parts.Count; i++)
{
var box1 = parts[i].BoundingBox;
for (var j = i + 1; j < parts.Count; j++)
{
var box2 = parts[j].BoundingBox;
// Fast bounding box rejection — if boxes don't overlap,
// the parts can't intersect. Eliminates nearly all pairs
// in grid layouts.
if (box1.Right < box2.Left || box2.Right < box1.Left ||
box1.Top < box2.Bottom || box2.Top < box1.Bottom)
continue;
List<Vector> pts;
if (parts[i].Intersects(parts[j], out pts))

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Controls;
@@ -170,6 +171,8 @@ namespace OpenNest.Actions
private void Fill()
{
var sw = Stopwatch.StartNew();
var plate = plateView.Plate;
var engine = new NestEngine(plate);
var groupParts = parts.Select(p => p.BasePart).ToList();
@@ -179,6 +182,8 @@ namespace OpenNest.Actions
if (plate.Parts.Count == 0)
{
engine.Fill(groupParts);
sw.Stop();
plateView.Status = $"Fill: {plate.Parts.Count} parts in {sw.ElapsedMilliseconds} ms";
return;
}
@@ -197,7 +202,10 @@ namespace OpenNest.Actions
if (bestArea == Box.Empty)
return;
var before = plate.Parts.Count;
engine.Fill(groupParts, bestArea);
sw.Stop();
plateView.Status = $"Fill: {plate.Parts.Count - before} parts in {sw.ElapsedMilliseconds} ms";
}
}
}

View File

@@ -139,7 +139,7 @@ namespace OpenNest.Controls
public string Status
{
get { return status; }
protected set
set
{
status = value;