using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using OpenNest.Engine.Jobs.Adapters; namespace OpenNest.Engine.Jobs; /// /// 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. /// public sealed record NestPipelineRequest( string EngineName, IReadOnlyList Items, IReadOnlyList Stock, NestJobOptions Options = null ); /// A proposed sheet: new parts bound by reference to the caller's drawings. public sealed record ProposedPlate(int PlateIndex, NestPlateStock Stock, IReadOnlyList Parts); /// /// 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. /// public sealed class NestPipelineResult { internal NestPipelineResult( string engineName, NestJob job, NestJobResult raw, IReadOnlyList plates, IReadOnlyList violations, TimeSpan solveTime, TimeSpan validationTime ) { EngineName = engineName; Job = job; Raw = raw; Plates = plates; Violations = violations; SolveTime = solveTime; ValidationTime = validationTime; } public string EngineName { get; } public NestJob Job { get; } public NestJobResult Raw { get; } public IReadOnlyList Plates { get; } public IReadOnlyList Violations { get; } public bool IsValid => Violations.Count == 0; public NestJobStatus Status => Raw.Status; public NestJobStopReason StopReason => Raw.StopReason; public TimeSpan SolveTime { get; } public TimeSpan ValidationTime { get; } } /// /// 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. /// public static class NestPipeline { /// Resolves through /// ; unknown names throw . /// Engine cancellation propagates unchanged. public static NestPipelineResult Run( NestPipelineRequest request, IProgress progress = null, CancellationToken token = default ) { ArgumentNullException.ThrowIfNull(request); var engine = NestingEngineRegistry.Create(request.EngineName); return Run(engine, request.EngineName, request, progress, token); } /// Runs a caller-supplied engine instance through the same validation and binding. public static NestPipelineResult Run( INestingEngine engine, string engineName, NestPipelineRequest request, IProgress progress = null, CancellationToken token = default ) { ArgumentNullException.ThrowIfNull(engine); ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request.Items); ArgumentNullException.ThrowIfNull(request.Stock); token.ThrowIfCancellationRequested(); var drawingsByPartId = new Dictionary(StringComparer.Ordinal); var parts = new List(); 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); var clock = Stopwatch.StartNew(); var raw = engine.Solve(job, progress, token) ?? throw new InvalidOperationException($"Engine '{engineName}' returned no result."); var solveTime = clock.Elapsed; clock.Restart(); var violations = Validate(job, raw, drawingsByPartId); var validationTime = clock.Elapsed; var plates = raw .Plates.Select(sheet => new ProposedPlate( sheet.PlateIndex, sheet.Stock, NestResultBinder.Bind(sheet, drawingsByPartId) )) .ToList(); return new NestPipelineResult( engineName, job, raw, plates, violations, solveTime, validationTime ); } /// Benchmark validation, with messages naming the caller's drawings rather than /// internal requirement IDs. Placements for unknown requirements are reported and then /// excluded so the remaining layout is still checked. private static IReadOnlyList Validate( NestJob job, NestJobResult raw, IReadOnlyDictionary drawingsByPartId ) { var violations = new List(); var known = raw; var unknown = raw .Plates.SelectMany(sheet => sheet.Placements.Where(p => !drawingsByPartId.ContainsKey(p.PartId)) .Select(p => (sheet.PlateIndex, p.PartId)) ) .ToList(); if (unknown.Count > 0) { foreach (var group in unknown.GroupBy(u => (u.PlateIndex, u.PartId))) violations.Add( $"Plate {group.Key.PlateIndex} has {group.Count()} placement(s) for '{group.Key.PartId}', which is not part of this job" ); known = new NestJobResult( raw.Status, raw.StopReason, raw.Plates.Select(sheet => new NestJobPlateResult( sheet.PlateIndex, sheet.Stock, sheet.Placements.Where(p => drawingsByPartId.ContainsKey(p.PartId)) )), raw.Fulfillment, raw.StockUsage ); } var names = drawingsByPartId.ToDictionary( kv => kv.Key, kv => kv.Value.Name ?? kv.Key, StringComparer.Ordinal ); violations.AddRange(NestLayoutCheck.Violations(job, known, names)); return violations; } }