From 4e0699de7ab0cf0ebf2250d6e2988a84c5477841 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 16 Oct 2026 12:15:27 -0400 Subject: [PATCH] fix(rectangles): break exact material-area ties the same way on every platform ZeroClampMixedShapeLayoutIsBitIdenticalToBaseline has failed on Windows since it was added in de9a087 while passing on Linux CI (bisect pointing at c374e02 was an artifact: the assumed-good start commit fails too). No commit changed the layout; the two platforms always chose differently. In that fixture two candidate sheets (ContactPoint/Global and BestShortSide/Ordered) both place all 30 parts at the same cost, so Choose falls to its "more material placed" tie-break. SheetPacker summed material area in placement order, and the disc's area differs in the last bit between the Windows CRT and glibc math libraries. On Linux the two orders rounded to ...253 vs ...257, so the later candidate won on rounding noise; on Windows both rounded to ...257 and the tie fell to enumeration order as Choose documents. Sum a plan's area per type in type order instead (the same form as the engine's demand area), so plans placing the same parts get bit-identical areas and exact ties keep enumeration order everywhere. No tolerance is loosened. The Windows layout is unchanged; Linux now matches it, so the frozen hash moves to the Windows value, verified identical on both. Verified: full Engine.Tests pass on Windows and Linux (1105 passed, 2 skipped); Rectangles and Default benchmark results on the six synthetic nests are identical before and after (validity, placed parts, cost, utilization, plates). Co-Authored-By: Claude Opus 5.5 --- .../NestingEngines/RectanglesClampZoneTests.cs | 4 +++- OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/OpenNest.Engine.Tests/NestingEngines/RectanglesClampZoneTests.cs b/OpenNest.Engine.Tests/NestingEngines/RectanglesClampZoneTests.cs index 16b1dddd..5cf4cb11 100644 --- a/OpenNest.Engine.Tests/NestingEngines/RectanglesClampZoneTests.cs +++ b/OpenNest.Engine.Tests/NestingEngines/RectanglesClampZoneTests.cs @@ -43,7 +43,9 @@ public class RectanglesClampZoneTests new[] { Stock("sheet", 48, 96, spacing: 0.25) })))); [Theory] - [InlineData("9B343B6C5CF2CE8753F2E04ACB0A9C4D5921F4ADC15AA2586AF466769C0F019B")] + // Two complete candidates tie here on cost and placed material; the tie must fall to + // enumeration order on every platform (hash identical on Windows and Linux). + [InlineData("BEEEDEC81A7F987720FB5F331873103AD2470C839D568CE587F230BC39D210CC")] public void ZeroClampMixedShapeLayoutIsBitIdenticalToBaseline(string hash) => Assert.Equal(hash, LayoutHash(Engine().Solve(Job( new[] diff --git a/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs b/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs index 51f3b82b..684724fb 100644 --- a/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs +++ b/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs @@ -70,14 +70,18 @@ internal static class SheetPacker .Select(p => new Placed(types[p.Type], types[p.Type].Orientations[p.Size], p.X, p.Y)) .ToList(); - var area = 0.0; + var counts = new int[types.Count]; Box? envelope = null; foreach (var p in placed) { - area += p.Type.MaterialArea; + counts[p.Type.Index]++; var box = new Box(work.Left + p.Left, work.Bottom + p.Bottom, p.Orientation.Width, p.Orientation.Height); envelope = envelope == null ? box : Union(envelope, box); } + // Summed per type in type order, the same form as the engine's demand area: plans placing + // the same parts get bit-identical areas, so the engine's material-area tie-break cannot + // be decided by placement-order rounding (which differs across platform math libraries). + var area = types.Sum(t => counts[t.Index] * t.MaterialArea); var world = placed .Select(p => p with { Left = work.Left + p.Left, Bottom = work.Bottom + p.Bottom })