mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 10:15:07 -05:00
The pair search slid part 2 in from offsets on a 0.25 step grid, so a pair that locks in a corner (two interlocking channels) stopped up to a step short of the tightest fit. Each resting position is now also slid sideways, both ways, and the new resting positions join the candidates. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
235 lines
8.2 KiB
C#
235 lines
8.2 KiB
C#
using System.Collections.Generic;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Engine.BestFit
|
|
{
|
|
public class RotationSlideStrategy : IBestFitStrategy
|
|
{
|
|
/// <summary>Settled positions closer than this to an earlier one are duplicates.</summary>
|
|
private const double PositionResolution = 1e-4;
|
|
|
|
private readonly IDistanceComputer _distanceComputer;
|
|
|
|
private static readonly (double DirX, double DirY)[] PushDirections =
|
|
{
|
|
(-1, 0), // Left
|
|
(0, -1), // Down
|
|
(1, 0), // Right
|
|
(0, 1), // Up
|
|
};
|
|
|
|
public RotationSlideStrategy(
|
|
double part2Rotation,
|
|
int strategyIndex,
|
|
string description,
|
|
IDistanceComputer distanceComputer
|
|
)
|
|
{
|
|
Part2Rotation = part2Rotation;
|
|
StrategyIndex = strategyIndex;
|
|
Description = description;
|
|
_distanceComputer = distanceComputer;
|
|
}
|
|
|
|
public double Part2Rotation { get; }
|
|
public int StrategyIndex { 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 halfSpacing = spacing / 2;
|
|
var part1Entities = PartGeometry.GetOffsetPerimeterEntities(part1, halfSpacing);
|
|
var part2Entities = PartGeometry.GetOffsetPerimeterEntities(part2Template, halfSpacing);
|
|
|
|
var bbox1 = part1.BoundingBox;
|
|
var bbox2 = part2Template.BoundingBox;
|
|
|
|
var offsets = BuildOffsets(bbox1, bbox2, spacing, stepSize);
|
|
|
|
if (offsets.Length == 0)
|
|
return candidates;
|
|
|
|
var distances = _distanceComputer.ComputeDistances(
|
|
part1Entities,
|
|
part2Entities,
|
|
offsets
|
|
);
|
|
|
|
// Where each slide came to rest, as an offset of the part 2 template.
|
|
var rests = new List<SlideOffset>();
|
|
|
|
for (var i = 0; i < offsets.Length; i++)
|
|
{
|
|
var slideDist = distances[i];
|
|
if (slideDist >= double.MaxValue || slideDist < 0)
|
|
continue;
|
|
|
|
rests.Add(
|
|
new SlideOffset(
|
|
offsets[i].Dx + offsets[i].DirX * slideDist,
|
|
offsets[i].Dy + offsets[i].DirY * slideDist,
|
|
offsets[i].DirX,
|
|
offsets[i].DirY
|
|
)
|
|
);
|
|
}
|
|
|
|
var testNumber = 0;
|
|
|
|
foreach (var rest in rests)
|
|
candidates.Add(CreateCandidate(drawing, part2Template, rest, spacing, testNumber++));
|
|
|
|
foreach (var settled in Settle(part1Entities, part2Entities, rests))
|
|
candidates.Add(CreateCandidate(drawing, part2Template, settled, spacing, testNumber++));
|
|
|
|
return candidates;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Slides every resting position sideways, both ways, until part 2 meets part 1 again.
|
|
/// The first slide starts on the step grid, so it can stop up to a step short of a
|
|
/// corner that locks the pair tighter (two interlocking channels, say); the sideways
|
|
/// slide closes that gap exactly. Positions that cannot move, never meet part 1 again
|
|
/// or repeat an earlier one are dropped.
|
|
/// </summary>
|
|
private List<SlideOffset> Settle(
|
|
List<Entity> part1Entities,
|
|
List<Entity> part2Entities,
|
|
List<SlideOffset> rests
|
|
)
|
|
{
|
|
var settled = new List<SlideOffset>();
|
|
|
|
if (rests.Count == 0)
|
|
return settled;
|
|
|
|
var sideways = new SlideOffset[rests.Count * 2];
|
|
|
|
for (var i = 0; i < rests.Count; i++)
|
|
{
|
|
var rest = rests[i];
|
|
// Perpendicular to the first slide: (x, y) -> (-y, x) and its opposite.
|
|
sideways[i * 2] = new SlideOffset(rest.Dx, rest.Dy, -rest.DirY, rest.DirX);
|
|
sideways[i * 2 + 1] = new SlideOffset(rest.Dx, rest.Dy, rest.DirY, -rest.DirX);
|
|
}
|
|
|
|
var distances = _distanceComputer.ComputeDistances(
|
|
part1Entities,
|
|
part2Entities,
|
|
sideways
|
|
);
|
|
|
|
var seen = new HashSet<(long X, long Y)>();
|
|
|
|
foreach (var rest in rests)
|
|
seen.Add(PositionKey(rest.Dx, rest.Dy));
|
|
|
|
for (var i = 0; i < sideways.Length; i++)
|
|
{
|
|
var slideDist = distances[i];
|
|
if (slideDist >= double.MaxValue || slideDist <= Tolerance.Epsilon)
|
|
continue;
|
|
|
|
var dx = sideways[i].Dx + sideways[i].DirX * slideDist;
|
|
var dy = sideways[i].Dy + sideways[i].DirY * slideDist;
|
|
|
|
if (seen.Add(PositionKey(dx, dy)))
|
|
settled.Add(new SlideOffset(dx, dy, sideways[i].DirX, sideways[i].DirY));
|
|
}
|
|
|
|
return settled;
|
|
}
|
|
|
|
private static (long X, long Y) PositionKey(double dx, double dy) =>
|
|
(
|
|
(long)System.Math.Round(dx / PositionResolution),
|
|
(long)System.Math.Round(dy / PositionResolution)
|
|
);
|
|
|
|
private PairCandidate CreateCandidate(
|
|
Drawing drawing,
|
|
Part part2Template,
|
|
SlideOffset rest,
|
|
double spacing,
|
|
int testNumber
|
|
)
|
|
{
|
|
return new PairCandidate
|
|
{
|
|
Drawing = drawing,
|
|
Part1Rotation = 0,
|
|
Part2Rotation = Part2Rotation,
|
|
Part2Offset = new Vector(
|
|
part2Template.Location.X + rest.Dx,
|
|
part2Template.Location.Y + rest.Dy
|
|
),
|
|
StrategyIndex = StrategyIndex,
|
|
TestNumber = testNumber,
|
|
Spacing = spacing,
|
|
};
|
|
}
|
|
|
|
private static SlideOffset[] BuildOffsets(
|
|
Box bbox1,
|
|
Box bbox2,
|
|
double spacing,
|
|
double stepSize
|
|
)
|
|
{
|
|
var offsets = new List<SlideOffset>();
|
|
|
|
foreach (var (dirX, dirY) in PushDirections)
|
|
{
|
|
var isHorizontalPush = System.Math.Abs(dirX) > System.Math.Abs(dirY);
|
|
|
|
double perpMin,
|
|
perpMax,
|
|
pushStartOffset;
|
|
|
|
if (isHorizontalPush)
|
|
{
|
|
// Perpendicular sweep along Y → Width; push extent along X → Length
|
|
// Trim to offsets where the parts overlap by at least 50%.
|
|
var halfOverlap = bbox2.Width * 0.5;
|
|
perpMin = -(halfOverlap - spacing);
|
|
perpMax = bbox1.Width + halfOverlap + spacing;
|
|
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
|
|
}
|
|
else
|
|
{
|
|
// Perpendicular sweep along X → Length; push extent along Y → Width
|
|
var halfOverlap = bbox2.Length * 0.5;
|
|
perpMin = -(halfOverlap - spacing);
|
|
perpMax = bbox1.Length + halfOverlap + spacing;
|
|
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
|
|
}
|
|
|
|
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 dx = isHorizontalPush ? startPos : offset;
|
|
var dy = isHorizontalPush ? offset : startPos;
|
|
offsets.Add(new SlideOffset(dx, dy, dirX, dirY));
|
|
}
|
|
}
|
|
|
|
return offsets.ToArray();
|
|
}
|
|
}
|
|
}
|