refactor: use IDistanceComputer and rename Type to StrategyIndex

Wire IDistanceComputer into RotationSlideStrategy, replacing inline
CPU/GPU branching. BestFitFinder constructs the appropriate implementation.
Replace PushDirection enum with direction vectors in BuildOffsets.
Rename IBestFitStrategy.Type and PairCandidate.StrategyType to StrategyIndex
for clarity (JSON field name unchanged for backward compatibility).

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

View File

@@ -1,29 +1,31 @@
using OpenNest.Geometry;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Engine.BestFit
{
public class RotationSlideStrategy : IBestFitStrategy
{
private readonly ISlideComputer _slideComputer;
private readonly IDistanceComputer _distanceComputer;
private static readonly PushDirection[] AllDirections =
private static readonly (double DirX, double DirY)[] PushDirections =
{
PushDirection.Left, PushDirection.Down, PushDirection.Right, PushDirection.Up
(-1, 0), // Left
(0, -1), // Down
(1, 0), // Right
(0, 1) // Up
};
public RotationSlideStrategy(double part2Rotation, int type, string description,
ISlideComputer slideComputer = null)
public RotationSlideStrategy(double part2Rotation, int strategyIndex, string description,
IDistanceComputer distanceComputer)
{
Part2Rotation = part2Rotation;
Type = type;
StrategyIndex = strategyIndex;
Description = description;
_slideComputer = slideComputer;
_distanceComputer = distanceComputer;
}
public double Part2Rotation { get; }
public int Type { get; }
public int StrategyIndex { get; }
public string Description { get; }
public List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize)
@@ -40,36 +42,25 @@ namespace OpenNest.Engine.BestFit
var bbox1 = part1.BoundingBox;
var bbox2 = part2Template.BoundingBox;
// Collect offsets and directions across all 4 axes
var allDx = new List<double>();
var allDy = new List<double>();
var allDirs = new List<PushDirection>();
var offsets = BuildOffsets(bbox1, bbox2, spacing, stepSize);
foreach (var pushDir in AllDirections)
BuildOffsets(bbox1, bbox2, spacing, stepSize, pushDir, allDx, allDy, allDirs);
if (allDx.Count == 0)
if (offsets.Length == 0)
return candidates;
// Compute all distances — single GPU dispatch or CPU loop
var distances = ComputeAllDistances(
part1Lines, part2TemplateLines, allDx, allDy, allDirs);
var distances = _distanceComputer.ComputeDistances(
part1Lines, part2TemplateLines, offsets);
// Create candidates from valid results
var testNumber = 0;
for (var i = 0; i < allDx.Count; i++)
for (var i = 0; i < offsets.Length; i++)
{
var slideDist = distances[i];
if (slideDist >= double.MaxValue || slideDist < 0)
continue;
var dx = allDx[i];
var dy = allDy[i];
var pushVector = GetPushVector(allDirs[i], slideDist);
var finalPosition = new Vector(
part2Template.Location.X + dx + pushVector.X,
part2Template.Location.Y + dy + pushVector.Y);
part2Template.Location.X + offsets[i].Dx + offsets[i].DirX * slideDist,
part2Template.Location.Y + offsets[i].Dy + offsets[i].DirY * slideDist);
candidates.Add(new PairCandidate
{
@@ -77,7 +68,7 @@ namespace OpenNest.Engine.BestFit
Part1Rotation = 0,
Part2Rotation = Part2Rotation,
Part2Offset = finalPosition,
StrategyType = Type,
StrategyIndex = StrategyIndex,
TestNumber = testNumber++,
Spacing = spacing
});
@@ -86,158 +77,44 @@ namespace OpenNest.Engine.BestFit
return candidates;
}
private static void BuildOffsets(
Box bbox1, Box bbox2, double spacing, double stepSize,
PushDirection pushDir, List<double> allDx, List<double> allDy,
List<PushDirection> allDirs)
private static SlideOffset[] BuildOffsets(Box bbox1, Box bbox2, double spacing, double stepSize)
{
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
var offsets = new List<SlideOffset>();
double perpMin, perpMax, pushStartOffset;
if (isHorizontalPush)
foreach (var (dirX, dirY) in PushDirections)
{
perpMin = -(bbox2.Length + spacing);
perpMax = bbox1.Length + bbox2.Length + spacing;
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
}
else
{
perpMin = -(bbox2.Width + spacing);
perpMax = bbox1.Width + bbox2.Width + spacing;
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
}
var isHorizontalPush = System.Math.Abs(dirX) > System.Math.Abs(dirY);
var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize;
var isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
double perpMin, perpMax, pushStartOffset;
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
{
allDx.Add(isHorizontalPush ? startPos : offset);
allDy.Add(isHorizontalPush ? offset : startPos);
allDirs.Add(pushDir);
}
}
private double[] ComputeAllDistances(
List<Line> part1Lines, List<Line> part2TemplateLines,
List<double> allDx, List<double> allDy, List<PushDirection> allDirs)
{
var count = allDx.Count;
if (_slideComputer != null)
{
var stationarySegments = SpatialQuery.FlattenLines(part1Lines);
var movingSegments = SpatialQuery.FlattenLines(part2TemplateLines);
var offsets = new double[count * 2];
var directions = new int[count];
for (var i = 0; i < count; i++)
if (isHorizontalPush)
{
offsets[i * 2] = allDx[i];
offsets[i * 2 + 1] = allDy[i];
directions[i] = (int)allDirs[i];
perpMin = -(bbox2.Length + spacing);
perpMax = bbox1.Length + bbox2.Length + spacing;
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
}
return _slideComputer.ComputeBatchMultiDir(
stationarySegments, part1Lines.Count,
movingSegments, part2TemplateLines.Count,
offsets, count, directions);
}
var results = new double[count];
// Pre-calculate moving vertices in local space.
var movingVerticesLocal = new HashSet<Vector>();
for (var i = 0; i < part2TemplateLines.Count; i++)
{
movingVerticesLocal.Add(part2TemplateLines[i].StartPoint);
movingVerticesLocal.Add(part2TemplateLines[i].EndPoint);
}
var movingVerticesArray = movingVerticesLocal.ToArray();
// Pre-calculate stationary vertices in local space.
var stationaryVerticesLocal = new HashSet<Vector>();
for (var i = 0; i < part1Lines.Count; i++)
{
stationaryVerticesLocal.Add(part1Lines[i].StartPoint);
stationaryVerticesLocal.Add(part1Lines[i].EndPoint);
}
var stationaryVerticesArray = stationaryVerticesLocal.ToArray();
// Pre-sort stationary and moving edges for all 4 directions.
var stationaryEdgesByDir = new Dictionary<PushDirection, (Vector start, Vector end)[]>();
var movingEdgesByDir = new Dictionary<PushDirection, (Vector start, Vector end)[]>();
foreach (var dir in AllDirections)
{
var sEdges = new (Vector start, Vector end)[part1Lines.Count];
for (var i = 0; i < part1Lines.Count; i++)
sEdges[i] = (part1Lines[i].StartPoint, part1Lines[i].EndPoint);
if (dir == PushDirection.Left || dir == PushDirection.Right)
sEdges = sEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
else
sEdges = sEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
stationaryEdgesByDir[dir] = sEdges;
var opposite = SpatialQuery.OppositeDirection(dir);
var mEdges = new (Vector start, Vector end)[part2TemplateLines.Count];
for (var i = 0; i < part2TemplateLines.Count; i++)
mEdges[i] = (part2TemplateLines[i].StartPoint, part2TemplateLines[i].EndPoint);
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
mEdges = mEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
else
mEdges = mEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
movingEdgesByDir[dir] = mEdges;
}
// Use Parallel.For for the heavy lifting.
System.Threading.Tasks.Parallel.For(0, count, i =>
{
var dx = allDx[i];
var dy = allDy[i];
var dir = allDirs[i];
var movingOffset = new Vector(dx, dy);
var sEdges = stationaryEdgesByDir[dir];
var mEdges = movingEdgesByDir[dir];
var opposite = SpatialQuery.OppositeDirection(dir);
var minDist = double.MaxValue;
// Case 1: Moving vertices -> Stationary edges
foreach (var mv in movingVerticesArray)
{
var d = SpatialQuery.OneWayDistance(mv + movingOffset, sEdges, Vector.Zero, dir);
if (d < minDist) minDist = d;
perpMin = -(bbox2.Width + spacing);
perpMax = bbox1.Width + bbox2.Width + spacing;
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
}
// Case 2: Stationary vertices -> Moving edges (translated)
foreach (var sv in stationaryVerticesArray)
var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize;
// Start on the opposite side of the push direction.
var pushComponent = isHorizontalPush ? dirX : dirY;
var startPos = pushComponent < 0 ? pushStartOffset : -pushStartOffset;
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
{
var d = SpatialQuery.OneWayDistance(sv, mEdges, movingOffset, opposite);
if (d < minDist) minDist = d;
var dx = isHorizontalPush ? startPos : offset;
var dy = isHorizontalPush ? offset : startPos;
offsets.Add(new SlideOffset(dx, dy, dirX, dirY));
}
results[i] = minDist;
});
return results;
}
private static Vector GetPushVector(PushDirection direction, double distance)
{
switch (direction)
{
case PushDirection.Left: return new Vector(-distance, 0);
case PushDirection.Right: return new Vector(distance, 0);
case PushDirection.Down: return new Vector(0, -distance);
case PushDirection.Up: return new Vector(0, distance);
default: return Vector.Zero;
}
return offsets.ToArray();
}
}
}