diff --git a/OpenNest.Engine.Tests/Jobs/ClampZoneSheetFrameTests.cs b/OpenNest.Engine.Tests/Jobs/ClampZoneSheetFrameTests.cs index a6e169e..5f2c005 100644 --- a/OpenNest.Engine.Tests/Jobs/ClampZoneSheetFrameTests.cs +++ b/OpenNest.Engine.Tests/Jobs/ClampZoneSheetFrameTests.cs @@ -79,6 +79,26 @@ public class ClampZoneSheetFrameTests Assert.False(report.CanPost(false)); } + [Fact] + public void StockClampZonesCannotBeEditedThroughTheReturnedZones() + { + // Stock is immutable input: editing a returned zone must not move the stock's clamp + // away from the zones its materialized plates (validation, fill, posting) use. + var live = new Plate(24, 48); + live.ClampZones.Add(new ClampZone("z", new Box(10, 0, 1.5, 0.625))); + var stock = DrawingJobMapper.FromPlate("s", live, 1); + + stock.ClampZones[0].Bounds.X += 10; + foreach (var zone in stock.ClampZones) + zone.Bounds.Y = 5; + + var current = Assert.Single(stock.ClampZones); + Assert.Equal(10, current.Bounds.X); + Assert.Equal(0, current.Bounds.Y); + var plate = DrawingJobMapper.CreatePlate(stock); + Assert.Equal(stock.ClampZones.Select(Bits), ClampZoneFrame.ToPlacement(plate).Select(Bits)); + } + [Theory] [InlineData(1)] [InlineData(2)] diff --git a/OpenNest.Engine/Jobs/NestPlateStock.cs b/OpenNest.Engine/Jobs/NestPlateStock.cs index a855ea7..649b9be 100644 --- a/OpenNest.Engine/Jobs/NestPlateStock.cs +++ b/OpenNest.Engine/Jobs/NestPlateStock.cs @@ -60,14 +60,16 @@ public sealed class NestPlateStock zones[i] = new ClampZone(clampZones[i].Id, clampZones[i].Bounds.Translate(left, bottom)); sheetZones[i] = new ClampZone(clampZones[i].Id, clampZones[i].Bounds); } - ClampZones = zones; + ClampZones = new DetachedZones(zones); SheetClampZones = sheetZones; } } /// /// Keep-out rectangles in the placement frame (quadrant applied), translated exactly - /// like . Empty when the plate defines none; never null. + /// like . Empty when the plate defines none; never null. Every + /// read returns detached copies: zone bounds are mutable, and the stock must stay + /// consistent with the zones its materialized plates use. /// public IReadOnlyList ClampZones { get; } @@ -102,4 +104,26 @@ public sealed class NestPlateStock /// True when a width (X) by height (Y) envelope fits the work area within epsilon. public bool Fits(double width, double height, double epsilon = 1e-9) => width <= workArea.Length + epsilon && height <= workArea.Width + epsilon; + + private sealed class DetachedZones : IReadOnlyList + { + private readonly ClampZone[] zones; + + public DetachedZones(ClampZone[] zones) => this.zones = zones; + + public int Count => zones.Length; + + public ClampZone this[int index] => Copy(zones[index]); + + public IEnumerator GetEnumerator() + { + foreach (var zone in zones) + yield return Copy(zone); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + // The ClampZone constructor copies the rectangle, keeping its exact bits. + private static ClampZone Copy(ClampZone zone) => new(zone.Id, zone.Bounds); + } }