From 00ee205b44d2bf57202ff8ce6efec4c00880f85b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 15 Mar 2026 12:41:29 -0400 Subject: [PATCH] feat(engine): add Compactor for post-fill gravity compaction Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Compactor.cs | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 OpenNest.Engine/Compactor.cs diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Compactor.cs new file mode 100644 index 0000000..b5ff5ab --- /dev/null +++ b/OpenNest.Engine/Compactor.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using OpenNest.Geometry; + +namespace OpenNest +{ + /// + /// Pushes a group of parts left and down to close gaps after placement. + /// Uses the same directional-distance logic as PlateView.PushSelected + /// but operates on Part objects directly. + /// + public static class Compactor + { + private const double ChordTolerance = 0.001; + + /// + /// Compacts movingParts toward the bottom-left of the plate work area. + /// Everything already on the plate (excluding movingParts) is treated + /// as stationary obstacles. + /// + 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); + } + + private static void Push(List movingParts, Plate plate, PushDirection direction) + { + var stationaryParts = plate.Parts + .Where(p => !movingParts.Contains(p)) + .ToList(); + + var stationaryBoxes = new Box[stationaryParts.Count]; + + for (var i = 0; i < stationaryParts.Count; i++) + stationaryBoxes[i] = stationaryParts[i].BoundingBox; + + var stationaryLines = new List[stationaryParts.Count]; + var opposite = Helper.OppositeDirection(direction); + var halfSpacing = plate.PartSpacing / 2; + var isHorizontal = Helper.IsHorizontalDirection(direction); + var workArea = plate.WorkArea(); + + foreach (var moving in movingParts) + { + var distance = double.MaxValue; + var movingBox = moving.BoundingBox; + + // Plate edge distance. + var edgeDist = Helper.EdgeDistance(movingBox, workArea, direction); + if (edgeDist > 0 && edgeDist < distance) + distance = edgeDist; + + List movingLines = null; + + for (var i = 0; i < stationaryBoxes.Length; i++) + { + 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; + + movingLines ??= halfSpacing > 0 + ? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) + : Helper.GetPartLines(moving, direction, ChordTolerance); + + stationaryLines[i] ??= halfSpacing > 0 + ? Helper.GetOffsetPartLines(stationaryParts[i], halfSpacing, opposite, ChordTolerance) + : Helper.GetPartLines(stationaryParts[i], opposite, ChordTolerance); + + 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); + moving.Offset(offset); + } + } + } + } +}