using System; using System.Collections.Generic; using System.Linq; using OpenNest.Converters; namespace OpenNest.Engine.Jobs.CommonLines; /// /// Decides whether a common-line layout replaces the best layout without common lines (or an /// earlier common-line trial), by the job's . Both must be valid, /// keepable proposals of the same job; an invalid incumbent always loses to a valid candidate. /// The engines' own ranking (and FillScore) is untouched: this only chooses between the /// layouts they returned. /// internal static class CommonLineRanking { /// Differences below this are rounding, never a reason to prefer either layout. private const double Epsilon = 1e-6; internal static bool Prefers(CommonLineUsage usage, NestPipelineResult candidate, NestPipelineResult incumbent) { ArgumentNullException.ThrowIfNull(candidate); ArgumentNullException.ThrowIfNull(incumbent); if (usage == CommonLineUsage.Off || !Keepable(candidate)) return false; if (!Keepable(incumbent)) return true; var placed = Placed(candidate).CompareTo(Placed(incumbent)); if (placed != 0) return placed > 0; var sheets = Sheets(candidate).CompareTo(Sheets(incumbent)); if (sheets != 0) return sheets < 0; // Equal parts and sheets: "when it adds parts" keeps the incumbent. if (usage != CommonLineUsage.Whenever) return false; var material = Material(candidate, incumbent); var cut = Compare(CutLength(candidate), CutLength(incumbent)); // Better on one measure and no worse on the other. return (material > 0 && cut <= 0) || (cut < 0 && material >= 0); } private static bool Keepable(NestPipelineResult result) => result.CanKeep && result.IsValid; private static int Placed(NestPipelineResult result) => result.Raw.Plates.Sum(p => p.Placements.Count); private static int Sheets(NestPipelineResult result) => result.Raw.Plates.Count; /// Positive when uses less material than : /// a lower net cost (a larger salvage-credited remnant), then the larger compactness offcut /// (a shorter used length). internal static int Material(NestPipelineResult a, NestPipelineResult b) { var cost = Compare(NestJobCost.Evaluate(a.Job, a.Raw), NestJobCost.Evaluate(b.Job, b.Raw)); if (cost != 0) return -cost; return Compare(NestJobCost.CompactnessOffcut(a.Job, a.Raw), NestJobCost.CompactnessOffcut(b.Job, b.Raw)); } /// /// Total cut length: every part's cut contours, less each shared span once (a common line is /// cut once). Repeated sheets count each copy. /// internal static double CutLength(NestPipelineResult result) { var lengths = new Dictionary(ReferenceEqualityComparer.Instance); var total = 0.0; foreach (var plate in result.Plates) { var length = plate.Parts.Sum(part => ContourLength(part.BaseDrawing, lengths)); foreach (var group in plate.CommonLines) length -= CommonLineGeometry.Check(group).Spans.Sum(span => span.Length); total += plate.Quantity * length; } return total; } private static double ContourLength(Drawing drawing, Dictionary lengths) { if (!lengths.TryGetValue(drawing, out var length)) { length = ConvertProgram.ToGeometry(drawing.Program).ForNesting().Sum(entity => entity.Length); lengths.Add(drawing, length); } return length; } private static int Compare(double a, double b) => System.Math.Abs(a - b) <= Epsilon * System.Math.Max(1, System.Math.Max(System.Math.Abs(a), System.Math.Abs(b))) ? 0 : a.CompareTo(b); }