30 KiB
Best-Fit Pair Finding Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Build a pair-finding engine that arranges two copies of a part in the tightest configuration, then tiles that pair across a plate.
Architecture: Strategy pattern where RotationSlideStrategy instances (parameterized by angle) generate candidate pair configurations by sliding one part against another using existing raycast collision. A PairEvaluator scores candidates by bounding area, a BestFitFilter prunes bad fits, and a TileEvaluator simulates tiling the best pairs onto a plate.
Tech Stack: .NET Framework 4.8, C# 7.3, OpenNest.Engine (class library referencing OpenNest.Core)
Important Context
Codebase Conventions
- All angles are in radians — use
Angle.ToRadians(),Angle.HalfPI,Angle.TwoPI - Always use
varinstead of explicit types OpenNest.MathshadowsSystem.Math— useSystem.Mathfully qualified- Legacy
.csproj— every new.csfile must be added toOpenNest.Engine.csproj<Compile>items - No test project exists — skip TDD steps, verify by building
Key Existing Types
Vector(struct,OpenNest.Geometry) — 2D point, hasRotate(),Offset(),DistanceTo(), operatorsBox(class,OpenNest.Geometry) — AABB withLeft/Right/Top/Bottom/Width/Height,Contains(),Intersects()Part(class,OpenNest) — wrapsDrawing+Program, hasLocation,Rotation,Rotate(),Offset(),Clone(),BoundingBoxDrawing(class,OpenNest) — hasProgram,Area,NameProgram(class,OpenNest.CNC) — G-code program, hasBoundingBox(),Rotate(),Clone()Plate(class,OpenNest) — hasSize(Width/Height),EdgeSpacing,PartSpacing,WorkArea()Shape(class,OpenNest.Geometry) — closed contour, hasIntersects(Shape),Area(),ToPolygon(),OffsetEntity()Polygon(class,OpenNest.Geometry) — vertex list, hasFindBestRotation(),Rotate(),Offset()ConvexHull.Compute(IList<Vector>)— returns closedPolygonBoundingRectangleResult—Angle,Width,Height,Areafrom rotating calipers
Key Existing Methods (in Helper)
Helper.GetShapes(IEnumerable<Entity>)— buildsShapelist from geometry entitiesHelper.GetPartLines(Part, PushDirection)— gets polygon edges facing a direction (uses chord tolerance 0.01)Helper.DirectionalDistance(movingLines, stationaryLines, PushDirection)— raycasts to find minimum contact distanceHelper.OppositeDirection(PushDirection)— flips directionConvertProgram.ToGeometry(Program)— converts CNC program to geometry entities
How Existing Push/Contact Works (in FillLinear)
1. Create partA at position
2. Clone to partB, offset by bounding box dimension along axis
3. Get facing lines: movingLines = GetPartLines(partB, pushDir)
4. Get facing lines: stationaryLines = GetPartLines(partA, oppositeDir)
5. slideDistance = DirectionalDistance(movingLines, stationaryLines, pushDir)
6. copyDistance = bboxDim - slideDistance + spacing
The best-fit system adapts this: part2 is rotated, offset perpendicular to the push axis, then pushed toward part1.
Hull Edge Angles (existing pattern in NestEngine)
1. Convert part to polygon via ConvertProgram.ToGeometry → GetShapes → ToPolygonWithTolerance
2. Compute convex hull via ConvexHull.Compute(vertices)
3. Extract edge angles: atan2(dy, dx) for each hull edge
4. Deduplicate angles (within Tolerance.Epsilon)
Task 1: PairCandidate Data Class
Files:
- Create:
OpenNest.Engine/BestFit/PairCandidate.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj(add Compile entry)
Step 1: Create directory and file
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit
{
public class PairCandidate
{
public Drawing Drawing { get; set; }
public double Part1Rotation { get; set; }
public double Part2Rotation { get; set; }
public Vector Part2Offset { get; set; }
public int StrategyType { get; set; }
public int TestNumber { get; set; }
public double Spacing { get; set; }
}
}
Step 2: Add to .csproj
Add inside the <ItemGroup> that contains <Compile> entries, before </ItemGroup>:
<Compile Include="BestFit\PairCandidate.cs" />
Step 3: Build to verify
Run: msbuild OpenNest.Engine/OpenNest.Engine.csproj /p:Configuration=Debug /v:q
Expected: Build succeeded
Step 4: Commit
feat: add PairCandidate data class for best-fit pair finding
Task 2: BestFitResult Data Class
Files:
- Create:
OpenNest.Engine/BestFit/BestFitResult.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Step 1: Create file
namespace OpenNest.Engine.BestFit
{
public class BestFitResult
{
public PairCandidate Candidate { get; set; }
public double RotatedArea { get; set; }
public double BoundingWidth { get; set; }
public double BoundingHeight { get; set; }
public double OptimalRotation { get; set; }
public bool Keep { get; set; }
public string Reason { get; set; }
public double TrueArea { get; set; }
public double Utilization
{
get { return RotatedArea > 0 ? TrueArea / RotatedArea : 0; }
}
public double LongestSide
{
get { return System.Math.Max(BoundingWidth, BoundingHeight); }
}
public double ShortestSide
{
get { return System.Math.Min(BoundingWidth, BoundingHeight); }
}
}
public enum BestFitSortField
{
Area,
LongestSide,
ShortestSide,
Type,
OriginalSequence,
Keep,
WhyKeepDrop
}
}
Step 2: Add to .csproj
<Compile Include="BestFit\BestFitResult.cs" />
Step 3: Build to verify
Run: msbuild OpenNest.Engine/OpenNest.Engine.csproj /p:Configuration=Debug /v:q
Step 4: Commit
feat: add BestFitResult data class and BestFitSortField enum
Task 3: IBestFitStrategy Interface
Files:
- Create:
OpenNest.Engine/BestFit/IBestFitStrategy.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Step 1: Create file
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit
{
public interface IBestFitStrategy
{
int Type { get; }
string Description { get; }
List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize);
}
}
Step 2: Add to .csproj
<Compile Include="BestFit\IBestFitStrategy.cs" />
Step 3: Build to verify
Step 4: Commit
feat: add IBestFitStrategy interface
Task 4: RotationSlideStrategy
This is the core algorithm. It generates pair candidates by:
- Creating part1 at origin
- Creating part2 with a specific rotation
- For each push direction (Left, Down):
- For each perpendicular offset (stepping across the part):
- Place part2 far away along the push axis
- Use
DirectionalDistanceto find contact - Record position as a candidate
- For each perpendicular offset (stepping across the part):
Files:
- Create:
OpenNest.Engine/BestFit/RotationSlideStrategy.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Step 1: Create file
using System;
using System.Collections.Generic;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.BestFit
{
public class RotationSlideStrategy : IBestFitStrategy
{
private const double ChordTolerance = 0.01;
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 = new Part(drawing);
var bbox1 = part1.Program.BoundingBox();
part1.Offset(-bbox1.Location.X, -bbox1.Location.Y);
part1.UpdateBounds();
var part2Template = new Part(drawing);
if (!Part2Rotation.IsEqualTo(0))
part2Template.Rotate(Part2Rotation);
var bbox2 = part2Template.Program.BoundingBox();
part2Template.Offset(-bbox2.Location.X, -bbox2.Location.Y);
part2Template.UpdateBounds();
var testNumber = 0;
// Slide along horizontal axis (push left toward part1)
GenerateCandidatesForAxis(
part1, part2Template, drawing, spacing, stepSize,
PushDirection.Left, candidates, ref testNumber);
// Slide along vertical axis (push down toward part1)
GenerateCandidatesForAxis(
part1, part2Template, drawing, spacing, stepSize,
PushDirection.Down, 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;
// Determine perpendicular range based on push direction
double perpMin, perpMax, pushStartOffset;
bool isHorizontalPush = (pushDir == PushDirection.Left || pushDir == PushDirection.Right);
if (isHorizontalPush)
{
// Pushing horizontally: perpendicular axis is Y
perpMin = -(bbox2.Height + spacing);
perpMax = bbox1.Height + bbox2.Height + spacing;
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
}
else
{
// Pushing vertically: perpendicular axis is X
perpMin = -(bbox2.Width + spacing);
perpMax = bbox1.Width + bbox2.Width + spacing;
pushStartOffset = bbox1.Height + bbox2.Height + spacing * 2;
}
var part1Lines = Helper.GetOffsetPartLines(part1, spacing / 2);
var opposite = Helper.OppositeDirection(pushDir);
for (var offset = perpMin; offset <= perpMax; offset += stepSize)
{
var part2 = (Part)part2Template.Clone();
if (isHorizontalPush)
part2.Offset(pushStartOffset, offset);
else
part2.Offset(offset, pushStartOffset);
var movingLines = Helper.GetOffsetPartLines(part2, spacing / 2);
var slideDist = Helper.DirectionalDistance(movingLines, part1Lines, pushDir);
if (slideDist >= double.MaxValue || slideDist < 0)
continue;
// Move part2 to contact position
var contactOffset = GetPushVector(pushDir, slideDist);
var finalPosition = part2.Location + contactOffset;
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;
}
}
}
}
Step 2: Add to .csproj
<Compile Include="BestFit\RotationSlideStrategy.cs" />
Step 3: Build to verify
Run: msbuild OpenNest.Engine/OpenNest.Engine.csproj /p:Configuration=Debug /v:q
Step 4: Commit
feat: add RotationSlideStrategy with directional push contact algorithm
Task 5: PairEvaluator
Scores each candidate by computing the combined bounding box, finding the optimal rotation (via rotating calipers on the convex hull), and checking for overlaps.
Files:
- Create:
OpenNest.Engine/BestFit/PairEvaluator.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Step 1: Create file
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit
{
public class PairEvaluator
{
private const double ChordTolerance = 0.01;
public BestFitResult Evaluate(PairCandidate candidate)
{
var drawing = candidate.Drawing;
// Build part1 at origin
var part1 = new Part(drawing);
var bbox1 = part1.Program.BoundingBox();
part1.Offset(-bbox1.Location.X, -bbox1.Location.Y);
part1.UpdateBounds();
// Build part2 with rotation and offset
var part2 = new Part(drawing);
if (!candidate.Part2Rotation.IsEqualTo(0))
part2.Rotate(candidate.Part2Rotation);
var bbox2 = part2.Program.BoundingBox();
part2.Offset(-bbox2.Location.X, -bbox2.Location.Y);
part2.Location = candidate.Part2Offset;
part2.UpdateBounds();
// Check overlap via shape intersection
var overlaps = CheckOverlap(part1, part2, candidate.Spacing);
// Collect all polygon vertices for convex hull / optimal rotation
var allPoints = GetPartVertices(part1);
allPoints.AddRange(GetPartVertices(part2));
// Find optimal bounding rectangle via rotating calipers
double bestArea, bestWidth, bestHeight, bestRotation;
if (allPoints.Count >= 3)
{
var hull = ConvexHull.Compute(allPoints);
var result = RotatingCalipers.MinimumBoundingRectangle(hull);
bestArea = result.Area;
bestWidth = result.Width;
bestHeight = result.Height;
bestRotation = result.Angle;
}
else
{
var combinedBox = ((IEnumerable<IBoundable>)new[] { part1, part2 }).GetBoundingBox();
bestArea = combinedBox.Area();
bestWidth = combinedBox.Width;
bestHeight = combinedBox.Height;
bestRotation = 0;
}
var trueArea = drawing.Area * 2;
return new BestFitResult
{
Candidate = candidate,
RotatedArea = bestArea,
BoundingWidth = bestWidth,
BoundingHeight = bestHeight,
OptimalRotation = bestRotation,
TrueArea = trueArea,
Keep = !overlaps,
Reason = overlaps ? "Overlap detected" : "Valid"
};
}
private bool CheckOverlap(Part part1, Part part2, double spacing)
{
var shapes1 = GetPartShapes(part1);
var shapes2 = GetPartShapes(part2);
for (var i = 0; i < shapes1.Count; i++)
{
for (var j = 0; j < shapes2.Count; j++)
{
List<Vector> pts;
if (shapes1[i].Intersects(shapes2[j], out pts))
return true;
}
}
return false;
}
private List<Shape> GetPartShapes(Part part)
{
var entities = ConvertProgram.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid);
var shapes = Helper.GetShapes(entities);
shapes.ForEach(s => s.Offset(part.Location));
return shapes;
}
private List<Vector> GetPartVertices(Part part)
{
var entities = ConvertProgram.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid);
var shapes = Helper.GetShapes(entities);
var points = new List<Vector>();
foreach (var shape in shapes)
{
var polygon = shape.ToPolygonWithTolerance(ChordTolerance);
polygon.Offset(part.Location);
foreach (var vertex in polygon.Vertices)
points.Add(vertex);
}
return points;
}
}
}
Step 2: Add to .csproj
<Compile Include="BestFit\PairEvaluator.cs" />
Step 3: Build to verify
Step 4: Commit
feat: add PairEvaluator with overlap detection and optimal rotation
Task 6: BestFitFilter
Files:
- Create:
OpenNest.Engine/BestFit/BestFitFilter.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Step 1: Create file
using System.Collections.Generic;
namespace OpenNest.Engine.BestFit
{
public class BestFitFilter
{
public double MaxPlateWidth { get; set; }
public double MaxPlateHeight { get; set; }
public double MaxAspectRatio { get; set; } = 5.0;
public double MinUtilization { get; set; } = 0.3;
public void Apply(List<BestFitResult> results)
{
foreach (var result in results)
{
if (!result.Keep)
continue;
if (result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight))
{
result.Keep = false;
result.Reason = "Exceeds plate dimensions";
continue;
}
var aspect = result.LongestSide / result.ShortestSide;
if (aspect > MaxAspectRatio)
{
result.Keep = false;
result.Reason = string.Format("Aspect ratio {0:F1} exceeds max {1}", aspect, MaxAspectRatio);
continue;
}
if (result.Utilization < MinUtilization)
{
result.Keep = false;
result.Reason = string.Format("Utilization {0:P0} below minimum", result.Utilization);
continue;
}
result.Reason = "Valid";
}
}
}
}
Step 2: Add to .csproj
<Compile Include="BestFit\BestFitFilter.cs" />
Step 3: Build to verify
Step 4: Commit
feat: add BestFitFilter with plate size, aspect ratio, and utilization rules
Task 7: TileResult and TileEvaluator
Files:
- Create:
OpenNest.Engine/BestFit/Tiling/TileResult.cs - Create:
OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Step 1: Create TileResult.cs
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit.Tiling
{
public class TileResult
{
public BestFitResult BestFit { get; set; }
public int PairsNested { get; set; }
public int PartsNested { get; set; }
public int Rows { get; set; }
public int Columns { get; set; }
public double Utilization { get; set; }
public List<PairPlacement> Placements { get; set; }
public bool PairRotated { get; set; }
}
public class PairPlacement
{
public Vector Position { get; set; }
public double PairRotation { get; set; }
}
}
Step 2: Create TileEvaluator.cs
using System;
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.BestFit.Tiling
{
public class TileEvaluator
{
public TileResult Evaluate(BestFitResult bestFit, Plate plate)
{
var plateWidth = plate.Size.Width - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right;
var plateHeight = plate.Size.Height - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom;
var result1 = TryTile(bestFit, plateWidth, plateHeight, false);
var result2 = TryTile(bestFit, plateWidth, plateHeight, true);
return result1.PartsNested >= result2.PartsNested ? result1 : result2;
}
private TileResult TryTile(BestFitResult bestFit, double plateWidth, double plateHeight, bool rotatePair)
{
var pairWidth = rotatePair ? bestFit.BoundingHeight : bestFit.BoundingWidth;
var pairHeight = rotatePair ? bestFit.BoundingWidth : bestFit.BoundingHeight;
var spacing = bestFit.Candidate.Spacing;
var cols = (int)System.Math.Floor((plateWidth + spacing) / (pairWidth + spacing));
var rows = (int)System.Math.Floor((plateHeight + spacing) / (pairHeight + spacing));
var pairsNested = cols * rows;
var partsNested = pairsNested * 2;
var usedArea = partsNested * (bestFit.TrueArea / 2);
var plateArea = plateWidth * plateHeight;
var placements = new List<PairPlacement>();
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
placements.Add(new PairPlacement
{
Position = new Vector(
col * (pairWidth + spacing),
row * (pairHeight + spacing)),
PairRotation = rotatePair ? Angle.HalfPI : 0
});
}
}
return new TileResult
{
BestFit = bestFit,
PairsNested = pairsNested,
PartsNested = partsNested,
Rows = rows,
Columns = cols,
Utilization = plateArea > 0 ? usedArea / plateArea : 0,
Placements = placements,
PairRotated = rotatePair
};
}
}
}
Step 3: Add to .csproj
<Compile Include="BestFit\Tiling\TileResult.cs" />
<Compile Include="BestFit\Tiling\TileEvaluator.cs" />
Step 4: Build to verify
Step 5: Commit
feat: add TileEvaluator and TileResult for pair tiling on plates
Task 8: BestFitFinder (Orchestrator)
Computes hull edge angles from the drawing, builds RotationSlideStrategy instances for each angle in {0, pi/2, pi, 3pi/2} + hull edges + hull edges + pi, runs all strategies, evaluates, filters, and sorts.
Files:
- Create:
OpenNest.Engine/BestFit/BestFitFinder.cs - Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Step 1: Create file
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Engine.BestFit.Tiling;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.BestFit
{
public class BestFitFinder
{
private readonly PairEvaluator _evaluator;
private readonly BestFitFilter _filter;
public BestFitFinder(double maxPlateWidth, double maxPlateHeight)
{
_evaluator = new PairEvaluator();
_filter = new BestFitFilter
{
MaxPlateWidth = maxPlateWidth,
MaxPlateHeight = maxPlateHeight
};
}
public List<BestFitResult> FindBestFits(
Drawing drawing,
double spacing = 0.25,
double stepSize = 0.25,
BestFitSortField sortBy = BestFitSortField.Area)
{
var strategies = BuildStrategies(drawing);
var allCandidates = new List<PairCandidate>();
foreach (var strategy in strategies)
allCandidates.AddRange(strategy.GenerateCandidates(drawing, spacing, stepSize));
var results = allCandidates.Select(c => _evaluator.Evaluate(c)).ToList();
_filter.Apply(results);
results = SortResults(results, sortBy);
for (var i = 0; i < results.Count; i++)
results[i].Candidate.TestNumber = i;
return results;
}
public List<TileResult> FindAndTile(
Drawing drawing, Plate plate,
double spacing = 0.25, double stepSize = 0.25, int topN = 10)
{
var bestFits = FindBestFits(drawing, spacing, stepSize);
var tileEvaluator = new TileEvaluator();
return bestFits
.Where(r => r.Keep)
.Take(topN)
.Select(r => tileEvaluator.Evaluate(r, plate))
.OrderByDescending(t => t.PartsNested)
.ThenByDescending(t => t.Utilization)
.ToList();
}
private List<IBestFitStrategy> BuildStrategies(Drawing drawing)
{
var angles = GetRotationAngles(drawing);
var strategies = new List<IBestFitStrategy>();
var type = 1;
foreach (var angle in angles)
{
var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle));
strategies.Add(new RotationSlideStrategy(angle, type++, desc));
}
return strategies;
}
private List<double> GetRotationAngles(Drawing drawing)
{
var angles = new List<double>
{
0,
Angle.HalfPI,
System.Math.PI,
Angle.HalfPI * 3
};
// Add hull edge angles
var hullAngles = GetHullEdgeAngles(drawing);
foreach (var hullAngle in hullAngles)
{
AddUniqueAngle(angles, hullAngle);
AddUniqueAngle(angles, Angle.NormalizeRad(hullAngle + System.Math.PI));
}
return angles;
}
private List<double> GetHullEdgeAngles(Drawing drawing)
{
var entities = ConvertProgram.ToGeometry(drawing.Program)
.Where(e => e.Layer != SpecialLayers.Rapid);
var shapes = Helper.GetShapes(entities);
var points = new List<Vector>();
foreach (var shape in shapes)
{
var polygon = shape.ToPolygonWithTolerance(0.1);
points.AddRange(polygon.Vertices);
}
if (points.Count < 3)
return new List<double>();
var hull = ConvexHull.Compute(points);
var vertices = hull.Vertices;
var n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count;
var hullAngles = new List<double>();
for (var i = 0; i < n; i++)
{
var next = (i + 1) % n;
var dx = vertices[next].X - vertices[i].X;
var dy = vertices[next].Y - vertices[i].Y;
if (dx * dx + dy * dy < Tolerance.Epsilon)
continue;
var angle = Angle.NormalizeRad(System.Math.Atan2(dy, dx));
AddUniqueAngle(hullAngles, angle);
}
return hullAngles;
}
private static void AddUniqueAngle(List<double> angles, double angle)
{
angle = Angle.NormalizeRad(angle);
foreach (var existing in angles)
{
if (existing.IsEqualTo(angle))
return;
}
angles.Add(angle);
}
private List<BestFitResult> SortResults(List<BestFitResult> results, BestFitSortField sortBy)
{
switch (sortBy)
{
case BestFitSortField.Area:
return results.OrderBy(r => r.RotatedArea).ToList();
case BestFitSortField.LongestSide:
return results.OrderBy(r => r.LongestSide).ToList();
case BestFitSortField.ShortestSide:
return results.OrderBy(r => r.ShortestSide).ToList();
case BestFitSortField.Type:
return results.OrderBy(r => r.Candidate.StrategyType)
.ThenBy(r => r.Candidate.TestNumber).ToList();
case BestFitSortField.OriginalSequence:
return results.OrderBy(r => r.Candidate.TestNumber).ToList();
case BestFitSortField.Keep:
return results.OrderByDescending(r => r.Keep)
.ThenBy(r => r.RotatedArea).ToList();
case BestFitSortField.WhyKeepDrop:
return results.OrderBy(r => r.Reason)
.ThenBy(r => r.RotatedArea).ToList();
default:
return results;
}
}
}
}
Step 2: Add to .csproj
<Compile Include="BestFit\BestFitFinder.cs" />
Step 3: Build full solution to verify all references resolve
Run: msbuild OpenNest.sln /p:Configuration=Debug /v:q
Step 4: Commit
feat: add BestFitFinder orchestrator with hull edge angle strategies
Task 9: Final Integration Build and Smoke Test
Step 1: Clean build of entire solution
Run: msbuild OpenNest.sln /t:Rebuild /p:Configuration=Debug /v:q
Expected: Build succeeded, 0 errors
Step 2: Verify all new files are included
Check that all 8 new files appear in the build output by reviewing the .csproj has these entries:
<Compile Include="BestFit\PairCandidate.cs" />
<Compile Include="BestFit\BestFitResult.cs" />
<Compile Include="BestFit\IBestFitStrategy.cs" />
<Compile Include="BestFit\RotationSlideStrategy.cs" />
<Compile Include="BestFit\PairEvaluator.cs" />
<Compile Include="BestFit\BestFitFilter.cs" />
<Compile Include="BestFit\Tiling\TileResult.cs" />
<Compile Include="BestFit\Tiling\TileEvaluator.cs" />
<Compile Include="BestFit\BestFitFinder.cs" />
Step 3: Final commit
If any build fixes were needed, commit them:
fix: resolve build issues in best-fit pair finding engine