Files
OpenNest/OpenNest.Engine/BestFit/RotationSlideStrategy.cs
AJ Isaacs 3220306d3a feat: add reverse push directions for concave interlocking and cache best-fit results
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>
2026-03-08 14:02:41 -04:00

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;
}
}
}
}