diff --git a/OpenNest.Engine.Tests/Fill/CompactorStepTests.cs b/OpenNest.Engine.Tests/Fill/CompactorStepTests.cs index 99827f9d..d33e761d 100644 --- a/OpenNest.Engine.Tests/Fill/CompactorStepTests.cs +++ b/OpenNest.Engine.Tests/Fill/CompactorStepTests.cs @@ -8,7 +8,7 @@ namespace OpenNest.Engine.Tests.Fill; /// /// Compactor.Step moves a selection by its own repeat distance at the part spacing, -/// and like a push never passes the work-area edge or another part. +/// always the full distance, whatever lies on the way or at the landing. /// public class CompactorStepTests { @@ -86,25 +86,29 @@ public class CompactorStepTests } [Fact] - public void StopsAtAnotherPartInsteadOfPassingIt() + public void MovesTheFullStepPastAnotherPart() { + // Interlocked rows cannot slide straight past their neighbors, so patterning + // them needs the jump, not a push. var plate = MakePlate(); var part = AddSquare(plate, 2, 10); - AddSquare(plate, 7, 10); // 2 clear of the mover: it may close 1 at 1.0 spacing. + AddSquare(plate, 7, 10); var moved = Compactor.Step(new List { part }, plate, PushDirection.Right); - Assert.Equal(1, moved, 3); + Assert.Equal(4, moved, 3); + Assert.Equal(6, part.Location.X, 3); } [Fact] - public void StopsAtTheWorkAreaEdge() + public void MovesTheFullStepOffThePlate() { var plate = MakePlate(); var part = AddSquare(plate, 18, 10); // Right side at 21, 2.5 from the work area's. var moved = Compactor.Step(new List { part }, plate, PushDirection.Right); - Assert.Equal(2.5, moved, 3); + Assert.Equal(4, moved, 3); + Assert.Equal(22, part.Location.X, 3); } } diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index 19b181b2..385eeb7c 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -31,16 +31,13 @@ namespace OpenNest.Engine.Fill public static double Push(List movingParts, Plate plate, double angle) => Push(movingParts, plate, new Vector(System.Math.Cos(angle), System.Math.Sin(angle))); - private static double Push(List movingParts, Plate plate, Vector direction) => - Push(movingParts, plate, direction, double.MaxValue); - - private static double Push(List movingParts, Plate plate, Vector direction, double limit) + private static double Push(List movingParts, Plate plate, Vector direction) { var obstacleParts = plate.Parts .Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts)) .ToList(); - return Push(movingParts, plate.WorkArea(), direction, limit, + return Push(movingParts, plate.WorkArea(), direction, new ObstacleSet(obstacleParts, plate.PartSpacing), ZoneObstacles(plate)); } @@ -48,10 +45,9 @@ namespace OpenNest.Engine.Fill /// Moves movingParts one repeat step along : the /// distance at which a copy left behind would sit at the plate's part spacing, /// measured on the parts' geometry like a fill's copy distance, so shapes that - /// nest into their own copy step less than their size. Like - /// , the move stops early at - /// the work-area edge, a clamp zone or another part, never passing one. - /// Returns the distance moved. + /// nest into their own copy step less than their size. Unlike a push it always + /// moves the full step, like a nudge: it ignores the work-area edge and other + /// parts, so it can land off the plate or on a part. Returns the distance moved. /// public static double Step(List movingParts, Plate plate, PushDirection direction) { @@ -68,7 +64,10 @@ namespace OpenNest.Engine.Fill if (!double.IsFinite(step) || step <= 0) return 0; - return Push(movingParts, plate, SpatialQuery.DirectionToOffset(direction, 1.0), step); + var offset = SpatialQuery.DirectionToOffset(direction, step); + foreach (var part in movingParts) + part.Offset(offset); + return step; } /// @@ -82,27 +81,19 @@ namespace OpenNest.Engine.Fill Vector direction ) => Push(movingParts, workArea, direction, new ObstacleSet(obstacleParts, partSpacing)); - private static double Push( - List movingParts, - Box workArea, - Vector direction, - params ObstacleSet[] obstacleSets - ) => Push(movingParts, workArea, direction, double.MaxValue, obstacleSets); - /// - /// Pushes movingParts at most toward the first contact - /// with the work-area edge or an obstacle, keeping each obstacle set's own spacing. + /// Pushes movingParts to the first contact with the work-area edge or an obstacle, + /// keeping each obstacle set's own spacing. /// private static double Push( List movingParts, Box workArea, Vector direction, - double limit, params ObstacleSet[] obstacleSets ) { var opposite = -direction; - var distance = limit; + var distance = double.MaxValue; foreach (var moving in movingParts) { diff --git a/OpenNest/Controls/SelectionManager.cs b/OpenNest/Controls/SelectionManager.cs index e78fd154..7445fd6f 100644 --- a/OpenNest/Controls/SelectionManager.cs +++ b/OpenNest/Controls/SelectionManager.cs @@ -110,8 +110,8 @@ namespace OpenNest.Controls /// /// Moves the selection one repeat step in : its own /// size along that axis plus the part spacing, measured on its geometry - /// (). It stops early at an edge or another part. - /// Repeated steps undo as one move, like nudges. + /// (). Like a nudge it always moves the full step, even + /// off the plate or onto a part. Repeated steps undo as one move. /// public void StepSelected(PushDirection direction) { diff --git a/README.md b/README.md index bbd7c78b..81d1c459 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Default compares the built-in Irregular, Rectangles and Circles engines and keep ## Keyboard Shortcuts -`Ctrl+F` fill area · `F` zoom to fit · `Shift+wheel` / middle-click rotate · `X`/`Y` push · arrows nudge by the offset increment (`F5`), or with its calculated distance by the selection's own size plus part spacing, landing tight with no push back · `Shift+arrow` push. +`Ctrl+F` fill area · `F` zoom to fit · `Shift+wheel` / middle-click rotate · `X`/`Y` push · arrows nudge by the offset increment (`F5`), or with its calculated distance by the selection's own size plus part spacing, landing tight with no push back (it always moves, even off the plate) · `Shift+arrow` push. ## Status & License diff --git a/docs/geometry/directional-slides.md b/docs/geometry/directional-slides.md index 5fb4adbc..983b635a 100644 --- a/docs/geometry/directional-slides.md +++ b/docs/geometry/directional-slides.md @@ -11,7 +11,7 @@ Open/incomplete chains and ambiguous contacts conservatively block. Loops are re ## Callers - PlateView uses `SelectionManager.PushSelected` → `Compactor.Push`. The zero-spacing nudge-and-discard workaround is removed. Cutout contours on stationary obstacles are retained, and the plate entry's existing-overlap filter accounts for holes rather than relying solely on `Part.Intersects` (which compares outer perimeters). -- With **Calculated distance** checked in Tools > Set Offset Increment, PlateView's arrow keys use `SelectionManager.StepSelected` → `Compactor.Step`. The step is the selection's linear-fill copy distance (`FillLinear.CopyDistance`) at the plate's part spacing, so a shape that nests into its own copy steps less than its size. That copy distance is the same in both directions along an axis. The move is then a push capped at that step, so it stops at the work-area edge, a clamp zone or another part and never passes one. A clone preview resting on the copy just placed ignores that copy, as a push does with overlapping parts, and so steps clear of it. +- With **Calculated distance** checked in Tools > Set Offset Increment, PlateView's arrow keys use `SelectionManager.StepSelected` → `Compactor.Step`. The step is the selection's linear-fill copy distance (`FillLinear.CopyDistance`) at the plate's part spacing, so a shape that nests into its own copy steps less than its size. That copy distance is the same in both directions along an axis. The move is not a slide: like a fixed nudge it always moves the full step, ignoring the work-area edge, clamps and other parts, because interlocked rows (staggered triangles, say) cannot slide straight past their neighbors to the next row. It can land off the plate or on a part; the layout check reports that as it does for any nudge. - Linear fill inherits the shared native-entity query unchanged. Extents fill passes complete boundary loops instead of direction-filtered fragments. - CPU best-fit batches prepare contact topology once and use all vertices plus curve/line interior and curve/curve tangency events. The old leading-half vertex filter cannot establish the next blocker after a skipped touch. - GPU kernels retain nearest-hit reduction and return unsnapped contact witnesses. The shared CPU classifier accepts a blocking witness or replays the full query after a nonblocking witness, preserving tied/later blockers. Both batch APIs honor active buffer lengths and refresh mutated/reused segment arrays. The GPU distance adapter sends only exact cardinal directions to the axis-only slide interface; arbitrary directions and native curves use the shared CPU path.