feat: smart strategy skipping, pack rotation, and dual-sort packing
- Skip ExtentsFillStrategy for rectangle/circle parts - Skip PairsFillStrategy for circle parts - PackBottomLeft now tries rotated orientation when items don't fit - PackBottomLeft tries both area-descending and length-descending sort orders, keeping whichever places more parts (tighter bbox on tie) - Add user constraint override tests for AngleCandidateBuilder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -7,33 +7,50 @@ namespace OpenNest.RectanglePacking
|
||||
{
|
||||
internal class PackBottomLeft : PackEngine
|
||||
{
|
||||
private List<Vector> points;
|
||||
|
||||
public PackBottomLeft(Bin bin)
|
||||
: base(bin)
|
||||
{
|
||||
points = new List<Vector>();
|
||||
}
|
||||
|
||||
public override void Pack(List<Item> items)
|
||||
{
|
||||
items = items.OrderBy(i => -i.Area()).ToList();
|
||||
var byArea = items.Select(i => i.Clone() as Item).OrderByDescending(i => i.Area()).ToList();
|
||||
var byLength = items.Select(i => i.Clone() as Item).OrderByDescending(i => System.Math.Max(i.Width, i.Length)).ToList();
|
||||
|
||||
points.Add(Bin.Location);
|
||||
var resultA = PackWithOrder(byArea);
|
||||
var resultB = PackWithOrder(byLength);
|
||||
|
||||
var winner = PickWinner(resultA, resultB);
|
||||
|
||||
Bin.Items.AddRange(winner);
|
||||
}
|
||||
|
||||
private List<Item> PackWithOrder(List<Item> items)
|
||||
{
|
||||
var points = new List<Vector> { Bin.Location };
|
||||
var placed = new List<Item>();
|
||||
var skip = new List<int>();
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
|
||||
if (skip.Contains(item.Id))
|
||||
continue;
|
||||
|
||||
var pt = FindPointVertical(item);
|
||||
var pt = FindPointVertical(item, points, placed);
|
||||
|
||||
// If it doesn't fit, try rotated.
|
||||
if (pt == null)
|
||||
{
|
||||
item.Rotate();
|
||||
pt = FindPointVertical(item, points, placed);
|
||||
}
|
||||
|
||||
if (pt == null)
|
||||
{
|
||||
if (item.IsRotated)
|
||||
item.Rotate();
|
||||
skip.Add(item.Id);
|
||||
continue;
|
||||
}
|
||||
@@ -44,23 +61,37 @@ namespace OpenNest.RectanglePacking
|
||||
points.Add(new Vector(item.Left, item.Top));
|
||||
points.Add(new Vector(item.Right, item.Bottom));
|
||||
|
||||
Bin.Items.Add(item);
|
||||
placed.Add(item);
|
||||
}
|
||||
|
||||
points.Clear();
|
||||
return placed;
|
||||
}
|
||||
|
||||
private Vector? FindPointVertical(Item item)
|
||||
private static List<Item> PickWinner(List<Item> a, List<Item> b)
|
||||
{
|
||||
if (a.Count != b.Count)
|
||||
return a.Count > b.Count ? a : b;
|
||||
|
||||
if (a.Count == 0)
|
||||
return a;
|
||||
|
||||
var areaA = a.GetBoundingBox().Area();
|
||||
var areaB = b.GetBoundingBox().Area();
|
||||
|
||||
return areaB < areaA ? b : a;
|
||||
}
|
||||
|
||||
private Vector? FindPointVertical(Item item, List<Vector> points, List<Item> placed)
|
||||
{
|
||||
var pt = new Vector(double.MaxValue, double.MaxValue);
|
||||
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
{
|
||||
var point = points[i];
|
||||
|
||||
item.Location = point;
|
||||
|
||||
if (!IsValid(item))
|
||||
if (!IsValid(item, placed))
|
||||
continue;
|
||||
|
||||
if (point.X < pt.X)
|
||||
@@ -75,12 +106,12 @@ namespace OpenNest.RectanglePacking
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsValid(Item item)
|
||||
private bool IsValid(Item item, List<Item> placed)
|
||||
{
|
||||
if (!Bin.Contains(item))
|
||||
return false;
|
||||
|
||||
foreach (var it in Bin.Items)
|
||||
foreach (var it in placed)
|
||||
{
|
||||
if (item.Intersects(it))
|
||||
return false;
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace OpenNest.Engine.Strategies
|
||||
|
||||
public List<Part> Fill(FillContext context)
|
||||
{
|
||||
if (context.PartType == PartType.Rectangle || context.PartType == PartType.Circle)
|
||||
return null;
|
||||
var filler = new FillExtents(context.WorkArea, context.Plate.PartSpacing);
|
||||
|
||||
var bestRotation = context.SharedState.TryGetValue("BestRotation", out var rot)
|
||||
|
||||
@@ -20,6 +20,9 @@ namespace OpenNest.Engine.Strategies
|
||||
if (active.Value)
|
||||
return null;
|
||||
|
||||
if (context.PartType == PartType.Circle)
|
||||
return null;
|
||||
|
||||
active.Value = true;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
@@ -110,4 +111,45 @@ public class AngleCandidateBuilderTests
|
||||
Assert.Single(angles);
|
||||
Assert.Equal(0, angles[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UserConstraints_OverrideRectangleClassification()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem
|
||||
{
|
||||
Drawing = MakeRectDrawing(100, 50),
|
||||
RotationStart = Angle.ToRadians(10),
|
||||
RotationEnd = Angle.ToRadians(90),
|
||||
StepAngle = Angle.ToRadians(10),
|
||||
};
|
||||
var classification = MakeClassification(0, PartType.Rectangle);
|
||||
var workArea = new Box(0, 0, 1000, 500);
|
||||
|
||||
var angles = builder.Build(item, classification, workArea);
|
||||
|
||||
Assert.True(angles.Count > 2,
|
||||
$"User constraints should override rect classification, got {angles.Count} angles");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UserConstraints_StartingAtZero_AreRespected()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem
|
||||
{
|
||||
Drawing = MakeRectDrawing(100, 50),
|
||||
RotationStart = 0,
|
||||
RotationEnd = System.Math.PI,
|
||||
StepAngle = Angle.ToRadians(45),
|
||||
};
|
||||
var classification = MakeClassification(0, PartType.Rectangle);
|
||||
var workArea = new Box(0, 0, 1000, 500);
|
||||
|
||||
var angles = builder.Build(item, classification, workArea);
|
||||
|
||||
// Start=0, End=PI is NOT "no constraints" — it's a real 0-180 range
|
||||
Assert.True(angles.Count > 2,
|
||||
$"0-to-PI constraint should produce multiple angles, got {angles.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user