mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-15 02:31:54 -04:00
fix(engine): remove only rail clamps and always name them on the sheet
Last-resort removal ran on any clamped stock, but only a rail-following plate can record which clamp to pull and its removal time: an explicit-zone target lost the instruction entirely. NestPlateStock now carries the rail its zones were resolved from (NestStockBuilder, DrawingJobMapper.FromPlate); recovery removes clamps only from rail-backed stock, and commit falls back to the stock's rail when the target plate has none. Runs are now ordered by distance from the origin along the edge, not by zone list order. PM 346d46ed-9336-4706-9392-52ed14d6bd33; PM 6ded8ac4 item (d)
This commit is contained in:
1 parent
3bf63b74f9
commit
af50da7609
7 files changed
+101
-13
No files matched your search
@@ -20,8 +20,14 @@ public class ClampRemovalRecoveryTests
|
||||
private static ClampZone Clamp1() => new("clamp-1", new Box(5.25, 0, 1.5, 0.625));
|
||||
private static ClampZone Clamp2() => new("clamp-2", new Box(20.25, 0, 1.5, 0.625));
|
||||
|
||||
// The machine rail those zones resolve from: only rail-backed clamps can be removed.
|
||||
private static readonly ClampRail Rail = new(1.5, 0.625, new[] { 6.0, 21.0 });
|
||||
|
||||
private static NestPlateStock ClampedSheet(params ClampZone[] zones) =>
|
||||
JobBuilder.Stock("sheet", width: 3, length: 23, clampZones: zones);
|
||||
RailSheet(null, zones);
|
||||
|
||||
private static NestPlateStock RailSheet(int? quantity, params ClampZone[] zones) =>
|
||||
new("sheet", new Size(3, 23), quantity, 0, default, 1, null, zones) { ClampRail = Rail };
|
||||
|
||||
private static readonly RectanglesNestingEngine Engine = new();
|
||||
|
||||
@@ -101,8 +107,7 @@ public class ClampRemovalRecoveryTests
|
||||
// The part fits a fresh clamped sheet on its own; only a single-sheet cap stops the
|
||||
// second copy. That is a capacity limit, not a clamp block, so nothing is removed.
|
||||
var part = JobBuilder.Rectangle("p", w: 12, h: 3, count: 2);
|
||||
var stock = JobBuilder.Stock("sheet", width: 3, length: 23, quantity: 1,
|
||||
clampZones: new[] { Clamp1(), Clamp2() });
|
||||
var stock = RailSheet(1, Clamp1(), Clamp2());
|
||||
var job = JobBuilder.Job(new[] { part }, new[] { stock });
|
||||
|
||||
var raw = Engine.Solve(job);
|
||||
|
||||
@@ -20,8 +20,15 @@ public class ClampRemovalRepairTests
|
||||
new ClampZone("clamp-2", new Box(20.25, 0, 1.5, 0.625)),
|
||||
};
|
||||
|
||||
private static readonly ClampRail Rail = new(1.5, 0.625, new[] { 6.0, 21.0 });
|
||||
|
||||
/// <summary>Stock resolved from the machine rail (as NestStockBuilder builds it).</summary>
|
||||
private static NestPlateStock Stock(int? quantity = null) =>
|
||||
JobBuilder.Stock("sheet", width: 3, length: 23, quantity: quantity, clampZones: Zones());
|
||||
new("sheet", new Size(3, 23), quantity, 0, default, 1, null, Zones()) { ClampRail = Rail };
|
||||
|
||||
/// <summary>The same zones given explicitly, with no rail behind them.</summary>
|
||||
private static NestPlateStock ExplicitStock() =>
|
||||
JobBuilder.Stock("sheet", width: 3, length: 23, clampZones: Zones());
|
||||
|
||||
private static NestItem Item(string name, double w, double h, int quantity) =>
|
||||
new() { Drawing = new Drawing(name, TestDrawingFactory.Rectangle(w, h)), Quantity = quantity };
|
||||
@@ -84,4 +91,57 @@ public class ClampRemovalRepairTests
|
||||
Assert.ThrowsAny<OperationCanceledException>(() =>
|
||||
NestPipeline.Run(new CancelOnSecondCallEngine(source), "cancel", request, token: source.Token));
|
||||
}
|
||||
// PM 346d46ed: explicit zones have no rail to name a removed clamp or its time.
|
||||
[Fact]
|
||||
public void ExplicitZonesWithoutARailAreNeverRemoved()
|
||||
{
|
||||
var result = NestPipeline.Run(Request(ExplicitStock(), Item("wide", 14, 3, 1)));
|
||||
|
||||
Assert.NotEqual(NestJobStatus.Complete, result.Status);
|
||||
Assert.All(result.Job.Plates, s => Assert.DoesNotContain("removed", s.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RailStockRemovalIsNamedOnACommittedPlateWithoutARail()
|
||||
{
|
||||
// The stock comes from a rail-following template; the commit target is an empty
|
||||
// sheet holding the same zones explicitly. The removal must still be recorded.
|
||||
var drawing = new Drawing("wide", TestDrawingFactory.Rectangle(14, 3));
|
||||
var template = new Plate(new Size(3, 23)) { EdgeSpacing = new Spacing(0, 0, 0, 0) };
|
||||
template.SetClampRail(Rail);
|
||||
var nest = new Nest();
|
||||
nest.Drawings.Add(drawing);
|
||||
nest.PlateDefaults.Size = new Size(3, 23);
|
||||
nest.PlateDefaults.EdgeSpacing = new Spacing(0, 0, 0, 0);
|
||||
var target = nest.CreatePlate();
|
||||
foreach (var zone in Zones())
|
||||
target.ClampZones.Add(zone);
|
||||
Assert.Null(target.ClampRail);
|
||||
using var manager = new PlateManager(nest);
|
||||
|
||||
var result = NestPipeline.Run(new NestPipelineRequest("Rectangles",
|
||||
new[] { new NestItem { Drawing = drawing, Quantity = 1 } }, NestStockBuilder.SinglePlate(template)));
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
var plate = Assert.Single(NestPipelineCommit.ApplyToEmptyPlates(result, manager));
|
||||
|
||||
Assert.Equal(new[] { "clamp-1" }, plate.RemovedClamps);
|
||||
Assert.NotNull(plate.ClampRail);
|
||||
Assert.Equal(ClampRail.DefaultRemovalMinutes, plate.ClampRemovalMinutes);
|
||||
}
|
||||
// PM 6ded8ac4 (d): the nearest-origin tie-break follows distance along the edge, not
|
||||
// the order the zones happen to be listed in.
|
||||
[Fact]
|
||||
public void TieBreakPrefersTheClampNearestTheOriginWhateverTheZoneOrder()
|
||||
{
|
||||
var reversed = new NestPlateStock("sheet", new Size(3, 23), null, 0, default, 1, null,
|
||||
Zones().Reverse().ToArray())
|
||||
{ ClampRail = Rail };
|
||||
|
||||
var result = NestPipeline.Run(Request(reversed, Item("wide", 14, 3, 1)));
|
||||
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
var sheet = Assert.Single(result.Raw.Plates);
|
||||
Assert.DoesNotContain(sheet.Stock.ClampZones, z => z.Id == "clamp-1");
|
||||
Assert.Contains(sheet.Stock.ClampZones, z => z.Id == "clamp-2");
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,8 @@ public static class DrawingJobMapper
|
||||
plate.Quadrant,
|
||||
cost: null,
|
||||
clampZones: new List<ClampZone>(plate.ClampZones)
|
||||
);
|
||||
)
|
||||
{ ClampRail = plate.ClampRail };
|
||||
}
|
||||
|
||||
public static Program ToProgram(PartGeometrySnapshot geometry)
|
||||
|
||||
@@ -56,7 +56,7 @@ internal static class ClampRemovalRecovery
|
||||
return null;
|
||||
|
||||
var availableStock = job.Plates.Where(s => s.Quantity != 0).ToList();
|
||||
if (!availableStock.Any(s => s.ClampZones.Count > 0))
|
||||
if (!availableStock.Any(CanRemoveClamps))
|
||||
return null;
|
||||
|
||||
var partsById = job.Parts.ToDictionary(p => p.Id, StringComparer.Ordinal);
|
||||
@@ -153,7 +153,7 @@ internal static class ClampRemovalRecovery
|
||||
Removal best = null;
|
||||
foreach (var stock in stocks)
|
||||
{
|
||||
if (stock.ClampZones.Count == 0)
|
||||
if (!CanRemoveClamps(stock))
|
||||
continue;
|
||||
token.ThrowIfCancellationRequested();
|
||||
var found = MinimalRemovalForStock(part, stock, options, engine, token);
|
||||
@@ -165,10 +165,16 @@ internal static class ClampRemovalRecovery
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>Only clamps resolved from a machine rail can be removed (see
|
||||
/// <see cref="NestPlateStock.ClampRail"/>): explicit zones have no clamp to name.</summary>
|
||||
private static bool CanRemoveClamps(NestPlateStock stock) =>
|
||||
stock.ClampRail != null && stock.ClampZones.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Ascending run size, then ascending start index: the first contiguous run of the
|
||||
/// stock's own clamp zones (already ordered by rail position) whose removal lets the
|
||||
/// part fit alone is both the fewest clamps and, among ties, the run nearest the origin.
|
||||
/// stock's own clamp zones, ordered by distance from the origin along the edge, whose
|
||||
/// removal lets the part fit alone is both the fewest clamps and, among ties, the run
|
||||
/// nearest the origin.
|
||||
/// </summary>
|
||||
private static Removal MinimalRemovalForStock(
|
||||
NestJobPart part,
|
||||
@@ -178,7 +184,11 @@ internal static class ClampRemovalRecovery
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
var zones = stock.ClampZones;
|
||||
// Placement-frame X is signed from the origin, so |centre X| is the distance along
|
||||
// the edge in every quadrant.
|
||||
var zones = stock.ClampZones
|
||||
.OrderBy(z => System.Math.Abs(z.Bounds.X + z.Bounds.Length / 2))
|
||||
.ToList();
|
||||
for (var run = 1; run <= zones.Count; run++)
|
||||
{
|
||||
for (var start = 0; start + run <= zones.Count; start++)
|
||||
@@ -263,7 +273,8 @@ internal static class ClampRemovalRecovery
|
||||
stock.Quadrant,
|
||||
stock.Cost,
|
||||
plateFrameZones
|
||||
);
|
||||
)
|
||||
{ ClampRail = stock.ClampRail };
|
||||
}
|
||||
|
||||
private static string Suffixed(string stockId, IReadOnlyList<string> removedIds) =>
|
||||
|
||||
@@ -71,7 +71,9 @@ public static class NestPipelineCommit
|
||||
if (SameInPlacementFrame(plate.ClampZones, stock.ClampZones, left, bottom))
|
||||
return;
|
||||
|
||||
var rail = plate.ClampRail;
|
||||
// A plate without a rail still takes the rail its stock was resolved from, so a
|
||||
// last-resort removal on that stock is never committed without naming the clamp.
|
||||
var rail = plate.ClampRail ?? stock.ClampRail;
|
||||
plate.SetClampRail(null); // explicit zones from here on
|
||||
foreach (var zone in stock.ClampZones)
|
||||
plate.ClampZones.Add(new ClampZone(zone.Id, zone.Bounds.Translate(-left, -bottom)));
|
||||
|
||||
@@ -81,6 +81,14 @@ public sealed class NestPlateStock
|
||||
/// </summary>
|
||||
internal IReadOnlyList<ClampZone> SheetClampZones { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The machine clamp rail <see cref="ClampZones"/> were resolved from, when the stock was
|
||||
/// built from a rail-following plate; null for explicit zones. Only rail-backed clamps may
|
||||
/// be taken off as a last resort (D8): the rail is what lets a committed sheet name the
|
||||
/// clamps to pull and carry their removal time, so explicit zones are never removed.
|
||||
/// </summary>
|
||||
internal ClampRail ClampRail { get; init; }
|
||||
|
||||
/// <summary>Generic per-physical-sheet cost; null selects area scoring.</summary>
|
||||
public double? Cost { get; }
|
||||
public string Id { get; }
|
||||
|
||||
@@ -33,7 +33,8 @@ public static class NestStockBuilder
|
||||
template.Quadrant,
|
||||
option.Cost == 0 ? null : option.Cost,
|
||||
ClampZonesFor(template, size)
|
||||
));
|
||||
)
|
||||
{ ClampRail = template.ClampRail });
|
||||
}
|
||||
NestJobValidator.ValidateStockCosts(stock);
|
||||
return stock.AsReadOnly();
|
||||
|
||||
Reference in new issue
Block a user