diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Compactor.cs index f7bc551..747e20f 100644 --- a/OpenNest.Engine/Compactor.cs +++ b/OpenNest.Engine/Compactor.cs @@ -81,6 +81,12 @@ namespace OpenNest .Where(p => !movingParts.Contains(p)) .ToList(); + return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); + } + + public static double Push(List movingParts, List obstacleParts, + Box workArea, double partSpacing, PushDirection direction) + { var obstacleBoxes = new Box[obstacleParts.Count]; var obstacleLines = new List[obstacleParts.Count]; @@ -88,9 +94,8 @@ namespace OpenNest obstacleBoxes[i] = obstacleParts[i].BoundingBox; var opposite = SpatialQuery.OppositeDirection(direction); - var halfSpacing = plate.PartSpacing / 2; + var halfSpacing = partSpacing / 2; var isHorizontal = SpatialQuery.IsHorizontalDirection(direction); - var workArea = plate.WorkArea(); var distance = double.MaxValue; // BB gap at which offset geometries are expected to be touching. @@ -152,5 +157,60 @@ namespace OpenNest return 0; } + + /// + /// Compacts parts individually toward the bottom-left of the work area. + /// Each part is pushed against all others as obstacles, closing geometry-based gaps. + /// Does not require parts to be on a plate. + /// + public static void CompactIndividual(List parts, Box workArea, double partSpacing) + { + if (parts == null || parts.Count < 2) + return; + + var savedPositions = SavePositions(parts); + + var leftFirst = CompactIndividualLoop(parts, workArea, partSpacing, + PushDirection.Left, PushDirection.Down); + + RestorePositions(parts, savedPositions); + var downFirst = CompactIndividualLoop(parts, workArea, partSpacing, + PushDirection.Down, PushDirection.Left); + + if (leftFirst > downFirst) + { + RestorePositions(parts, savedPositions); + CompactIndividualLoop(parts, workArea, partSpacing, + PushDirection.Left, PushDirection.Down); + } + } + + private static double CompactIndividualLoop(List parts, Box workArea, + double partSpacing, PushDirection first, PushDirection second) + { + var total = 0.0; + + for (var pass = 0; pass < MaxIterations; pass++) + { + var moved = 0.0; + + foreach (var part in parts) + { + var single = new List(1) { part }; + var obstacles = new List(parts.Count - 1); + foreach (var p in parts) + if (p != part) obstacles.Add(p); + + moved += Push(single, obstacles, workArea, partSpacing, first); + moved += Push(single, obstacles, workArea, partSpacing, second); + } + + total += moved; + if (moved <= RepeatThreshold) + break; + } + + return total; + } } } diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index 7053617..6e21279 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -214,6 +214,16 @@ namespace OpenNest : trialPlacedBox.Right - workArea.X; } + // TODO: Compact strip parts individually to close geometry-based gaps. + // Disabled pending investigation — remnant finder picks up gaps created + // by compaction and scatters parts into them. + // Compactor.CompactIndividual(bestParts, workArea, Plate.PartSpacing); + // + // var compactedBox = bestParts.Cast().GetBoundingBox(); + // bestDim = direction == StripDirection.Bottom + // ? compactedBox.Top - workArea.Y + // : compactedBox.Right - workArea.X; + // Build remnant box with spacing gap. var spacing = Plate.PartSpacing; var remnantBox = direction == StripDirection.Bottom