refactor: extract IDistanceComputer with CPU and GPU implementations

Extract distance computation from RotationSlideStrategy into a pluggable
IDistanceComputer interface. CpuDistanceComputer adds leading-face vertex
culling (~50% fewer rays per direction) with early exit on overlap.
GpuDistanceComputer wraps ISlideComputer with Line-to-flat-array conversion.
SlideOffset struct uses direction vectors (DirX/DirY) instead of PushDirection.
SpatialQuery.RayEdgeDistance(dirX,dirY) made public for CPU path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 00:04:12 -04:00
parent 7f96d632f3
commit 4f21fb91a1
5 changed files with 235 additions and 1 deletions

View File

@@ -76,7 +76,7 @@ namespace OpenNest.Geometry
/// </summary> /// </summary>
[System.Runtime.CompilerServices.MethodImpl( [System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private static double RayEdgeDistance( public static double RayEdgeDistance(
double vx, double vy, double vx, double vy,
double p1x, double p1y, double p2x, double p2y, double p1x, double p1y, double p2x, double p2y,
double dirX, double dirY) double dirX, double dirY)

View File

@@ -0,0 +1,152 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine.BestFit
{
public class CpuDistanceComputer : IDistanceComputer
{
public double[] ComputeDistances(
List<Line> stationaryLines,
List<Line> movingTemplateLines,
SlideOffset[] offsets)
{
var count = offsets.Length;
var results = new double[count];
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)
{
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 edges
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 < stationaryLines.Count; j++)
{
var e = stationaryLines[j];
var d = SpatialQuery.RayEdgeDistance(
vx, vy,
e.StartPoint.X, e.StartPoint.Y,
e.EndPoint.X, e.EndPoint.Y,
dirX, dirY);
if (d < minDist)
{
minDist = d;
if (d <= 0) { results[i] = 0; return; }
}
}
}
// Case 2: Facing stationary vertices → moving edges (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 < movingTemplateLines.Count; j++)
{
var e = movingTemplateLines[j];
var d = SpatialQuery.RayEdgeDistance(
svx, svy,
e.StartPoint.X + offset.Dx, e.StartPoint.Y + offset.Dy,
e.EndPoint.X + offset.Dx, e.EndPoint.Y + offset.Dy,
oppX, oppY);
if (d < minDist)
{
minDist = d;
if (d <= 0) { results[i] = 0; return; }
}
}
}
results[i] = minDist;
});
return results;
}
private static Vector[] ExtractUniqueVertices(List<Line> lines)
{
var vertices = new HashSet<Vector>();
for (var i = 0; i < lines.Count; i++)
{
vertices.Add(lines[i].StartPoint);
vertices.Add(lines[i].EndPoint);
}
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(
Vector[] vertices, double dirX, double dirY, bool keepHigh)
{
if (vertices.Length == 0)
return vertices;
var projections = new double[vertices.Length];
var min = double.MaxValue;
var max = double.MinValue;
for (var i = 0; i < vertices.Length; i++)
{
projections[i] = vertices[i].X * dirX + vertices[i].Y * dirY;
if (projections[i] < min) min = projections[i];
if (projections[i] > max) max = projections[i];
}
var midpoint = (min + max) / 2;
var count = 0;
for (var i = 0; i < vertices.Length; i++)
{
if (keepHigh ? projections[i] >= midpoint : projections[i] <= midpoint)
count++;
}
var result = new Vector[count];
var idx = 0;
for (var i = 0; i < vertices.Length; i++)
{
if (keepHigh ? projections[i] >= midpoint : projections[i] <= midpoint)
result[idx++] = vertices[i];
}
return result;
}
}
}

View File

@@ -0,0 +1,51 @@
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit
{
public class GpuDistanceComputer : IDistanceComputer
{
private readonly ISlideComputer _slideComputer;
public GpuDistanceComputer(ISlideComputer slideComputer)
{
_slideComputer = slideComputer;
}
public double[] ComputeDistances(
List<Line> stationaryLines,
List<Line> movingTemplateLines,
SlideOffset[] offsets)
{
var stationarySegments = SpatialQuery.FlattenLines(stationaryLines);
var movingSegments = SpatialQuery.FlattenLines(movingTemplateLines);
var count = offsets.Length;
var flatOffsets = new double[count * 2];
var directions = new int[count];
for (var i = 0; i < count; i++)
{
flatOffsets[i * 2] = offsets[i].Dx;
flatOffsets[i * 2 + 1] = offsets[i].Dy;
directions[i] = DirectionVectorToInt(offsets[i].DirX, offsets[i].DirY);
}
return _slideComputer.ComputeBatchMultiDir(
stationarySegments, stationaryLines.Count,
movingSegments, movingTemplateLines.Count,
flatOffsets, count, directions);
}
/// <summary>
/// Maps a unit direction vector to a PushDirection int for the GPU interface.
/// Left=0, Down=1, Right=2, Up=3.
/// </summary>
private static int DirectionVectorToInt(double dirX, double dirY)
{
if (dirX < -0.5) return (int)PushDirection.Left;
if (dirX > 0.5) return (int)PushDirection.Right;
if (dirY < -0.5) return (int)PushDirection.Down;
return (int)PushDirection.Up;
}
}
}

View File

@@ -0,0 +1,13 @@
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit
{
public interface IDistanceComputer
{
double[] ComputeDistances(
List<Line> stationaryLines,
List<Line> movingTemplateLines,
SlideOffset[] offsets);
}
}

View File

@@ -0,0 +1,18 @@
namespace OpenNest.Engine.BestFit
{
public readonly struct SlideOffset
{
public double Dx { get; }
public double Dy { get; }
public double DirX { get; }
public double DirY { get; }
public SlideOffset(double dx, double dy, double dirX, double dirY)
{
Dx = dx;
Dy = dy;
DirX = dirX;
DirY = dirY;
}
}
}