Add PushDirection.Right and PushDirection.Up to RotationSlideStrategy so parts can approach from all four directions. This discovers concave interlocking arrangements (e.g. L-shaped parts nesting into each other's cavities) that the original Left/Down-only slides could never reach. Introduce BestFitCache so best-fit results are computed once at step size 0.25 and shared between the viewer and nesting engine. The GPU evaluator factory is configured once at startup instead of being wired per call site, and NestEngine.CreateEvaluator is removed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
139 lines
5.5 KiB
C#
139 lines
5.5 KiB
C#
using System.Collections.Generic;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.BestFit
|
|
{
|
|
public class RotationSlideStrategy : IBestFitStrategy
|
|
{
|
|
public RotationSlideStrategy(double part2Rotation, int type, string description)
|
|
{
|
|
Part2Rotation = part2Rotation;
|
|
Type = type;
|
|
Description = description;
|
|
}
|
|
|
|
public double Part2Rotation { get; }
|
|
public int Type { get; }
|
|
public string Description { get; }
|
|
|
|
public List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize)
|
|
{
|
|
var candidates = new List<PairCandidate>();
|
|
|
|
var part1 = Part.CreateAtOrigin(drawing);
|
|
var part2Template = Part.CreateAtOrigin(drawing, Part2Rotation);
|
|
|
|
var testNumber = 0;
|
|
|
|
// Try pushing left (horizontal slide)
|
|
GenerateCandidatesForAxis(
|
|
part1, part2Template, drawing, spacing, stepSize,
|
|
PushDirection.Left, candidates, ref testNumber);
|
|
|
|
// Try pushing down (vertical slide)
|
|
GenerateCandidatesForAxis(
|
|
part1, part2Template, drawing, spacing, stepSize,
|
|
PushDirection.Down, candidates, ref testNumber);
|
|
|
|
// Try pushing right (approach from left — finds concave interlocking)
|
|
GenerateCandidatesForAxis(
|
|
part1, part2Template, drawing, spacing, stepSize,
|
|
PushDirection.Right, candidates, ref testNumber);
|
|
|
|
// Try pushing up (approach from below — finds concave interlocking)
|
|
GenerateCandidatesForAxis(
|
|
part1, part2Template, drawing, spacing, stepSize,
|
|
PushDirection.Up, candidates, ref testNumber);
|
|
|
|
return candidates;
|
|
}
|
|
|
|
private void GenerateCandidatesForAxis(
|
|
Part part1, Part part2Template, Drawing drawing,
|
|
double spacing, double stepSize, PushDirection pushDir,
|
|
List<PairCandidate> candidates, ref int testNumber)
|
|
{
|
|
var bbox1 = part1.BoundingBox;
|
|
var bbox2 = part2Template.BoundingBox;
|
|
var halfSpacing = spacing / 2;
|
|
|
|
var isHorizontalPush = pushDir == PushDirection.Left || pushDir == PushDirection.Right;
|
|
|
|
// Perpendicular range: part2 slides across the full extent of part1
|
|
double perpMin, perpMax, pushStartOffset;
|
|
|
|
if (isHorizontalPush)
|
|
{
|
|
perpMin = -(bbox2.Height + spacing);
|
|
perpMax = bbox1.Height + bbox2.Height + spacing;
|
|
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
|
}
|
|
else
|
|
{
|
|
perpMin = -(bbox2.Width + spacing);
|
|
perpMax = bbox1.Width + bbox2.Width + spacing;
|
|
pushStartOffset = bbox1.Height + bbox2.Height + spacing * 2;
|
|
}
|
|
|
|
// Pre-compute part1's offset lines (half-spacing outward)
|
|
var part1Lines = Helper.GetOffsetPartLines(part1, halfSpacing);
|
|
|
|
// Align sweep start to a multiple of stepSize so that offset=0 is always
|
|
// included. This ensures perfect grid arrangements (side-by-side, stacked)
|
|
// are generated for rectangular parts.
|
|
var alignedStart = System.Math.Ceiling(perpMin / stepSize) * stepSize;
|
|
|
|
for (var offset = alignedStart; offset <= perpMax; offset += stepSize)
|
|
{
|
|
var part2 = (Part)part2Template.Clone();
|
|
|
|
// Place part2 far away along push axis, at perpendicular offset.
|
|
// Left/Down: start on the positive side; Right/Up: start on the negative side.
|
|
var isPositiveStart = pushDir == PushDirection.Left || pushDir == PushDirection.Down;
|
|
var startPos = isPositiveStart ? pushStartOffset : -pushStartOffset;
|
|
|
|
if (isHorizontalPush)
|
|
part2.Offset(startPos, offset);
|
|
else
|
|
part2.Offset(offset, startPos);
|
|
|
|
// Get part2's offset lines (half-spacing outward)
|
|
var part2Lines = Helper.GetOffsetPartLines(part2, halfSpacing);
|
|
|
|
// Find contact distance
|
|
var slideDist = Helper.DirectionalDistance(part2Lines, part1Lines, pushDir);
|
|
|
|
if (slideDist >= double.MaxValue || slideDist < 0)
|
|
continue;
|
|
|
|
// Move part2 to contact position
|
|
var pushVector = GetPushVector(pushDir, slideDist);
|
|
var finalPosition = part2.Location + pushVector;
|
|
|
|
candidates.Add(new PairCandidate
|
|
{
|
|
Drawing = drawing,
|
|
Part1Rotation = 0,
|
|
Part2Rotation = Part2Rotation,
|
|
Part2Offset = finalPosition,
|
|
StrategyType = Type,
|
|
TestNumber = testNumber++,
|
|
Spacing = spacing
|
|
});
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|