mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-05 05:51:34 -05:00
Equal-cost plans break ties by the largest edge offcut, from any of the four edges. A caller that wants the leftover off one way, such as a shop that keeps only end drops, or a benchmark scoring the used sheet height, had no way to say so. On the converted Nestbolt jobs the extended Irregular search then found plans whose larger offcut is a strip down the side, which Nestbolt scores as worse: b-blaz1-strip 3.601 -> 3.867, b-shirts-strip 2.674 -> 2.995. NestJobOptions.OffcutCut (a CutOffAxis, default null) counts only the strips one cut along that axis frees, for salvage credit and the compactness tie-break alike: Horizontal the full-width bottom and top strips, Vertical the full-length sides. PlateRemnant.LargestOffcut takes the axis; NestJobCost, the time limit and clamp-removal recovery pass it on. Unset, nothing changes. Benchmark manifests accept "offcutCut": "horizontal" or "vertical". With horizontal, one CPU, 20 s limit, Default: b-blaz1-strip 3.934 -> 3.333 b-shirts-strip 2.995 -> 2.610 m-box-deep 1.918 -> 1.276 (Nestbolt 1.251) Manifests accept only the words horizontal or vertical (any case); a list such as "horizontal,vertical" is rejected. PlateRemnant rejects an undefined axis. Fixed strategies keep their own no-salvage tie-break and ignore the option; desktop, Console and MCP do not offer it yet. Mutation reds: 14/14 (offcut-mutations.py).
136 lines
6.8 KiB
C#
136 lines
6.8 KiB
C#
using System;
|
|
using System.Linq;
|
|
using OpenNest.Engine.Jobs.Adapters;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Jobs;
|
|
|
|
/// <summary>Generic per-sheet scoring; unspecified costs retain legacy area scores.</summary>
|
|
public static class NestJobCost
|
|
{
|
|
/// <summary>
|
|
/// Sheet area less SalvageRate times the largest usable full-width/full-length edge offcut.
|
|
/// Both offcut dimensions must meet MinimumSalvageDimension, which must be positive.
|
|
/// Bounds use rotated material contours only; scribe/etch marks never shrink salvage offcuts.
|
|
/// </summary>
|
|
public static double NetSheetArea(NestJob job, NestJobPlateResult sheet)
|
|
{
|
|
var area = sheet.Stock.Area;
|
|
var minimum = job.Options.MinimumSalvageDimension;
|
|
if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0)
|
|
return area;
|
|
var (left, bottom, right, top) = MaterialEnvelope(job, sheet);
|
|
return NetSheetArea(job.Options, sheet.Stock, left, bottom, right, top);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tie-break for layouts of equal cost: the largest full-width/full-length edge offcut, as
|
|
/// salvage measures it but whatever the salvage rate. Offcuts must meet a positive
|
|
/// MinimumSalvageDimension; with none, any offcut counts. Larger is more compact. An empty
|
|
/// sheet has none. Never part of the cost, so it cannot outrank a cheaper layout.
|
|
/// </summary>
|
|
public static double CompactnessOffcut(NestJob job, NestJobPlateResult sheet)
|
|
{
|
|
if (sheet.Placements.Count == 0)
|
|
return 0;
|
|
var (left, bottom, right, top) = MaterialEnvelope(job, sheet);
|
|
return LargestOffcut(sheet.Stock, left, bottom, right, top,
|
|
System.Math.Max(0, job.Options.MinimumSalvageDimension), job.Options.OffcutCut);
|
|
}
|
|
|
|
/// <summary><see cref="CompactnessOffcut(NestJob, NestJobPlateResult)"/> from an existing
|
|
/// material-only placed-parts envelope.</summary>
|
|
public static double CompactnessOffcut(NestJobOptions options, NestPlateStock stock, Box partsEnvelope) =>
|
|
LargestOffcut(stock, partsEnvelope.Left, partsEnvelope.Bottom, partsEnvelope.Right, partsEnvelope.Top,
|
|
System.Math.Max(0, options.MinimumSalvageDimension), options.OffcutCut);
|
|
|
|
/// <summary>
|
|
/// An offcut on a 1e-6 grid, for exact tie-break comparisons: differences below it are
|
|
/// envelope rounding, which must not decide a tie differently across platform math libraries.
|
|
/// </summary>
|
|
public static double TieBreakKey(double offcut) => System.Math.Round(offcut, 6);
|
|
|
|
/// <summary>Whole-job <see cref="CompactnessOffcut(NestJob, NestJobPlateResult)"/>, summed over sheets.</summary>
|
|
public static double CompactnessOffcut(NestJob job, NestJobResult result) =>
|
|
RequireFinite(result.Plates.Sum(sheet => CompactnessOffcut(job, sheet)));
|
|
|
|
/// <summary>Bounds of the sheet's rotated material contours; scribe/etch marks are excluded.</summary>
|
|
private static (double Left, double Bottom, double Right, double Top) MaterialEnvelope(
|
|
NestJob job, NestJobPlateResult sheet)
|
|
{
|
|
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 NestLayoutCheck.MaterialBounds(part);
|
|
})
|
|
.ToList();
|
|
return (boxes.Min(b => b.Left), boxes.Min(b => b.Bottom), boxes.Max(b => b.Right), boxes.Max(b => b.Top));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes net area from an existing placed-parts envelope without rebuilding geometry.
|
|
/// The envelope must contain material only; exclude scribe/etch marks.
|
|
/// Empty sheets should use the sheet overload, which returns their full area.
|
|
/// </summary>
|
|
public static double NetSheetArea(NestJobOptions options, NestPlateStock stock, Box partsEnvelope) =>
|
|
NetSheetArea(options, stock, partsEnvelope.Left, partsEnvelope.Bottom,
|
|
partsEnvelope.Right, partsEnvelope.Top);
|
|
|
|
private static double NetSheetArea(
|
|
NestJobOptions options, NestPlateStock stock, double left, double bottom, double right, double top)
|
|
{
|
|
var area = stock.Area;
|
|
var minimum = options.MinimumSalvageDimension;
|
|
if (options.SalvageRate == 0 || minimum <= 0)
|
|
return area;
|
|
return area - options.SalvageRate
|
|
* LargestOffcut(stock, left, bottom, right, top, minimum, options.OffcutCut);
|
|
}
|
|
|
|
// No rectangular edge strip is guaranteed inside an outlined sheet, so it earns no credit.
|
|
private static double LargestOffcut(
|
|
NestPlateStock stock, double left, double bottom, double right, double top, double minimum,
|
|
CutOffAxis? cut) =>
|
|
stock.Outline == null
|
|
&& PlateRemnant.LargestOffcut(stock.WorkArea, stock.PartSpacing,
|
|
left, bottom, right, top, minimum, cut)
|
|
is { } offcut ? offcut.Length * offcut.Width : 0;
|
|
|
|
public static bool UsesExplicitCosts(NestJob job) =>
|
|
job.Plates.Any(s => s.Quantity != 0 && s.Cost.HasValue);
|
|
|
|
public static double RequireFinite(double value) => double.IsFinite(value) ? value
|
|
: throw new OverflowException("Stock cost arithmetic exceeded the finite scoring range.");
|
|
|
|
public static double GrossSheetCost(NestPlateStock stock) => RequireFinite(stock.Cost ?? stock.Area);
|
|
|
|
public static double NetSheetCost(NestJob job, NestJobPlateResult sheet) =>
|
|
PriceArea(sheet.Stock, NetSheetArea(job, sheet));
|
|
|
|
public static double NetSheetCost(NestJobOptions options, NestPlateStock stock, Box envelope) =>
|
|
PriceArea(stock, NetSheetArea(options, stock, envelope));
|
|
|
|
private static double PriceArea(NestPlateStock stock, double area) =>
|
|
RequireFinite(stock.Cost is double cost ? cost * (area / stock.Area) : area);
|
|
|
|
/// <summary>Largest offered sheet score, charged per unplaced part.</summary>
|
|
public static double UnplacedPartPenalty(NestJob job) =>
|
|
job.Plates.Count == 0 ? 0 : UsesExplicitCosts(job)
|
|
? job.Plates.Where(s => s.Quantity != 0).Max(GrossSheetCost)
|
|
: job.Plates.Max(s => RequireFinite(s.Area));
|
|
|
|
/// <summary>
|
|
/// Sum of net sheet costs plus unplaced quantity times the largest offered sheet score.
|
|
/// Matches the benchmark for a valid run; this method does not validate the result.
|
|
/// </summary>
|
|
public static double Evaluate(NestJob job, NestJobResult result) =>
|
|
RequireFinite(result.Plates.Sum(sheet => NetSheetCost(job, sheet))
|
|
+ System.Math.Max(0, job.Parts.Sum(part => part.Quantity)
|
|
- result.Plates.Sum(sheet => sheet.Placements.Count)) * UnplacedPartPenalty(job));
|
|
}
|