style: apply CSharpier formatting to all C# sources
Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests pass after reformat, full solution builds 0 errors. Added .csharpierignore so csproj/config XML keeps its existing layout (CSharpier's XML wrapping churns attributes with zero benefit). Formatting is now enforceable: dotnet csharpier check . passes.
This commit is contained in:
@@ -13,12 +13,18 @@ namespace OpenNest;
|
||||
public sealed class StockLadderNestingEngine : INestingEngine
|
||||
{
|
||||
private readonly Func<IPlateNester> factory;
|
||||
public StockLadderNestingEngine() : this(() => new OrderedPlateNester()) { }
|
||||
|
||||
public StockLadderNestingEngine()
|
||||
: this(() => new OrderedPlateNester()) { }
|
||||
|
||||
public StockLadderNestingEngine(Func<IPlateNester> factory) =>
|
||||
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
public NestJobResult Solve(
|
||||
NestJob job,
|
||||
IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -33,32 +39,41 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
|
||||
// Probe actual validated single-part placements, not bounding-box fit assertions.
|
||||
foreach (var part in job.Parts)
|
||||
foreach (var stock in job.Plates.Where(s => s.Quantity != 0))
|
||||
{
|
||||
var probe = Trial(stock, new[] { WithQuantity(part, 1) });
|
||||
if (probe.Placements.Count != 0) feasible[part.Id].Add(stock.Id);
|
||||
}
|
||||
var ordered = job.Parts.OrderBy(p => p.Priority)
|
||||
.ThenBy(p => feasible[p.Id].Count).ThenByDescending(p => areas[p.Id]).ToList();
|
||||
foreach (var stock in job.Plates.Where(s => s.Quantity != 0))
|
||||
{
|
||||
var probe = Trial(stock, new[] { WithQuantity(part, 1) });
|
||||
if (probe.Placements.Count != 0)
|
||||
feasible[part.Id].Add(stock.Id);
|
||||
}
|
||||
var ordered = job
|
||||
.Parts.OrderBy(p => p.Priority)
|
||||
.ThenBy(p => feasible[p.Id].Count)
|
||||
.ThenByDescending(p => areas[p.Id])
|
||||
.ToList();
|
||||
var reason = NestJobStopReason.Completed;
|
||||
while (remaining.Values.Any(n => n > 0))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (job.Options.MaxPlates <= sheets.Count)
|
||||
{
|
||||
if (Consolidate()) continue;
|
||||
if (Consolidate())
|
||||
continue;
|
||||
reason = NestJobStopReason.PlateLimitReached;
|
||||
break;
|
||||
}
|
||||
var available = job.Plates.Where(s => s.Quantity == null || used[s.Id] < s.Quantity).ToList();
|
||||
var available = job
|
||||
.Plates.Where(s => s.Quantity == null || used[s.Id] < s.Quantity)
|
||||
.ToList();
|
||||
if (available.Count == 0)
|
||||
{
|
||||
if (Consolidate()) continue;
|
||||
if (Consolidate())
|
||||
continue;
|
||||
reason = NestJobStopReason.StockExhausted;
|
||||
break;
|
||||
}
|
||||
var anchor = ordered.FirstOrDefault(p => remaining[p.Id] > 0 &&
|
||||
available.Any(s => feasible[p.Id].Contains(s.Id)));
|
||||
var anchor = ordered.FirstOrDefault(p =>
|
||||
remaining[p.Id] > 0 && available.Any(s => feasible[p.Id].Contains(s.Id))
|
||||
);
|
||||
if (anchor == null)
|
||||
{
|
||||
reason = NestJobStopReason.NoPlacementFound;
|
||||
@@ -69,14 +84,18 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
foreach (var stock in available.Where(s => feasible[anchor.Id].Contains(s.Id)))
|
||||
{
|
||||
// Pin the constrained anchor before fillers, including quantity-one requirements.
|
||||
var requests = new[] { anchor }.Concat(ordered.Where(p => p.Id != anchor.Id))
|
||||
.Where(p => remaining[p.Id] > 0).Select(p => WithQuantity(p, remaining[p.Id]));
|
||||
var requests = new[] { anchor }
|
||||
.Concat(ordered.Where(p => p.Id != anchor.Id))
|
||||
.Where(p => remaining[p.Id] > 0)
|
||||
.Select(p => WithQuantity(p, remaining[p.Id]));
|
||||
var candidate = Trial(stock, requests);
|
||||
if (!candidate.Placements.Any(p => p.PartId == anchor.Id)) continue;
|
||||
if (!candidate.Placements.Any(p => p.PartId == anchor.Id))
|
||||
continue;
|
||||
var sheet = new NestJobPlateResult(sheets.Count, stock, candidate.Placements);
|
||||
// Initial construction only: material area, never raw part counts. Repacking below
|
||||
// compares EXACTLY equivalent demand, and never replaces a sheet by a partial fill.
|
||||
var value = EstimateNetArea(job, sheet) / candidate.Placements.Sum(p => areas[p.PartId]);
|
||||
var value =
|
||||
EstimateNetArea(job, sheet) / candidate.Placements.Sum(p => areas[p.PartId]);
|
||||
if (value < score - 1e-9)
|
||||
{
|
||||
winner = sheet;
|
||||
@@ -90,28 +109,69 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
}
|
||||
sheets.Add(winner);
|
||||
used[winner.StockId]++;
|
||||
foreach (var pose in winner.Placements) remaining[pose.PartId]--;
|
||||
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, winner.StockId,
|
||||
sheets.Count - 1, sheets.Count, sheets.Sum(s => s.Placements.Count)));
|
||||
foreach (var pose in winner.Placements)
|
||||
remaining[pose.PartId]--;
|
||||
progress?.Report(
|
||||
new NestJobProgress(
|
||||
NestJobStage.PlateCommitted,
|
||||
winner.StockId,
|
||||
sheets.Count - 1,
|
||||
sheets.Count,
|
||||
sheets.Sum(s => s.Placements.Count)
|
||||
)
|
||||
);
|
||||
}
|
||||
Consolidate();
|
||||
token.ThrowIfCancellationRequested();
|
||||
var placed = job.Parts.ToDictionary(p => p.Id, _ => 0);
|
||||
var final = sheets.Select((sheet, index) => new NestJobPlateResult(index, sheet.Stock,
|
||||
sheet.Placements.Select(p => p with { InstanceIndex = placed[p.PartId]++ }).ToList())).ToList();
|
||||
return new NestJobResult(reason == NestJobStopReason.Completed ? NestJobStatus.Complete : NestJobStatus.Incomplete,
|
||||
reason, final, job.Parts.Select(p => new PartFulfillment(p.Id, p.Quantity, placed[p.Id], remaining[p.Id])),
|
||||
job.Plates.Select(s => new StockUsage(s.Id, used[s.Id], s.Quantity - used[s.Id])));
|
||||
var final = sheets
|
||||
.Select(
|
||||
(sheet, index) =>
|
||||
new NestJobPlateResult(
|
||||
index,
|
||||
sheet.Stock,
|
||||
sheet
|
||||
.Placements.Select(p => p with { InstanceIndex = placed[p.PartId]++ })
|
||||
.ToList()
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
return new NestJobResult(
|
||||
reason == NestJobStopReason.Completed
|
||||
? NestJobStatus.Complete
|
||||
: NestJobStatus.Incomplete,
|
||||
reason,
|
||||
final,
|
||||
job.Parts.Select(p => new PartFulfillment(
|
||||
p.Id,
|
||||
p.Quantity,
|
||||
placed[p.Id],
|
||||
remaining[p.Id]
|
||||
)),
|
||||
job.Plates.Select(s => new StockUsage(s.Id, used[s.Id], s.Quantity - used[s.Id]))
|
||||
);
|
||||
|
||||
PlateCandidate Trial(NestPlateStock stock, IEnumerable<NestJobPart> requirements)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var request = new PlatePlacementRequest(stock, requirements);
|
||||
progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id,
|
||||
sheets.Count, sheets.Count, sheets.Sum(s => s.Placements.Count)));
|
||||
progress?.Report(
|
||||
new NestJobProgress(
|
||||
NestJobStage.EvaluatingCandidate,
|
||||
stock.Id,
|
||||
sheets.Count,
|
||||
sheets.Count,
|
||||
sheets.Sum(s => s.Placements.Count)
|
||||
)
|
||||
);
|
||||
var candidate = nester.Place(request, null, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
NestJobValidator.ValidateCandidate(candidate, stock, request.Parts.ToDictionary(p => p.Id, p => p.Quantity), parts);
|
||||
NestJobValidator.ValidateCandidate(
|
||||
candidate,
|
||||
stock,
|
||||
request.Parts.ToDictionary(p => p.Id, p => p.Quantity),
|
||||
parts
|
||||
);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@@ -120,40 +180,55 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
var changed = false;
|
||||
// Single downgrade and adjacent pair merge only: bounded local search, no combinatorial tree.
|
||||
for (var index = 0; index < sheets.Count; index++)
|
||||
for (var count = System.Math.Min(2, sheets.Count - index); count >= 1; count--)
|
||||
for (var count = System.Math.Min(2, sheets.Count - index); count >= 1; count--)
|
||||
{
|
||||
var old = sheets.Skip(index).Take(count).ToList();
|
||||
var demand = old.SelectMany(s => s.Placements)
|
||||
.GroupBy(p => p.PartId)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
var baseline = old.Sum(s => EstimateNetArea(job, s));
|
||||
NestJobPlateResult replacement = null;
|
||||
foreach (var stock in job.Plates)
|
||||
{
|
||||
var old = sheets.Skip(index).Take(count).ToList();
|
||||
var demand = old.SelectMany(s => s.Placements).GroupBy(p => p.PartId)
|
||||
token.ThrowIfCancellationRequested();
|
||||
var returned = old.Count(s => s.StockId == stock.Id);
|
||||
if (stock.Quantity is int limit && used[stock.Id] - returned >= limit)
|
||||
continue;
|
||||
// Even the maximum possible salvage credit cannot beat the incumbent.
|
||||
var lowerBound =
|
||||
stock.Size.Width * stock.Size.Length * (1 - job.Options.SalvageRate);
|
||||
if (lowerBound >= baseline - 1e-9)
|
||||
continue;
|
||||
if (demand.Keys.Any(id => !feasible[id].Contains(stock.Id)))
|
||||
continue;
|
||||
var candidate = Trial(
|
||||
stock,
|
||||
ordered
|
||||
.Where(p => demand.ContainsKey(p.Id))
|
||||
.Select(p => WithQuantity(p, demand[p.Id]))
|
||||
);
|
||||
var actual = candidate
|
||||
.Placements.GroupBy(p => p.PartId)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
var baseline = old.Sum(s => EstimateNetArea(job, s));
|
||||
NestJobPlateResult replacement = null;
|
||||
foreach (var stock in job.Plates)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var returned = old.Count(s => s.StockId == stock.Id);
|
||||
if (stock.Quantity is int limit && used[stock.Id] - returned >= limit) continue;
|
||||
// Even the maximum possible salvage credit cannot beat the incumbent.
|
||||
var lowerBound = stock.Size.Width * stock.Size.Length * (1 - job.Options.SalvageRate);
|
||||
if (lowerBound >= baseline - 1e-9) continue;
|
||||
if (demand.Keys.Any(id => !feasible[id].Contains(stock.Id))) continue;
|
||||
var candidate = Trial(stock, ordered.Where(p => demand.ContainsKey(p.Id))
|
||||
.Select(p => WithQuantity(p, demand[p.Id])));
|
||||
var actual = candidate.Placements.GroupBy(p => p.PartId).ToDictionary(g => g.Key, g => g.Count());
|
||||
if (demand.Any(kv => !actual.TryGetValue(kv.Key, out var n) || n != kv.Value)) continue;
|
||||
var trial = new NestJobPlateResult(index, stock, candidate.Placements);
|
||||
var cost = EstimateNetArea(job, trial);
|
||||
if (cost >= baseline - 1e-9) continue;
|
||||
baseline = cost;
|
||||
replacement = trial;
|
||||
}
|
||||
if (replacement == null) continue;
|
||||
// No accounting changes until the entire equivalent-demand candidate is valid.
|
||||
foreach (var sheet in old) used[sheet.StockId]--;
|
||||
used[replacement.StockId]++;
|
||||
sheets.RemoveRange(index, count);
|
||||
sheets.Insert(index, replacement);
|
||||
changed = true;
|
||||
if (demand.Any(kv => !actual.TryGetValue(kv.Key, out var n) || n != kv.Value))
|
||||
continue;
|
||||
var trial = new NestJobPlateResult(index, stock, candidate.Placements);
|
||||
var cost = EstimateNetArea(job, trial);
|
||||
if (cost >= baseline - 1e-9)
|
||||
continue;
|
||||
baseline = cost;
|
||||
replacement = trial;
|
||||
}
|
||||
if (replacement == null)
|
||||
continue;
|
||||
// No accounting changes until the entire equivalent-demand candidate is valid.
|
||||
foreach (var sheet in old)
|
||||
used[sheet.StockId]--;
|
||||
used[replacement.StockId]++;
|
||||
sheets.RemoveRange(index, count);
|
||||
sheets.Insert(index, replacement);
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -169,27 +244,33 @@ public sealed class StockLadderNestingEngine : INestingEngine
|
||||
{
|
||||
var area = sheet.Stock.Size.Width * sheet.Stock.Size.Length;
|
||||
var minimum = job.Options.MinimumSalvageDimension;
|
||||
if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0) return area;
|
||||
if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0)
|
||||
return area;
|
||||
var work = DrawingJobMapper.CreatePlate(sheet.Stock).WorkArea();
|
||||
var parts = job.Parts.ToDictionary(p => p.Id);
|
||||
var boxes = sheet.Placements.Select(p =>
|
||||
{
|
||||
var part = new Part(DrawingJobMapper.CreateDrawing(parts[p.PartId]));
|
||||
part.Rotate(p.Rotation);
|
||||
part.Location = new OpenNest.Geometry.Vector(p.X, p.Y);
|
||||
part.UpdateBounds();
|
||||
return part.BoundingBox;
|
||||
}).ToList();
|
||||
var boxes = sheet
|
||||
.Placements.Select(p =>
|
||||
{
|
||||
var part = new Part(DrawingJobMapper.CreateDrawing(parts[p.PartId]));
|
||||
part.Rotate(p.Rotation);
|
||||
part.Location = new OpenNest.Geometry.Vector(p.X, p.Y);
|
||||
part.UpdateBounds();
|
||||
return part.BoundingBox;
|
||||
})
|
||||
.ToList();
|
||||
var gap = sheet.Stock.PartSpacing;
|
||||
var candidates = new[]
|
||||
{
|
||||
(work.Length, boxes.Min(b => b.Bottom) - work.Bottom - gap),
|
||||
(work.Length, work.Top - boxes.Max(b => b.Top) - gap),
|
||||
(boxes.Min(b => b.Left) - work.Left - gap, work.Width),
|
||||
(work.Right - boxes.Max(b => b.Right) - gap, work.Width)
|
||||
(work.Right - boxes.Max(b => b.Right) - gap, work.Width),
|
||||
};
|
||||
var salvage = candidates.Where(c => c.Item1 >= minimum && c.Item2 >= minimum)
|
||||
.Select(c => c.Item1 * c.Item2).DefaultIfEmpty(0).Max();
|
||||
var salvage = candidates
|
||||
.Where(c => c.Item1 >= minimum && c.Item2 >= minimum)
|
||||
.Select(c => c.Item1 * c.Item2)
|
||||
.DefaultIfEmpty(0)
|
||||
.Max();
|
||||
return area - job.Options.SalvageRate * salvage;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user