fix(engine): treat a part as clamp-blocked only when no lane can place it

Last-resort removal treated the selected engine's failure to place a lone
part as proof it could not fit with the clamps in place. Rectangles packs
bounding boxes, so a concave part whose material clears a clamp lost that
clamp for nothing (C10). A part now counts as blocked only when the caller's
engine, Irregular (true material, NFP) and Rectangles all fail on every
offered stock; Default already runs both lanes.

PM c5984c6e-3882-4d15-8a42-8de76b4c2080
This commit is contained in:
aj committed 2026-10-13 18:15:49 -04:00
1 parent af50da7609
commit f19a825c4e
2 files changed
+57 -2

No files matched your search

@@ -1,4 +1,5 @@
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Engine.NestingEngines.Rectangles;
using OpenNest.Engine.Tests.NestingEngines;
using OpenNest.Geometry;
@@ -144,4 +145,39 @@ public class ClampRemovalRepairTests
Assert.DoesNotContain(sheet.Stock.ClampZones, z => z.Id == "clamp-1");
Assert.Contains(sheet.Stock.ClampZones, z => z.Id == "clamp-2");
}
// PM c5984c6e (C10): one lane's failure to place is not proof the part cannot fit. The
// fixed-rotation L below fits at the origin with its notch over the clamp (an explicit
// witness passes the layout check), but Rectangles packs its 10 x 10 bounding box and
// cannot place it; no clamp may be removed for it.
[Fact]
public void PartThatFitsClampedInSomeLaneNeverRemovesAClamp()
{
var program = new OpenNest.CNC.Program();
program.MoveTo(0, 0);
program.LineTo(2, 0);
program.LineTo(2, 8);
program.LineTo(10, 8);
program.LineTo(10, 10);
program.LineTo(0, 10);
program.LineTo(0, 0);
var rail = new ClampRail(1.5, 0.625, new[] { 5.75 }); // clamp-1 spans X [5, 6.5]
var size = new Size(10, 10);
var stock = new NestPlateStock("sheet", size, null, 0, default, 1, null, rail.ResolveFor(size, 1))
{ ClampRail = rail };
var drawing = new Drawing("notched", program);
var witnessJob = new NestJob(new[] { DrawingJobMapper.FromItem("p", new NestItem { Drawing = drawing, Quantity = 1 }) },
new[] { stock });
var witness = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
new[] { new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("p", 0, 0, 0, 0) }) },
Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>());
Assert.Empty(NestLayoutCheck.Violations(witnessJob, witness)); // fits with the clamp in place
var result = NestPipeline.Run(new NestPipelineRequest("Rectangles",
new[] { new NestItem { Drawing = drawing, Quantity = 1 } },
new[] { stock }));
Assert.All(result.Job.Plates, s => Assert.DoesNotContain("removed", s.Id));
Assert.All(result.Raw.Plates, sheet => Assert.Single(sheet.Stock.ClampZones));
}
}
+21 -2
View File
@@ -69,9 +69,11 @@ internal static class ClampRemovalRecovery
token.ThrowIfCancellationRequested();
// C10: only a part that fits nowhere with every clamp in place is "blocked".
// C10: only a part that fits nowhere with every clamp in place is "blocked". One
// lane's failure is not proof: a part any built-in lane can place clamped is not.
var lanes = FeasibilityLanes(engine);
var blocked = remaining
.Where(x => !FitsAnywhere(x.Part, availableStock, job.Options, engine, token))
.Where(x => !lanes.Any(lane => FitsAnywhere(x.Part, availableStock, job.Options, lane, token)))
.Select(x => x.Part)
.ToList();
if (blocked.Count == 0)
@@ -131,6 +133,23 @@ internal static class ClampRemovalRecovery
return merged;
}
/// <summary>
/// Engines whose placement of a lone part proves it fits (C10): the caller's own engine,
/// plus the built-in Irregular (NFP, true material) and Rectangles lanes. Default already
/// runs both, so it is enough on its own.
/// </summary>
private static IReadOnlyList<INestingEngine> FeasibilityLanes(INestingEngine engine)
{
if (engine is NestingEngines.Default.DefaultNestingEngine)
return new[] { engine };
var lanes = new List<INestingEngine> { engine };
if (engine is not NestingEngines.Irregular.IrregularNestingEngine)
lanes.Add(new NestingEngines.Irregular.IrregularNestingEngine());
if (engine is not NestingEngines.Rectangles.RectanglesNestingEngine)
lanes.Add(new NestingEngines.Rectangles.RectanglesNestingEngine());
return lanes;
}
/// <summary>Same stock, quantity reduced by what phase 1 already used (unlimited stays
/// unlimited); id/zones unchanged. Used for stock phase 2 may still draw from.</summary>
private static NestPlateStock ResidualCopy(NestPlateStock stock, IReadOnlyDictionary<string, int> usedByStock) =>