diff --git a/OpenNest.Engine/FillScore.cs b/OpenNest.Engine/FillScore.cs new file mode 100644 index 0000000..e06cc65 --- /dev/null +++ b/OpenNest.Engine/FillScore.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Linq; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest +{ + public readonly struct FillScore : System.IComparable + { + /// + /// Minimum short-side dimension for a remnant to be considered usable. + /// + public const double MinRemnantDimension = 12.0; + + public int Count { get; } + + /// + /// Area of the largest remnant whose short side >= MinRemnantDimension. + /// Zero if no usable remnant exists. + /// + public double UsableRemnantArea { get; } + + /// + /// Total part area / bounding box area of all placed parts. + /// + public double Density { get; } + + public FillScore(int count, double usableRemnantArea, double density) + { + Count = count; + UsableRemnantArea = usableRemnantArea; + Density = density; + } + + /// + /// Computes a fill score from placed parts and the work area they were placed in. + /// + public static FillScore Compute(List parts, Box workArea) + { + if (parts == null || parts.Count == 0) + return default; + + var totalPartArea = 0.0; + + foreach (var part in parts) + totalPartArea += part.BaseDrawing.Area; + + var bbox = ((IEnumerable)parts).GetBoundingBox(); + var bboxArea = bbox.Area(); + var density = bboxArea > 0 ? totalPartArea / bboxArea : 0; + + var usableRemnantArea = ComputeUsableRemnantArea(parts, workArea); + + return new FillScore(parts.Count, usableRemnantArea, density); + } + + /// + /// Finds the largest usable remnant (short side >= MinRemnantDimension) + /// by checking right and top edge strips between placed parts and the work area boundary. + /// + private static double ComputeUsableRemnantArea(List parts, Box workArea) + { + var maxRight = double.MinValue; + var maxTop = double.MinValue; + + foreach (var part in parts) + { + var bb = part.BoundingBox; + + if (bb.Right > maxRight) + maxRight = bb.Right; + + if (bb.Top > maxTop) + maxTop = bb.Top; + } + + var largest = 0.0; + + // Right strip + if (maxRight < workArea.Right) + { + var width = workArea.Right - maxRight; + var height = workArea.Height; + + if (System.Math.Min(width, height) >= MinRemnantDimension) + largest = System.Math.Max(largest, width * height); + } + + // Top strip + if (maxTop < workArea.Top) + { + var width = workArea.Width; + var height = workArea.Top - maxTop; + + if (System.Math.Min(width, height) >= MinRemnantDimension) + largest = System.Math.Max(largest, width * height); + } + + return largest; + } + + /// + /// Lexicographic comparison: count, then usable remnant area, then density. + /// + public int CompareTo(FillScore other) + { + var c = Count.CompareTo(other.Count); + + if (c != 0) + return c; + + c = UsableRemnantArea.CompareTo(other.UsableRemnantArea); + + if (c != 0) + return c; + + return Density.CompareTo(other.Density); + } + + public static bool operator >(FillScore a, FillScore b) => a.CompareTo(b) > 0; + public static bool operator <(FillScore a, FillScore b) => a.CompareTo(b) < 0; + public static bool operator >=(FillScore a, FillScore b) => a.CompareTo(b) >= 0; + public static bool operator <=(FillScore a, FillScore b) => a.CompareTo(b) <= 0; + } +}