mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-07 10:45:44 -05:00
Slice 3 of common-line cutting. On costly plate (N26-295: 3/16 RA310S at about $2,500 a sheet) pairing parts one kerf apart fits the part the spacing cannot: the last plate's 38 parts fit on one 48x120 with Auto Nest only with pairs. - Drawing Properties gains "Allow common line" (Drawing.AllowCommonLine, off by default because customer edge-quality needs vary). The nest's CommonLineUsage (Off / When it adds parts / Whenever available) is set in Auto Nest and seeded from Tools > Nest Defaults; both persist (nest.json, defaults.json). - NestPipeline runs the ordinary solve, then, with a level on, trials where eligible parts are offered as kerf-gap pair compounds. Placed pairs expand into parts plus explicit NestJobCommonLine groups, are validated again, and CommonLineRanking keeps a trial only by the level: count first, then sheets; "when it adds parts" keeps the plain layout on a tie, "whenever" also takes a shorter cut or less material, never at the cost of a part. Engines, FillScore and their comparers are unchanged; Off asks the engine exactly what it did. - NestLayoutCheck exempts members only through a sheet's explicit group list and only when the job allows common lines; materializing and committing create Plate.CommonLines. - Server nesting writes common-line jobs as version 3 and replays the recorded trial answers, so the desktop binds the same groups. - Fill Area honors the level through a PlateFillService overload. - The benchmark gains --common-lines <level> --kerf <k>. Benchmark (synthetic nests + N26-295, --parallel 1): with no flag and with --common-lines off every engine matches the base commit; with a level on no layout becomes invalid and three runs save a sheet at equal nested area. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
506 lines
23 KiB
C#
506 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.Engine.Jobs.Adapters;
|
|
using OpenNest.Engine.Jobs.CommonLines;
|
|
using OpenNest.Engine.Jobs.Cutouts;
|
|
|
|
namespace OpenNest.Engine.Jobs;
|
|
|
|
/// <summary>
|
|
/// One automatic-nesting request: the named engine, the caller's items (drawing, quantity,
|
|
/// priority, rotation), the stock it may use, and optional job options. Items with a
|
|
/// nonpositive quantity are skipped.
|
|
/// </summary>
|
|
public sealed record NestPipelineRequest(
|
|
string EngineName,
|
|
IReadOnlyList<NestItem> Items,
|
|
IReadOnlyList<NestPlateStock> Stock,
|
|
NestJobOptions Options = null
|
|
);
|
|
|
|
/// <summary>A proposed layout: new parts bound to caller drawings. PlateIndex is the first
|
|
/// raw physical occurrence; Quantity is the total physical copies, including the first.</summary>
|
|
public sealed record ProposedPlate(int PlateIndex, NestPlateStock Stock, IReadOnlyList<Part> Parts)
|
|
{
|
|
/// <summary>Common-line groups among <see cref="Parts"/>; committing the plate adds them to
|
|
/// <see cref="Plate.CommonLines"/>. Empty when the layout has none.</summary>
|
|
public IReadOnlyList<CommonLineGroup> CommonLines { get; init; } = Array.Empty<CommonLineGroup>();
|
|
|
|
private int quantity = 1;
|
|
public int Quantity
|
|
{
|
|
get => quantity;
|
|
init => quantity = value > 0 ? value : throw new ArgumentOutOfRangeException(nameof(Quantity));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The engine's raw result, its independent validation, and the proposed plates. Nothing
|
|
/// has been committed; the caller decides what to do with an invalid result.
|
|
/// </summary>
|
|
public sealed class NestPipelineResult
|
|
{
|
|
internal NestPipelineResult(
|
|
string engineName,
|
|
NestJob job,
|
|
NestJobResult raw,
|
|
IReadOnlyList<ProposedPlate> plates,
|
|
IReadOnlyList<string> violations,
|
|
bool canKeep,
|
|
TimeSpan solveTime,
|
|
TimeSpan validationTime,
|
|
IReadOnlyDictionary<string, Drawing> drawingsByPartId,
|
|
IReadOnlyList<NestUnplacedPart> unplaced
|
|
)
|
|
{
|
|
EngineName = engineName;
|
|
Job = job;
|
|
Raw = raw;
|
|
Plates = plates;
|
|
Violations = violations;
|
|
CanKeep = canKeep;
|
|
SolveTime = solveTime;
|
|
ValidationTime = validationTime;
|
|
DrawingsByPartId = drawingsByPartId;
|
|
Unplaced = unplaced;
|
|
}
|
|
|
|
public string EngineName { get; }
|
|
public NestJob Job { get; }
|
|
public NestJobResult Raw { get; }
|
|
public IReadOnlyList<ProposedPlate> Plates { get; }
|
|
public IReadOnlyList<string> Violations { get; }
|
|
public bool IsValid => Violations.Count == 0;
|
|
/// <summary>True when every placement can be represented, even if layout rules fail.
|
|
/// False for unknown/null requirement IDs or nonfinite poses; Plates is then empty.</summary>
|
|
public bool CanKeep { get; }
|
|
public NestJobStatus Status => Raw.Status;
|
|
public NestJobStopReason StopReason => Raw.StopReason;
|
|
public TimeSpan SolveTime { get; }
|
|
public TimeSpan ValidationTime { get; }
|
|
internal IReadOnlyDictionary<string, Drawing> DrawingsByPartId { get; }
|
|
|
|
/// <summary>Every requested copy this proposal leaves unplaced, by requirement and reason,
|
|
/// counted from the returned placements. Empty when the proposal places every copy; when it
|
|
/// cannot be kept, all demand is listed as <see cref="NestUnplacedReason.ProposalNotKeepable"/>.</summary>
|
|
public IReadOnlyList<NestUnplacedPart> Unplaced { get; }
|
|
|
|
/// <summary>Why a bundled cutout trial was refused before this ordinary solve replaced it;
|
|
/// empty when no trial ran or the trial was not refused. Never part of <see cref="Violations"/>:
|
|
/// the refused trial has no bindable plates.</summary>
|
|
public IReadOnlyList<string> RefusedCutoutTrial { get; private init; } = Array.Empty<string>();
|
|
|
|
internal NestPipelineResult WithRefusedCutoutTrial(IReadOnlyList<string> refusals) =>
|
|
new(EngineName, Job, Raw, Plates, Violations, CanKeep, SolveTime, ValidationTime,
|
|
DrawingsByPartId, Unplaced)
|
|
{ RefusedCutoutTrial = refusals };
|
|
}
|
|
|
|
/// <summary>
|
|
/// The single automatic-nesting path shared by every front end: build the job, solve it
|
|
/// with the named engine, validate the result with the benchmark's rules, then bind
|
|
/// placements to the caller's drawings. The pipeline never mutates caller items, drawings,
|
|
/// or plates, and it does not care which engine is selected.
|
|
/// </summary>
|
|
public static class NestPipeline
|
|
{
|
|
/// <summary>A bundled cutout trial's progress: its commits count proxies, not physical
|
|
/// parts, so only candidates and plans pass, with no committed counts.</summary>
|
|
internal sealed class PreviewProgress(IProgress<NestJobProgress> inner) : IProgress<NestJobProgress>
|
|
{
|
|
public void Report(NestJobProgress value)
|
|
{
|
|
if (value != null && value.Stage is NestJobStage.EvaluatingCandidate or NestJobStage.PlanImproved)
|
|
inner.Report(value with { CommittedPlates = 0, CommittedParts = 0 });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills <see cref="NestJobProgress.PlanSheets"/> on an engine's plan reports. Parts use
|
|
/// detached drawings built from the solved job's geometry (bundled cutout frames included),
|
|
/// one per part, so a preview never touches the caller's drawings or their quantities.
|
|
/// </summary>
|
|
private sealed class PlanProgress(IProgress<NestJobProgress> inner, NestJob engineJob,
|
|
IReadOnlyDictionary<string, Drawing> callerDrawings, CancellationToken token)
|
|
: IProgress<NestJobProgress>
|
|
{
|
|
private readonly Dictionary<string, NestJobPart> parts =
|
|
engineJob.Parts.ToDictionary(p => p.Id, StringComparer.Ordinal);
|
|
private readonly Dictionary<string, Drawing> drawings = new(StringComparer.Ordinal);
|
|
|
|
public void Report(NestJobProgress value)
|
|
{
|
|
if (value?.Stage == NestJobStage.PlanImproved && value.Plan != null && value.PlanSheets == null)
|
|
{
|
|
lock (drawings)
|
|
{
|
|
// A plan naming unknown parts has nothing to draw; validation reports it later.
|
|
if (value.Plan.Plates.Any(s => s.Placements.Any(p => p.PartId == null || !parts.ContainsKey(p.PartId))))
|
|
return;
|
|
value = value with { PlanSheets = PlanSheets(value.Plan, parts, callerDrawings, drawings, token) };
|
|
}
|
|
}
|
|
inner.Report(value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A plan as displayable sheets: identical sheets once with their count, parts on detached
|
|
/// drawings built from <paramref name="parts"/>' geometry and kept in <paramref name="drawings"/>
|
|
/// (one per part id), named and colored like the caller's. Every placement must name one of
|
|
/// <paramref name="parts"/>.
|
|
/// </summary>
|
|
internal static IReadOnlyList<ProposedPlate> PlanSheets(NestJobResult plan,
|
|
IReadOnlyDictionary<string, NestJobPart> parts, IReadOnlyDictionary<string, Drawing> callerDrawings,
|
|
Dictionary<string, Drawing> drawings, CancellationToken token)
|
|
{
|
|
var sheets = new List<ProposedPlate>();
|
|
foreach (var group in RepeatedPlateGrouping.Create(plan.Plates, true, token))
|
|
{
|
|
var placed = new List<Part>(group.Sheet.Placements.Count);
|
|
foreach (var pose in group.Sheet.Placements)
|
|
{
|
|
if (!drawings.TryGetValue(pose.PartId, out var drawing))
|
|
{
|
|
drawing = DrawingJobMapper.CreateDrawing(parts[pose.PartId]);
|
|
// Borrow the caller's name and color for display; bundled frames keep their own.
|
|
if (callerDrawings.TryGetValue(pose.PartId, out var caller))
|
|
{
|
|
drawing.Name = caller.Name;
|
|
drawing.Color = caller.Color;
|
|
}
|
|
drawings[pose.PartId] = drawing;
|
|
}
|
|
// Same pose mapping as NestResultMaterializer: rotate in the snapshot frame, then move.
|
|
var part = new Part(drawing);
|
|
part.Rotate(pose.Rotation);
|
|
part.Location = new Geometry.Vector(pose.X, pose.Y);
|
|
part.UpdateBounds();
|
|
placed.Add(part);
|
|
}
|
|
sheets.Add(new ProposedPlate(group.Sheet.PlateIndex, group.Sheet.Stock, placed)
|
|
{
|
|
Quantity = group.Quantity,
|
|
CommonLines = NestResultBinder.BindCommonLines(group.Sheet, placed),
|
|
});
|
|
}
|
|
return sheets;
|
|
}
|
|
|
|
/// <summary>Resolves <see cref="NestPipelineRequest.EngineName"/> through
|
|
/// <see cref="NestingEngineRegistry"/>; unknown names throw <see cref="NotSupportedException"/>.
|
|
/// Engine cancellation propagates unchanged.</summary>
|
|
public static NestPipelineResult Run(
|
|
NestPipelineRequest request,
|
|
IProgress<NestJobProgress> progress = null,
|
|
CancellationToken token = default
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
var engine = NestingEngineRegistry.Create(request.EngineName);
|
|
return Run(engine, request.EngineName, request, progress, token);
|
|
}
|
|
|
|
/// <summary>Runs a caller-supplied engine instance through the same validation and binding.
|
|
/// Parts that fit other parts' enclosed cutouts are first bundled into their frames
|
|
/// (engine-agnostic); see <see cref="CutoutPipelinePrepass"/>. The engine then solves once.
|
|
/// A bundled trial that fails physical validation, or leaves demand unplaced, is compared
|
|
/// with an ordinary solve of the same job and the better valid proposal is returned.</summary>
|
|
public static NestPipelineResult Run(
|
|
INestingEngine engine,
|
|
string engineName,
|
|
NestPipelineRequest request,
|
|
IProgress<NestJobProgress> progress = null,
|
|
CancellationToken token = default
|
|
) => Run(engine, engineName, request, progress, token, cutouts: true);
|
|
|
|
/// <summary>The ordinary solve alone, with no cutout pre-pass: a characterization baseline
|
|
/// for tests and benchmarks, not a front-end setting.</summary>
|
|
internal static NestPipelineResult RunWithoutCutouts(
|
|
NestPipelineRequest request,
|
|
IProgress<NestJobProgress> progress = null,
|
|
CancellationToken token = default
|
|
) => Run(NestingEngineRegistry.Create(request.EngineName), request.EngineName, request,
|
|
progress, token, cutouts: false);
|
|
|
|
/// <summary>The bundled cutout trial alone, without the ordinary-solve comparison, so tests
|
|
/// can exercise its validation boundary. Jobs without bundles run the ordinary solve.</summary>
|
|
internal static NestPipelineResult RunCutoutTrial(
|
|
INestingEngine engine,
|
|
string engineName,
|
|
NestPipelineRequest request,
|
|
IProgress<NestJobProgress> progress = null,
|
|
CancellationToken token = default
|
|
)
|
|
{
|
|
var (job, drawingsByPartId) = CreateJob(engine, request, token);
|
|
var prepass = CutoutPipelinePrepass.Prepare(job, token);
|
|
var result = Evaluate(engine, engineName, job, prepass, drawingsByPartId, progress, token);
|
|
return prepass != null && result.CanKeep && result.IsValid
|
|
? Committed(result, progress, token) : result;
|
|
}
|
|
|
|
internal static NestPipelineResult RunCutoutTrial(
|
|
NestPipelineRequest request,
|
|
IProgress<NestJobProgress> progress = null,
|
|
CancellationToken token = default
|
|
) => RunCutoutTrial(NestingEngineRegistry.Create(request.EngineName),
|
|
request.EngineName, request, progress, token);
|
|
|
|
private static NestPipelineResult Run(
|
|
INestingEngine engine,
|
|
string engineName,
|
|
NestPipelineRequest request,
|
|
IProgress<NestJobProgress> progress,
|
|
CancellationToken token,
|
|
bool cutouts
|
|
)
|
|
{
|
|
var (job, drawingsByPartId) = CreateJob(engine, request, token);
|
|
return Run(engine, engineName, job, drawingsByPartId, progress, token, cutouts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The pipeline for an already built job whose part ids map to <paramref name="drawingsByPartId"/>
|
|
/// (the benchmark builds its own jobs); identical to <see cref="Run(INestingEngine, string, NestPipelineRequest, IProgress{NestJobProgress}, CancellationToken)"/> otherwise.
|
|
/// </summary>
|
|
internal static NestPipelineResult Run(INestingEngine engine, string engineName, NestJob job,
|
|
IReadOnlyDictionary<string, Drawing> drawingsByPartId, IProgress<NestJobProgress> progress,
|
|
CancellationToken token, bool cutouts = true)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(engine);
|
|
ArgumentNullException.ThrowIfNull(job);
|
|
NestJobValidator.Validate(job);
|
|
var ordinary = RunOrdinary(engine, engineName, job, drawingsByPartId, progress, token, cutouts);
|
|
return job.Options.UsesCommonLines
|
|
? WithCommonLines(engine, engineName, job, drawingsByPartId, ordinary, progress, token)
|
|
: ordinary;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The common-line trials (see <see cref="CommonLinePipelinePrepass"/>), each solved after
|
|
/// the ordinary solve and kept only when <see cref="CommonLineRanking"/> prefers it at the
|
|
/// job's usage level; otherwise the ordinary result stands as it was.
|
|
/// </summary>
|
|
private static NestPipelineResult WithCommonLines(INestingEngine engine, string engineName, NestJob job,
|
|
IReadOnlyDictionary<string, Drawing> drawingsByPartId, NestPipelineResult ordinary,
|
|
IProgress<NestJobProgress> progress, CancellationToken token)
|
|
{
|
|
var best = ordinary;
|
|
var asked = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var share in new[] { CommonLinePipelinePrepass.Share.All, CommonLinePipelinePrepass.Share.Half })
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var prepass = CommonLinePipelinePrepass.Prepare(job, share, token);
|
|
// Two shares that pair the same counts would ask the engine the same question.
|
|
if (prepass == null || !asked.Add(string.Join("|", Remote.NestSolveTranscript.Fingerprint(prepass.EngineJob))))
|
|
continue;
|
|
var trial = Evaluate(engine, engineName, job, prepass, drawingsByPartId, progress, token);
|
|
if (CommonLineRanking.Prefers(job.Options.CommonLineUsage, trial, best))
|
|
best = trial;
|
|
}
|
|
return ReferenceEquals(best, ordinary) ? ordinary : Committed(best, progress, token);
|
|
}
|
|
|
|
private static NestPipelineResult RunOrdinary(INestingEngine engine, string engineName, NestJob job,
|
|
IReadOnlyDictionary<string, Drawing> drawingsByPartId, IProgress<NestJobProgress> progress,
|
|
CancellationToken token, bool cutouts)
|
|
{
|
|
var prepass = cutouts ? CutoutPipelinePrepass.Prepare(job, token) : null;
|
|
if (prepass == null)
|
|
return Evaluate(engine, engineName, job, null, drawingsByPartId, progress, token);
|
|
|
|
var trial = Evaluate(engine, engineName, job, prepass, drawingsByPartId, progress, token);
|
|
var trialKeepable = trial.CanKeep && trial.IsValid;
|
|
if (trialKeepable && trial.Status == NestJobStatus.Complete)
|
|
return Committed(trial, progress, token);
|
|
|
|
// Unplaced frames return their reserved inserts to unplaced demand, and an untrusted
|
|
// composite is never bindable; the ordinary solve is the established baseline.
|
|
var plain = Evaluate(engine, engineName, job, null, drawingsByPartId, progress, token);
|
|
if (!trialKeepable)
|
|
return plain.WithRefusedCutoutTrial(trial.Violations);
|
|
if (plain.CanKeep && plain.IsValid && Placed(plain) > Placed(trial))
|
|
return plain;
|
|
return Committed(trial, progress, token);
|
|
}
|
|
|
|
private static (NestJob Job, Dictionary<string, Drawing> Drawings) CreateJob(
|
|
INestingEngine engine,
|
|
NestPipelineRequest request,
|
|
CancellationToken token
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(engine);
|
|
return CreateJob(request, token);
|
|
}
|
|
|
|
/// <summary>The job the pipeline builds for <paramref name="request"/> (before any cutout
|
|
/// bundling), and the caller drawing behind each part id.</summary>
|
|
internal static (NestJob Job, Dictionary<string, Drawing> Drawings) CreateJob(
|
|
NestPipelineRequest request,
|
|
CancellationToken token
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
ArgumentNullException.ThrowIfNull(request.Items);
|
|
ArgumentNullException.ThrowIfNull(request.Stock);
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
var drawingsByPartId = new Dictionary<string, Drawing>(StringComparer.Ordinal);
|
|
var parts = new List<NestJobPart>();
|
|
for (var i = 0; i < request.Items.Count; i++)
|
|
{
|
|
var item = request.Items[i];
|
|
if (item == null || item.Quantity <= 0)
|
|
continue;
|
|
var partId = $"part-{i}";
|
|
parts.Add(DrawingJobMapper.FromItem(partId, item));
|
|
drawingsByPartId[partId] = item.Drawing;
|
|
}
|
|
|
|
var job = new NestJob(parts, request.Stock, request.Options);
|
|
NestJobValidator.Validate(job);
|
|
return (job, drawingsByPartId);
|
|
}
|
|
|
|
private static int Placed(NestPipelineResult result) =>
|
|
result.Raw.Plates.Sum(p => p.Placements.Count);
|
|
|
|
// Trial progress hid the engine's proxy commit counts; report validated physical ones.
|
|
private static NestPipelineResult Committed(NestPipelineResult result,
|
|
IProgress<NestJobProgress> progress, CancellationToken token)
|
|
{
|
|
var count = 0;
|
|
for (var i = 0; i < result.Raw.Plates.Count; i++)
|
|
{
|
|
count += result.Raw.Plates[i].Placements.Count;
|
|
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted,
|
|
result.Raw.Plates[i].StockId, i, i + 1, count));
|
|
}
|
|
token.ThrowIfCancellationRequested();
|
|
return result;
|
|
}
|
|
|
|
private static NestPipelineResult Evaluate(
|
|
INestingEngine engine,
|
|
string engineName,
|
|
NestJob job,
|
|
IPipelinePrepass prepass,
|
|
IReadOnlyDictionary<string, Drawing> drawingsByPartId,
|
|
IProgress<NestJobProgress> progress,
|
|
CancellationToken token
|
|
)
|
|
{
|
|
var engineJob = prepass?.EngineJob ?? job;
|
|
IReadOnlyDictionary<string, int> released = new Dictionary<string, int>();
|
|
if (prepass != null)
|
|
NestJobValidator.Validate(engineJob);
|
|
|
|
var clock = Stopwatch.StartNew();
|
|
var engineProgress = progress == null ? null
|
|
: new PlanProgress(prepass == null ? progress : new PreviewProgress(progress), engineJob, drawingsByPartId, token);
|
|
var raw =
|
|
engine.Solve(engineJob, engineProgress, token)
|
|
?? throw new InvalidOperationException($"Engine '{engineName}' returned no result.");
|
|
var solveTime = clock.Elapsed;
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
// D8/C10: only the ordinary solve offers last-resort clamp removal; an incomplete
|
|
// cutout trial is compared with that solve instead. No-op whenever nothing is blocked.
|
|
if (prepass == null)
|
|
{
|
|
var recovered = ClampRemovalRecovery.TryRecover(job, raw, engine, token);
|
|
if (recovered != null)
|
|
{
|
|
job = recovered.Job;
|
|
engineJob = recovered.Job;
|
|
raw = recovered.Result;
|
|
}
|
|
}
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
clock.Restart();
|
|
var names = drawingsByPartId.ToDictionary(
|
|
kv => kv.Key,
|
|
kv => kv.Value.Name ?? kv.Key,
|
|
StringComparer.Ordinal
|
|
);
|
|
var checkedNames = prepass == null ? names : engineJob.Parts.ToDictionary(p => p.Id,
|
|
p => names.GetValueOrDefault(p.Id, p.Id), StringComparer.Ordinal);
|
|
var violations = NestLayoutCheck.Violations(engineJob, raw, checkedNames, out var canKeep).ToList();
|
|
if (prepass != null)
|
|
{
|
|
// A compound may be physically invalid even when its envelope is legal.
|
|
// Refuse every unverified trial, including with explicit invalid-result consent.
|
|
var failure = prepass.ReconciliationFailure;
|
|
if (violations.Count == 0 && prepass.TryExpand(job, raw, token, out var expanded, out failure))
|
|
{
|
|
released = prepass.ReleasedInserts(raw);
|
|
raw = expanded;
|
|
violations.AddRange(NestLayoutCheck.Violations(job, raw, names, out var physicalKeep));
|
|
canKeep = physicalKeep && violations.Count == 0;
|
|
}
|
|
else
|
|
{
|
|
if (violations.Count == 0)
|
|
violations.Add(failure);
|
|
canKeep = false;
|
|
}
|
|
}
|
|
var freshness = NestPipelineDrawingFreshness.Changes(job, drawingsByPartId);
|
|
violations.AddRange(freshness);
|
|
if (freshness.Count > 0)
|
|
canKeep = false;
|
|
var validationTime = clock.Elapsed;
|
|
if (canKeep && violations.Count == 0)
|
|
_ = NestJobCost.Evaluate(job, raw);
|
|
|
|
// Invalid-but-representable output retains its physical review correspondence.
|
|
var consolidate = violations.Count == 0;
|
|
var plates = new List<ProposedPlate>();
|
|
if (canKeep)
|
|
foreach (var group in RepeatedPlateGrouping.Create(raw.Plates, consolidate, token))
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var bound = NestResultBinder.Bind(group.Sheet, drawingsByPartId);
|
|
plates.Add(new ProposedPlate(group.Sheet.PlateIndex, group.Sheet.Stock, bound)
|
|
{
|
|
Quantity = group.Quantity,
|
|
CommonLines = NestResultBinder.BindCommonLines(group.Sheet, bound),
|
|
});
|
|
}
|
|
|
|
// Binding clones the caller's current Program; a concurrent edit during binding
|
|
// must not return parts whose bytes differ from the already validated snapshot.
|
|
if (canKeep)
|
|
{
|
|
var changes = NestPipelineDrawingFreshness.Changes(job, drawingsByPartId).ToList();
|
|
changes.AddRange(NestPipelineDrawingFreshness.BoundChanges(job, raw, plates, drawingsByPartId, consolidate, token));
|
|
if (changes.Count > 0)
|
|
{
|
|
violations.AddRange(changes);
|
|
plates.Clear();
|
|
canKeep = false;
|
|
}
|
|
}
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
return new NestPipelineResult(
|
|
engineName,
|
|
job,
|
|
raw,
|
|
plates.AsReadOnly(),
|
|
violations,
|
|
canKeep,
|
|
solveTime,
|
|
validationTime,
|
|
drawingsByPartId,
|
|
NestUnplacedParts.Classify(job, raw, canKeep, drawingsByPartId, released)
|
|
);
|
|
}
|
|
}
|