#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Runtime.ExceptionServices; using System.Threading; using OpenNest.Engine.Jobs; using OpenNest.Engine.NestingEngines.Circles; using OpenNest.Engine.NestingEngines.Irregular; using OpenNest.Engine.NestingEngines.Rectangles; namespace OpenNest.Engine.NestingEngines.Default; /// /// The engine used when the caller names none. It runs each candidate engine on the whole job, /// checks every result with and returns the best one: a valid /// layout before an invalid one, then the fewest unplaced parts, then the lowest /// ; remaining ties keep candidate order. /// /// Candidates are Irregular, then Rectangles, then Circles. Neither of the first two wins every /// job: Rectangles packs each part as its box, which is often best for plain plates and is a /// cheap, valid fallback for the rest. Circles bids only when every part is round, and otherwise /// returns everything unplaced at once. /// A candidate that throws or returns no result is skipped; when every candidate throws, the /// exception of the first in tie-break order is rethrown. Cancellation stops the search. /// Candidates' plate commits are not forwarded; the chosen result's commits are reported once it /// is selected. /// /// Candidates' whole-job plans () are scored the same way /// and reported only when one beats every plan reported so far, so a later candidate never shows a /// worse plan after a better one. A candidate result that beats them is reported too, and the /// chosen result is always the last plan reported. /// /// Rectangles, the quick and always-valid fallback, runs first, then Circles, then Irregular, so /// a caller that stops the search early, such as , already /// holds a valid plan, and Irregular can search until cancelled /// () last. The choice is made after all have /// run, in candidate (tie-break) order, so the run order never changes it. /// /// Deterministic: candidates run one after another and the choice uses no clock. /// public sealed class DefaultNestingEngine : INestingEngine { private readonly IReadOnlyList> candidates; private readonly IReadOnlyList runOrder; public DefaultNestingEngine() : this( new Func[] { () => new IrregularNestingEngine(), () => new RectanglesNestingEngine(), () => new CirclesNestingEngine(), }, new[] { 1, 2, 0 }) { } /// Test seam: candidate engines in tie-break order, run in that order. internal DefaultNestingEngine(IReadOnlyList> candidates) : this(candidates, Enumerable.Range(0, candidates?.Count ?? 0).ToArray()) { } /// Test seam: candidate engines in tie-break order, and the order to run them in /// (a permutation of their indexes); the run order affects progress, never the choice. /// internal DefaultNestingEngine( IReadOnlyList> candidates, IReadOnlyList runOrder ) { ArgumentNullException.ThrowIfNull(candidates); ArgumentNullException.ThrowIfNull(runOrder); if (candidates.Count == 0) throw new ArgumentException( "At least one candidate engine is required.", nameof(candidates) ); if ( runOrder.Count != candidates.Count || runOrder.Distinct().Count() != candidates.Count || runOrder.Any(i => i < 0 || i >= candidates.Count) ) throw new ArgumentException( "The run order must list every candidate index once.", nameof(runOrder) ); this.candidates = candidates; this.runOrder = runOrder; } /// Candidate indexes in the order they run. internal IReadOnlyList RunOrder => runOrder; public NestJobResult Solve( NestJob job, IProgress? progress = null, CancellationToken token = default ) { ArgumentNullException.ThrowIfNull(job); token.ThrowIfCancellationRequested(); var forward = progress == null ? null : new PlanFilter(job, progress, token); var scores = new CandidateScore?[candidates.Count]; ExceptionDispatchInfo? firstFailure = null; var firstFailureOrder = int.MaxValue; foreach (var order in runOrder) { token.ThrowIfCancellationRequested(); NestJobResult? result; if (forward != null) forward.Order = order; try { result = candidates[order]().Solve(job, forward, token); } catch (Exception ex) { // A canceled solve is rethrown by the token checks around each candidate. if (order < firstFailureOrder) { firstFailure = ExceptionDispatchInfo.Capture(ex); firstFailureOrder = order; } continue; } if (result == null) continue; var scored = CandidateScore.Of(job, result, order); scores[order] = scored; forward?.Offer(scored); } // Choose in tie-break order, whatever order the candidates ran in. CandidateScore? best = null; foreach (var scored in scores) if (scored != null && (best == null || scored.IsBetterThan(best))) best = scored; token.ThrowIfCancellationRequested(); if (best == null) { firstFailure?.Throw(); throw new InvalidOperationException("No candidate engine returned a result."); } forward?.Finish(best); CandidateProgress.ReportCommits(best.Result, progress); return best.Result; } /// /// Forwards candidate progress except plate commits, and replaces candidates' raw plan reports /// with Default's own best plan so far by . /// private sealed class PlanFilter(NestJob job, IProgress inner, CancellationToken token) : IProgress { private CandidateScore? shown; /// Candidate order of the plans now arriving, for score ties. public int Order { get; set; } public void Report(NestJobProgress value) { if (value == null || value.Stage == NestJobStage.PlateCommitted) return; if (value.Stage != NestJobStage.PlanImproved) { inner.Report(value); return; } if (value.Plan == null) return; CandidateScore plan; try { plan = CandidateScore.Of(job, value.Plan, Order); } catch (Exception) when (!token.IsCancellationRequested) { // A plan that cannot be scored is not shown; throwing would fail the candidate. return; } Offer(plan); } /// Reports the plan when it beats every plan reported so far. public void Offer(CandidateScore plan) { if (shown != null && !plan.IsBetterThan(shown)) return; shown = plan; CandidateProgress.ReportPlan(plan.Result, inner); } /// Makes the chosen result the last plan reported. A candidate's plan can outscore /// every returned result (its engine judged a later plan better), so this may follow it. public void Finish(CandidateScore chosen) { if (shown != null && CandidateProgress.SameLayout(shown.Result, chosen.Result)) return; shown = chosen; CandidateProgress.ReportPlan(chosen.Result, inner); } } }