using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.Engine.Fill; using OpenNest.Geometry; using OpenNest.Engine.Jobs.Adapters; namespace OpenNest.Engine.Jobs.Placement; /// Constrained-order linear fills in conservative rectangular free regions. /// Regions are only search hints; every accepted pose passes the job geometry validator. internal sealed class OrderedPlateNester : IPlateNester { private readonly Dictionary drawings = new(StringComparer.Ordinal); public PlateCandidate Place( PlatePlacementRequest request, IProgress progress = null, CancellationToken token = default ) { var work = DrawingJobMapper.CreatePlate(request.Stock).WorkArea(); var poses = new List(); var obstacles = new List(); var requirements = request.Parts.ToDictionary(p => p.Id); var demand = request.Parts.ToDictionary(p => p.Id, p => p.Quantity); foreach (var requirement in request.Parts) { token.ThrowIfCancellationRequested(); if (!drawings.TryGetValue(requirement.Id, out var drawing)) drawings.Add(requirement.Id, drawing = DrawingJobMapper.CreateDrawing(requirement)); var left = requirement.Quantity; while (left > 0) { var regions = new RemnantFinder(work, obstacles).FindRemnants(); List best = null; foreach (var region in regions) { foreach (var angle in Angles(requirement.Rotation)) { token.ThrowIfCancellationRequested(); // FillLinear uses actual line/arc geometry for copy distances. var parts = new FillLinear(region, request.Stock.PartSpacing) .Fill(drawing, angle, NestDirection.Horizontal) .Take(left) .ToList(); if (parts.Count == 0 || (best != null && parts.Count <= best.Count)) continue; var trial = poses .Concat( parts.Select(p => new NestJobPlacement( requirement.Id, 0, p.Location.X, p.Location.Y, p.Rotation )) ) .ToList(); try { NestJobValidator.ValidateCandidate( new PlateCandidate(trial), request.Stock, demand, requirements ); best = parts; } catch (InvalidOperationException) { // Geometry kernels are proposal generators, never the acceptance gate. } if (best?.Count == left) break; } if (best?.Count == left) break; } if (best == null) break; foreach (var part in best) { poses.Add( new NestJobPlacement( requirement.Id, 0, part.Location.X, part.Location.Y, part.Rotation ) ); obstacles.Add(part.BoundingBox.Offset(request.Stock.PartSpacing)); } left -= best.Count; } } token.ThrowIfCancellationRequested(); return new PlateCandidate(poses); } private static IEnumerable Angles(RotationPolicy policy) { if (policy.Kind == RotationPolicyKind.Fixed) { yield return policy.Start; yield break; } // A bounded deterministic search, not a proof that an unplaced part cannot fit. if (policy.Kind == RotationPolicyKind.Automatic) { yield return 0; yield return System.Math.PI / 2; yield return System.Math.PI; yield return 3 * System.Math.PI / 2; for (var degrees = 5; degrees < 180; degrees += 5) if (degrees != 90) yield return degrees * System.Math.PI / 180; yield break; } for (var index = 0L; ; index++) { var angle = policy.Start + index * policy.Step; if (angle > policy.End + 1e-9) yield break; yield return angle; } } }