diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Compactor.cs index 3ef73eb..4a4f781 100644 --- a/OpenNest.Engine/Compactor.cs +++ b/OpenNest.Engine/Compactor.cs @@ -18,49 +18,91 @@ namespace OpenNest /// Everything already on the plate (excluding movingParts) is treated /// as stationary obstacles. /// + private const double RepeatThreshold = 0.01; + private const int MaxIterations = 20; + public static void Compact(List movingParts, Plate plate) { if (movingParts == null || movingParts.Count == 0) return; - Push(movingParts, plate, PushDirection.Left); - Push(movingParts, plate, PushDirection.Down); + var savedPositions = SavePositions(movingParts); + + // Try left-first. + var leftFirst = CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down); + + // Restore and try down-first. + RestorePositions(movingParts, savedPositions); + var downFirst = CompactLoop(movingParts, plate, PushDirection.Down, PushDirection.Left); + + // Keep left-first if it traveled further. + if (leftFirst > downFirst) + { + RestorePositions(movingParts, savedPositions); + CompactLoop(movingParts, plate, PushDirection.Left, PushDirection.Down); + } } - private static void Push(List movingParts, Plate plate, PushDirection direction) + private static double CompactLoop(List parts, Plate plate, + PushDirection first, PushDirection second) + { + var total = 0.0; + + for (var i = 0; i < MaxIterations; i++) + { + var a = Push(parts, plate, first); + var b = Push(parts, plate, second); + total += a + b; + + if (a <= RepeatThreshold && b <= RepeatThreshold) + break; + } + + return total; + } + + private static Vector[] SavePositions(List parts) + { + var positions = new Vector[parts.Count]; + for (var i = 0; i < parts.Count; i++) + positions[i] = parts[i].Location; + return positions; + } + + private static void RestorePositions(List parts, Vector[] positions) + { + for (var i = 0; i < parts.Count; i++) + parts[i].Location = positions[i]; + } + + public static double Push(List movingParts, Plate plate, PushDirection direction) { - // Start with parts already on the plate (excluding the moving group). var obstacleParts = plate.Parts .Where(p => !movingParts.Contains(p)) .ToList(); - var obstacleBoxes = new List(obstacleParts.Count + movingParts.Count); - var obstacleLines = new List>(obstacleParts.Count + movingParts.Count); + var obstacleBoxes = new Box[obstacleParts.Count]; + var obstacleLines = new List[obstacleParts.Count]; for (var i = 0; i < obstacleParts.Count; i++) - { - obstacleBoxes.Add(obstacleParts[i].BoundingBox); - obstacleLines.Add(null); // lazy - } + obstacleBoxes[i] = obstacleParts[i].BoundingBox; var opposite = Helper.OppositeDirection(direction); var halfSpacing = plate.PartSpacing / 2; var isHorizontal = Helper.IsHorizontalDirection(direction); var workArea = plate.WorkArea(); + var distance = double.MaxValue; foreach (var moving in movingParts) { - var distance = double.MaxValue; - var movingBox = moving.BoundingBox; - - // Plate edge distance. - var edgeDist = Helper.EdgeDistance(movingBox, workArea, direction); + var edgeDist = Helper.EdgeDistance(moving.BoundingBox, workArea, direction); if (edgeDist > 0 && edgeDist < distance) distance = edgeDist; + var movingBox = moving.BoundingBox; List movingLines = null; - for (var i = 0; i < obstacleBoxes.Count; i++) + for (var i = 0; i < obstacleBoxes.Length; i++) { var gap = Helper.DirectionalGap(movingBox, obstacleBoxes[i], direction); if (gap < 0 || gap >= distance) @@ -77,32 +119,25 @@ namespace OpenNest ? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) : Helper.GetPartLines(moving, direction, ChordTolerance); - var obstaclePart = i < obstacleParts.Count ? obstacleParts[i] : null; - - obstacleLines[i] ??= obstaclePart != null - ? (halfSpacing > 0 - ? Helper.GetOffsetPartLines(obstaclePart, halfSpacing, opposite, ChordTolerance) - : Helper.GetPartLines(obstaclePart, opposite, ChordTolerance)) - : (halfSpacing > 0 - ? Helper.GetOffsetPartLines(moving, halfSpacing, opposite, ChordTolerance) - : Helper.GetPartLines(moving, opposite, ChordTolerance)); + obstacleLines[i] ??= halfSpacing > 0 + ? Helper.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) + : Helper.GetPartLines(obstacleParts[i], opposite, ChordTolerance); var d = Helper.DirectionalDistance(movingLines, obstacleLines[i], direction); if (d < distance) distance = d; } - - if (distance < double.MaxValue && distance > 0) - { - var offset = Helper.DirectionToOffset(direction, distance); - moving.Offset(offset); - } - - // This part is now an obstacle for subsequent moving parts. - obstacleBoxes.Add(moving.BoundingBox); - obstacleParts.Add(moving); - obstacleLines.Add(null); // will be lazily computed if needed } + + if (distance < double.MaxValue && distance > 0) + { + var offset = Helper.DirectionToOffset(direction, distance); + foreach (var moving in movingParts) + moving.Offset(offset); + return distance; + } + + return 0; } } } diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 051d82c..fce56be 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -837,7 +837,7 @@ namespace OpenNest.Controls var parts = await Task.Run(() => engine.Fill(groupParts, workArea, progress, cts.Token)); - if (parts.Count > 0) + if (parts.Count > 0 && !cts.IsCancellationRequested) { AcceptTemporaryParts(); sw.Stop(); @@ -937,65 +937,9 @@ namespace OpenNest.Controls public void PushSelected(PushDirection direction) { - var stationaryParts = parts.Where(p => !p.IsSelected && !SelectedParts.Contains(p)).ToList(); - var stationaryBoxes = new Box[stationaryParts.Count]; - var stationaryLines = new List[stationaryParts.Count]; - - var opposite = Helper.OppositeDirection(direction); - var halfSpacing = Plate.PartSpacing / 2; - var isHorizontal = Helper.IsHorizontalDirection(direction); - - for (var i = 0; i < stationaryParts.Count; i++) - stationaryBoxes[i] = stationaryParts[i].BoundingBox; - - var workArea = Plate.WorkArea(); - var distance = double.MaxValue; - - foreach (var selected in SelectedParts) - { - // Check plate edge first to tighten the upper bound. - var edgeDist = Helper.EdgeDistance(selected.BoundingBox, workArea, direction); - if (edgeDist > 0 && edgeDist < distance) - distance = edgeDist; - - var movingBox = selected.BoundingBox; - List movingLines = null; - - for (var i = 0; i < stationaryBoxes.Length; i++) - { - // Skip parts not ahead in the push direction or further than current best. - var gap = Helper.DirectionalGap(movingBox, stationaryBoxes[i], direction); - if (gap < 0 || gap >= distance) - continue; - - var perpOverlap = isHorizontal - ? movingBox.IsHorizontalTo(stationaryBoxes[i], out _) - : movingBox.IsVerticalTo(stationaryBoxes[i], out _); - - if (!perpOverlap) - continue; - - // Compute lines lazily — only for parts that survive bounding box checks. - movingLines ??= halfSpacing > 0 - ? Helper.GetOffsetPartLines(selected.BasePart, halfSpacing, direction, OffsetTolerance) - : Helper.GetPartLines(selected.BasePart, direction, OffsetTolerance); - - stationaryLines[i] ??= halfSpacing > 0 - ? Helper.GetOffsetPartLines(stationaryParts[i].BasePart, halfSpacing, opposite, OffsetTolerance) - : Helper.GetPartLines(stationaryParts[i].BasePart, opposite, OffsetTolerance); - - var d = Helper.DirectionalDistance(movingLines, stationaryLines[i], direction); - if (d < distance) - distance = d; - } - } - - if (distance < double.MaxValue && distance > 0) - { - var offset = Helper.DirectionToOffset(direction, distance); - SelectedParts.ForEach(p => p.Offset(offset)); - Invalidate(); - } + var movingParts = SelectedParts.Select(p => p.BasePart).ToList(); + Compactor.Push(movingParts, Plate, direction); + Invalidate(); } private string GetDisplayName(Type type)