diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index 8e08bcc..87a6b4a 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -248,35 +248,48 @@ namespace OpenNest }) .ToList(); - // Fill remnant with remainder items, shrinking the available area after each. - // Wrap progress so remnant fills include the strip parts already found. + // Fill remnant with remainder items using free-rectangle tracking. + // After each fill, the consumed box is split into two non-overlapping + // sub-rectangles (guillotine cut) so no usable area is lost. if (remnantBox.Width > 0 && remnantBox.Length > 0) { - var currentRemnant = remnantBox; + var freeBoxes = new List { remnantBox }; var remnantProgress = progress != null ? new AccumulatingProgress(progress, allParts) : null; foreach (var item in effectiveRemainder) { - if (token.IsCancellationRequested) + if (token.IsCancellationRequested || freeBoxes.Count == 0) break; - if (currentRemnant.Width <= 0 || currentRemnant.Length <= 0) - break; + var itemBbox = item.Drawing.Program.BoundingBox(); + var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length); - var remnantInner = new DefaultNestEngine(Plate); - var remnantParts = remnantInner.Fill( - new NestItem { Drawing = item.Drawing, Quantity = item.Quantity }, - currentRemnant, remnantProgress, token); + // Try free boxes from largest to smallest. + freeBoxes.Sort((a, b) => b.Area().CompareTo(a.Area())); - if (remnantParts != null && remnantParts.Count > 0) + for (var i = 0; i < freeBoxes.Count; i++) { - allParts.AddRange(remnantParts); + var box = freeBoxes[i]; - // Shrink remnant to avoid overlap with next item. - var usedBox = remnantParts.Cast().GetBoundingBox(); - currentRemnant = ComputeRemainderWithin(currentRemnant, usedBox, spacing); + if (System.Math.Min(box.Width, box.Length) < minItemDim) + continue; + + var remnantInner = new DefaultNestEngine(Plate); + var remnantParts = remnantInner.Fill( + new NestItem { Drawing = item.Drawing, Quantity = item.Quantity }, + box, remnantProgress, token); + + if (remnantParts != null && remnantParts.Count > 0) + { + allParts.AddRange(remnantParts); + freeBoxes.RemoveAt(i); + + var usedBox = remnantParts.Cast().GetBoundingBox(); + SplitFreeBox(box, usedBox, spacing, freeBoxes); + break; + } } } } @@ -291,7 +304,44 @@ namespace OpenNest return result; } - // ComputeRemainderWithin inherited from NestEngineBase + private static void SplitFreeBox(Box parent, Box used, double spacing, List freeBoxes) + { + var hWidth = parent.Right - used.Right - spacing; + var vHeight = parent.Top - used.Top - spacing; + + if (hWidth > spacing && vHeight > spacing) + { + // Guillotine split: give the overlapping corner to the larger strip. + var hFullArea = hWidth * parent.Length; + var vFullArea = parent.Width * vHeight; + + if (hFullArea >= vFullArea) + { + // hStrip gets full height; vStrip truncated to left of split line. + freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, parent.Length)); + var vWidth = used.Right + spacing - parent.X; + if (vWidth > spacing) + freeBoxes.Add(new Box(parent.X, used.Top + spacing, vWidth, vHeight)); + } + else + { + // vStrip gets full width; hStrip truncated below split line. + freeBoxes.Add(new Box(parent.X, used.Top + spacing, parent.Width, vHeight)); + var hHeight = used.Top + spacing - parent.Y; + if (hHeight > spacing) + freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, hHeight)); + } + } + else if (hWidth > spacing) + { + freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, parent.Length)); + } + else if (vHeight > spacing) + { + freeBoxes.Add(new Box(parent.X, used.Top + spacing, parent.Width, vHeight)); + } + } + /// /// Wraps an IProgress to prepend previously placed parts to each report, /// so the UI shows the full picture (strip + remnant) during remnant fills.