From 104b0aef9928ff3131038bde5304b49295d67a26 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 12 Oct 2026 23:08:32 -0400 Subject: [PATCH] fix(clamps): retain complete groups ahead of partial fill tails --- .../Jobs/ClampZoneInteractiveFillTests.cs | 20 +++++++++++++++++++ .../Jobs/Placement/PlateFillService.cs | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs b/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs index 7430b0f..e42b36a 100644 --- a/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs +++ b/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs @@ -127,6 +127,26 @@ public class ClampZoneInteractiveFillTests AssertClearOfClamps(result, new Box(15, 0, 1.5, 0.625)); } + [Fact] + public void FillGroupRetainsCompleteCopiesAheadOfAPartialTrailingCopy() + { + var drawing = RectDrawing(2, 2); + var group = new List + { + new(drawing, new Vector(0, 0)), new(drawing, new Vector(3, 0)), + }; + var plate = new Plate(new Size(20, 20)) { PartSpacing = 0 }; + var area = new Box(0, 0, 8, 2); + var original = PlateFillService.FillGroup("Fill", plate, group, area, + null, CancellationToken.None); + Assert.True(original.Count > group.Count && original.Count % group.Count != 0, + $"expected a full copy and partial tail, got {original.Count}"); + plate.ClampZones.Add(new ClampZone("remote", new Box(18, 0, 1, 0.5))); + var clamped = PlateFillService.FillGroup("Fill", plate, group, area, + null, CancellationToken.None); + Assert.Equal(group.Count, clamped.Count); + } + [Fact] public void FillGroupRejectsTheWholeCopyWhenOnlyOneMemberTouchesAClamp() { diff --git a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs index 5a11871..b75e65e 100644 --- a/OpenNest.Engine/Jobs/Placement/PlateFillService.cs +++ b/OpenNest.Engine/Jobs/Placement/PlateFillService.cs @@ -203,9 +203,11 @@ public static class PlateFillService Box area, List demand = null, List previouslyPlaced = null, int groupSize = 1) { - if (proposed == null || groupSize < 1 || proposed.Count % groupSize != 0) + if (proposed == null || groupSize < 1) return; - for (var start = 0; start < proposed.Count; start += groupSize) + // FillLinear may append an incomplete trailing tile. Keep complete copies + // ahead of it, but never expose the incomplete tail as a group. + for (var start = 0; start + groupSize <= proposed.Count; start += groupSize) { // A group is one proposal: never return just its frame or just its // insert when a member collides with a clamp or occupied material.