mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-16 03:11:37 -04:00
test(expand): verify cancellation after scratch movement
This commit is contained in:
1 parent
fdc6f074ee
commit
73be301ebf
2 files changed
+149
-15
No files matched your search
@@ -579,6 +579,50 @@ public class ExpanderTests
|
||||
Assert.True(poses[1].X + 2 <= wall.Location.X + 1e-6);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData((int)Expander.Checkpoint.AfterScratchMove)]
|
||||
[InlineData((int)Expander.Checkpoint.AfterSuccessfulProbe)]
|
||||
[InlineData((int)Expander.Checkpoint.BeforeApply)]
|
||||
public void Expand_CancellationAtDeterministicCheckpoint_LeavesLiveStateBitIdentical(int stage)
|
||||
{
|
||||
var cancelAt = (Expander.Checkpoint)stage;
|
||||
var plate = MakePlate(30, 12, edge: 0);
|
||||
var anchor = AddSquare(plate, 5, 4);
|
||||
var moving = AddSquare(plate, 10, 4);
|
||||
var wall = AddSquare(plate, 0, 4);
|
||||
var before = plate.Parts.Select(p => (
|
||||
Part: p, Program: p.Program,
|
||||
X: BitConverter.DoubleToInt64Bits(p.Location.X),
|
||||
Y: BitConverter.DoubleToInt64Bits(p.Location.Y),
|
||||
Left: BitConverter.DoubleToInt64Bits(p.BoundingBox.Left))).ToArray();
|
||||
var partSpacing = plate.PartSpacing;
|
||||
var edgeSpacing = plate.EdgeSpacing;
|
||||
using var cts = new CancellationTokenSource();
|
||||
var seen = false;
|
||||
|
||||
var result = Expander.ExpandObserved(new List<Part> { anchor, moving }, plate,
|
||||
new Expander.Options { MaxSpacing = 3 }, checkpoint =>
|
||||
{
|
||||
if (checkpoint != cancelAt)
|
||||
return;
|
||||
seen = true;
|
||||
cts.Cancel();
|
||||
}, cts.Token);
|
||||
|
||||
Assert.True(seen, $"checkpoint {cancelAt} was not reached");
|
||||
Assert.True(result.Cancelled);
|
||||
foreach (var state in before)
|
||||
{
|
||||
Assert.Same(state.Program, state.Part.Program);
|
||||
Assert.Equal(state.X, BitConverter.DoubleToInt64Bits(state.Part.Location.X));
|
||||
Assert.Equal(state.Y, BitConverter.DoubleToInt64Bits(state.Part.Location.Y));
|
||||
Assert.Equal(state.Left, BitConverter.DoubleToInt64Bits(state.Part.BoundingBox.Left));
|
||||
}
|
||||
Assert.Equal(partSpacing, plate.PartSpacing);
|
||||
Assert.Equal(edgeSpacing, plate.EdgeSpacing);
|
||||
Assert.Equal(0, wall.Location.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Expand_WallsAndUnselectedPairs_StayExactlyAtClearance()
|
||||
{
|
||||
|
||||
@@ -71,6 +71,19 @@ namespace OpenNest.Engine.Fill
|
||||
public List<Violation> Violations = new();
|
||||
}
|
||||
|
||||
/// <summary>Per-call test checkpoints; no process-global mutable hook.</summary>
|
||||
internal enum Checkpoint
|
||||
{
|
||||
AfterScratchMove,
|
||||
AfterSuccessfulProbe,
|
||||
BeforeApply,
|
||||
}
|
||||
|
||||
internal static Result ExpandObserved(
|
||||
List<Part> selected, Plate plate, Options options,
|
||||
Action<Checkpoint> observer, CancellationToken token = default
|
||||
) => ExpandCore(selected, plate, options, token, observer);
|
||||
|
||||
/// <summary>
|
||||
/// Raises the part-to-part spacing of the selection as far as the plate
|
||||
/// and its other parts allow, applying the best spacing found. Plate
|
||||
@@ -86,6 +99,14 @@ namespace OpenNest.Engine.Fill
|
||||
Plate plate,
|
||||
Options options = null,
|
||||
CancellationToken token = default
|
||||
) => ExpandCore(selected, plate, options, token, null);
|
||||
|
||||
private static Result ExpandCore(
|
||||
List<Part> selected,
|
||||
Plate plate,
|
||||
Options options,
|
||||
CancellationToken token,
|
||||
Action<Checkpoint> observer
|
||||
)
|
||||
{
|
||||
if (plate == null)
|
||||
@@ -102,11 +123,29 @@ namespace OpenNest.Engine.Fill
|
||||
);
|
||||
|
||||
var opt = options ?? new Options();
|
||||
var context = SeparationContext.Prepare(selected, plate);
|
||||
var entry = context.Positions(); // all parts; movers are [0, count)
|
||||
var result = new Result();
|
||||
SeparationContext context;
|
||||
try
|
||||
{
|
||||
context = SeparationContext.Prepare(selected, plate, token);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
return result;
|
||||
}
|
||||
var entry = context.Positions(); // all parts; movers are [0, count)
|
||||
|
||||
var sMin = MinimumPairClearance(context, entry);
|
||||
double sMin;
|
||||
try
|
||||
{
|
||||
sMin = MinimumPairClearance(context, entry, token);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
return result;
|
||||
}
|
||||
result.AchievedSpacing = System.Math.Max(0, sMin);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
@@ -121,7 +160,9 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
(bool Converged, List<Vector> Positions) Probe(double spacing)
|
||||
{
|
||||
var attempt = Separate(context, entry, spacing, opt.MaxIterations, token);
|
||||
var attempt = Separate(context, entry, spacing, opt.MaxIterations, token, observer);
|
||||
if (attempt.Converged)
|
||||
observer?.Invoke(Checkpoint.AfterSuccessfulProbe);
|
||||
return (attempt.Converged, attempt.Positions);
|
||||
}
|
||||
|
||||
@@ -151,6 +192,11 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
var probe = Probe(s);
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (probe.Converged)
|
||||
{
|
||||
@@ -179,7 +225,13 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
|
||||
var mid = (lo + hi) / 2;
|
||||
if (Probe(mid).Converged)
|
||||
var midProbe = Probe(mid);
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
return result;
|
||||
}
|
||||
if (midProbe.Converged)
|
||||
lo = mid;
|
||||
else
|
||||
hi = mid;
|
||||
@@ -191,8 +243,9 @@ namespace OpenNest.Engine.Fill
|
||||
// Deterministic, so this reproduces any committed probe exactly; when
|
||||
// sGood was never probed (overlapping entry) it still performs the
|
||||
// best-effort cleanup and yields the violation report.
|
||||
var final = Separate(context, entry, System.Math.Max(0, sGood), opt.MaxIterations, token);
|
||||
var final = Separate(context, entry, System.Math.Max(0, sGood), opt.MaxIterations, token, observer);
|
||||
|
||||
observer?.Invoke(Checkpoint.BeforeApply);
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
@@ -205,9 +258,23 @@ namespace OpenNest.Engine.Fill
|
||||
if (!context.MeetsEntryFloors(positions, entry))
|
||||
throw new InvalidOperationException("Expand proposal violates an entry clearance floor.");
|
||||
|
||||
double measured;
|
||||
try
|
||||
{
|
||||
measured = MinimumPairClearance(context, positions, token);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
return result;
|
||||
}
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
return result;
|
||||
}
|
||||
Apply(selected, entry, positions);
|
||||
|
||||
var measured = MinimumPairClearance(context, positions);
|
||||
result.AchievedSpacing = System.Math.Max(0, System.Math.Min(System.Math.Max(0, sGood), measured));
|
||||
result.Violations = final.Violations;
|
||||
return result;
|
||||
@@ -226,9 +293,17 @@ namespace OpenNest.Engine.Fill
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
var context = SeparationContext.Prepare(selected, plate);
|
||||
SeparationContext context;
|
||||
try
|
||||
{
|
||||
context = SeparationContext.Prepare(selected, plate, token);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
return (false, selected.Select(p => p.Location).ToList(), new List<Violation>());
|
||||
}
|
||||
var positions = context.Positions();
|
||||
return Separate(context, positions, spacing, maxIterations, token);
|
||||
return Separate(context, positions, spacing, maxIterations, token, null);
|
||||
}
|
||||
|
||||
private static void Apply(List<Part> selected, List<Vector> from, List<Vector> to)
|
||||
@@ -242,12 +317,14 @@ namespace OpenNest.Engine.Fill
|
||||
}
|
||||
}
|
||||
|
||||
private static double MinimumPairClearance(SeparationContext context, List<Vector> positions)
|
||||
private static double MinimumPairClearance(
|
||||
SeparationContext context, List<Vector> positions, CancellationToken token)
|
||||
{
|
||||
var min = double.MaxValue;
|
||||
|
||||
foreach (var pair in context.Pairs)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var (distance, _) = context.PairClearance(pair.IndexA, pair.IndexB, positions);
|
||||
if (distance < min)
|
||||
min = distance;
|
||||
@@ -261,7 +338,8 @@ namespace OpenNest.Engine.Fill
|
||||
List<Vector> start,
|
||||
double spacing,
|
||||
int maxIterations,
|
||||
CancellationToken token
|
||||
CancellationToken token,
|
||||
Action<Checkpoint> observer
|
||||
)
|
||||
{
|
||||
var positions = new List<Vector>(start);
|
||||
@@ -285,7 +363,8 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
foreach (var pair in context.Pairs)
|
||||
{
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return (false, start, violations);
|
||||
var (distance, directionA) = context.PairClearance(
|
||||
pair.IndexA,
|
||||
pair.IndexB,
|
||||
@@ -330,6 +409,9 @@ namespace OpenNest.Engine.Fill
|
||||
{
|
||||
positions[moverIndex] = attempt.Proposal;
|
||||
moved += applied;
|
||||
observer?.Invoke(Checkpoint.AfterScratchMove);
|
||||
if (token.IsCancellationRequested)
|
||||
return (false, start, violations);
|
||||
}
|
||||
|
||||
// A pair fully separated to the user target (the internal margin
|
||||
@@ -364,6 +446,8 @@ namespace OpenNest.Engine.Fill
|
||||
violations.Clear();
|
||||
foreach (var pair in context.Pairs)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return (false, start, violations);
|
||||
var (distance, directionA) = context.PairClearance(
|
||||
pair.IndexA,
|
||||
pair.IndexB,
|
||||
@@ -491,8 +575,9 @@ namespace OpenNest.Engine.Fill
|
||||
SpacingCap = spacingCap;
|
||||
}
|
||||
|
||||
public static SeparationContext Prepare(List<Part> selected, Plate plate)
|
||||
public static SeparationContext Prepare(List<Part> selected, Plate plate, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var movers = new List<Part>(selected);
|
||||
var walls = plate.Parts.Where(p => !movers.Contains(p)).ToList();
|
||||
var clampStart = movers.Count + walls.Count;
|
||||
@@ -514,6 +599,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
shapes.Add(PrepareProgram(programs, parts[i].Program));
|
||||
localBoxes[i] = LocalBox(parts[i]);
|
||||
}
|
||||
@@ -533,10 +619,13 @@ namespace OpenNest.Engine.Fill
|
||||
workArea,
|
||||
spacingCap
|
||||
);
|
||||
context.BuildPairs();
|
||||
context.BuildPairs(token);
|
||||
var entry = context.Positions();
|
||||
foreach (var pair in context.Pairs)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
pair.EntryFloor = context.PairClearance(pair.IndexA, pair.IndexB, entry).Distance;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -609,10 +698,11 @@ namespace OpenNest.Engine.Fill
|
||||
);
|
||||
}
|
||||
|
||||
private void BuildPairs()
|
||||
private void BuildPairs(CancellationToken token)
|
||||
{
|
||||
for (var a = 0; a < parts.Count; a++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
for (var b = a + 1; b < parts.Count; b++)
|
||||
{
|
||||
var aMover = a < moverCount;
|
||||
|
||||
Reference in new issue
Block a user