using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.Engine.Jobs.Adapters; namespace OpenNest.Engine.Jobs; /// Applies accepted layouts and total copy quantities to empty/new plates only. /// Caller must keep the nest and its drawings stable from request construction through commit. public static class NestPipelineCommit { public static IReadOnlyList ApplyToEmptyPlates( NestPipelineResult result, PlateManager manager, bool allowInvalid = false, CancellationToken token = default ) { ArgumentNullException.ThrowIfNull(manager); EnsureCommittable(result, allowInvalid, token); // Commit is synchronous on the caller's owning thread. Cancellation is checked // before mutation, not partway through attachment (which would leave half a job). manager.BeginBatch(); try { return Attach(result, manager.GetOrCreateEmpty); } finally { manager.EndBatch(); } } /// /// Same checks and commit as for callers without a /// (no trailing empty sheet). is /// called once per non-empty proposed layout, after every check has passed, and must /// return a plate without parts that already belongs to the nest. /// public static IReadOnlyList ApplyToPlates( NestPipelineResult result, Func nextPlate, bool allowInvalid = false, CancellationToken token = default ) { ArgumentNullException.ThrowIfNull(nextPlate); EnsureCommittable(result, allowInvalid, token); return Attach(result, nextPlate); } /// /// Pairs the plates or returned /// for with the stock each was solved on: one per proposed plate /// with parts, in proposal order. /// public static IReadOnlyList<(Plate Plate, NestPlateStock Stock)> PairWithStock( NestPipelineResult result, IReadOnlyList applied ) { ArgumentNullException.ThrowIfNull(result); ArgumentNullException.ThrowIfNull(applied); var solved = result.Plates.Where(p => p.Parts.Count > 0).ToList(); if (solved.Count != applied.Count) throw new ArgumentException("The plates were not committed from this result.", nameof(applied)); return applied.Zip(solved, (plate, proposed) => (plate, proposed.Stock)).ToList(); } private static void EnsureCommittable(NestPipelineResult result, bool allowInvalid, CancellationToken token) { ArgumentNullException.ThrowIfNull(result); token.ThrowIfCancellationRequested(); if (!result.CanKeep || (!result.IsValid && !allowInvalid)) throw new InvalidOperationException("The nesting result cannot be committed without a keepable layout and explicit consent to its violations."); if (NestPipelineDrawingFreshness.Changes(result.Job, result.DrawingsByPartId).Count > 0 || NestPipelineDrawingFreshness.BoundChanges(result.Job, result.Raw, result.Plates, result.DrawingsByPartId, result.IsValid, token).Count > 0) throw new InvalidOperationException("A drawing or proposed part changed after the nesting proposal was validated; run Auto Nest again."); token.ThrowIfCancellationRequested(); } private static IReadOnlyList Attach(NestPipelineResult result, Func nextPlate) { var applied = new List(); foreach (var proposed in result.Plates) { if (proposed.Parts.Count == 0) continue; var plate = nextPlate(); plate.Size = proposed.Stock.Size; plate.Outline = proposed.Stock.Outline; plate.PartSpacing = proposed.Stock.PartSpacing; plate.EdgeSpacing = proposed.Stock.EdgeSpacing; plate.Quadrant = proposed.Stock.Quadrant; plate.Quantity = proposed.Quantity; SyncClampZones(plate, proposed.Stock); plate.Parts.AddRange(proposed.Parts); foreach (var group in proposed.CommonLines) plate.CommonLines.Add(group); applied.Add(plate); } return applied.AsReadOnly(); } /// /// The live plate keeps exactly the clamp keep-outs its layout was solved and validated /// against, including sheets created for this commit. Stock zones live in the placement /// frame; plate zones in plate coordinates, so the stock's translation is inverted. Zones /// already equal in the placement frame are left untouched (no change events). Replaced /// zones stay tied to the plate's clamp rail only when that rail recreates them (directly, /// or minus whichever clamps Auto Nest removed as a last resort, D8/C13); otherwise a /// later size or quadrant assignment would silently swap in other clamps. /// private static void SyncClampZones(Plate plate, NestPlateStock stock) { var left = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length; var bottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width; // The rail the solved zones were resolved from wins over whatever the target follows; // explicit stock keeps the target's own rail as before. var rail = stock.ClampRail ?? plate.ClampRail; if (!SameInPlacementFrame(plate.ClampZones, stock.ClampZones, left, bottom)) { plate.SetClampRail(null); // explicit zones from here on foreach (var zone in stock.ClampZones) plate.ClampZones.Add(new ClampZone(zone.Id, zone.Bounds.Translate(-left, -bottom))); } // This runs even when the target already held exactly these zones, so a removal is // never lost. if (rail != null) DrawingJobMapper.FollowClampRail(plate, rail); } private static bool SameInPlacementFrame( IList plateZones, IReadOnlyList stockZones, double left, double bottom) { if (plateZones.Count != stockZones.Count) return false; for (var i = 0; i < plateZones.Count; i++) { // Same arithmetic NestPlateStock uses to carry plate zones into the placement frame. var placed = plateZones[i].Bounds.Translate(left, bottom); var solved = stockZones[i].Bounds; if (!string.Equals(plateZones[i].Id, stockZones[i].Id, StringComparison.Ordinal) || placed.X != solved.X || placed.Y != solved.Y || placed.Length != solved.Length || placed.Width != solved.Width) return false; } return true; } }