From 9f7580a486dd2519f7bfdf14466c73490972c772 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 4 Oct 2026 21:51:43 -0400 Subject: [PATCH] fix(cutting): bind replay proposals to exact captured pose bits --- .../CuttingPlanning/CuttingPlanService.cs | 8 +++- .../CuttingPlanning/ReplayPoseBindingTests.cs | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 OpenNest.Tests/CuttingPlanning/ReplayPoseBindingTests.cs diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs index 4df3631..89cefb7 100644 --- a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs +++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs @@ -190,8 +190,9 @@ public static class CuttingPlanService { token.ThrowIfCancellationRequested(); var source = snapshot.Placements[proposal.SourceOrdinal]; - if (!ReferenceEquals(source.SourcePart, proposal.SourcePart) || source.Location != proposal.Location - || source.Rotation != proposal.Rotation || source.LeadInsLocked != proposal.LeadInsLocked + if (!ReferenceEquals(source.SourcePart, proposal.SourcePart) + || !SameBits(source.Location.X, proposal.Location.X) || !SameBits(source.Location.Y, proposal.Location.Y) + || !SameBits(source.Rotation, proposal.Rotation) || source.LeadInsLocked != proposal.LeadInsLocked || !ReferenceEquals(source.Prepared, proposal.Prepared) || !ReferenceEquals(source.Material, proposal.Material) || snapshot.PreservePartOrder && proposal.SourceOrdinal != order.TakeWhile(p => !ReferenceEquals(p, proposal)).Count()) return new(CuttingPlanStatus.InvalidInput, expansions: expansions); @@ -272,6 +273,9 @@ public static class CuttingPlanService } } + private static bool SameBits(double source, double proposed) => + BitConverter.DoubleToInt64Bits(source) == BitConverter.DoubleToInt64Bits(proposed); + private static IEnumerable Map(CuttingPlanSnapshot snapshot, IEnumerable findings) => findings.Select(finding => { diff --git a/OpenNest.Tests/CuttingPlanning/ReplayPoseBindingTests.cs b/OpenNest.Tests/CuttingPlanning/ReplayPoseBindingTests.cs new file mode 100644 index 0000000..e8c9151 --- /dev/null +++ b/OpenNest.Tests/CuttingPlanning/ReplayPoseBindingTests.cs @@ -0,0 +1,47 @@ +using OpenNest.CNC.CuttingPlanning; +using OpenNest.Engine.CuttingPlanning; +using OpenNest.Geometry; + +namespace OpenNest.Tests.CuttingPlanning; + +public class ReplayPoseBindingTests +{ + [Theory] + [InlineData("near-x")] + [InlineData("near-y")] + [InlineData("zero-x")] + [InlineData("zero-y")] + [InlineData("zero-rotation")] + [InlineData("matching-bits")] + public void ExactReplay_BindsEveryPoseScalarByBits(string fault) + { + var part = new Part(new Drawing("pose", LeadPathValidationTests.Rectangle(0, 0, 10, 10))); + var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], new Vector(-2, 5), + confirmedParameters: ExplicitContourTests.Parameters())); + var ready = CuttingPlanService.Plan(snapshot); + Assert.Equal(CuttingPlanStatus.Ready, ready.Status); + var source = Assert.Single(snapshot.Placements); + var proposal = Assert.Single(ready.ProposedOrder); + var x = source.Location.X; var y = source.Location.Y; var rotation = source.Rotation; + var negativeZero = BitConverter.Int64BitsToDouble(long.MinValue); + if (fault == "near-x") x += 1e-10; + if (fault == "near-y") y += 1e-10; + if (fault == "zero-x") x = negativeZero; + if (fault == "zero-y") y = negativeZero; + if (fault == "zero-rotation") rotation = negativeZero; + // These deliberately remain equal under the old geometric/numeric operators. + Assert.True(source.Location == new Vector(x, y)); + Assert.True(source.Rotation == rotation); + var sameBits = BitConverter.DoubleToInt64Bits(source.Location.X) == BitConverter.DoubleToInt64Bits(x) + && BitConverter.DoubleToInt64Bits(source.Location.Y) == BitConverter.DoubleToInt64Bits(y) + && BitConverter.DoubleToInt64Bits(source.Rotation) == BitConverter.DoubleToInt64Bits(rotation); + Assert.Equal(fault == "matching-bits", sameBits); + var selected = new FixedProgramPlacement(source.SourcePart, source.SourceOrdinal, new Vector(x, y), rotation, + source.LeadInsLocked, proposal.Execution, proposal.CopyProgram(), source.Prepared, source.Material, proposal.ContourChoices); + var replay = CuttingPlanService.ReplayPrograms(snapshot, [selected], 0, default); + Assert.Equal(sameBits ? CuttingPlanStatus.Ready : CuttingPlanStatus.InvalidInput, replay.Status); + Assert.Equal(sameBits, replay.IndependentlyReplayed); + if (sameBits) Assert.Single(replay.ProposedOrder); + else Assert.Empty(replay.ProposedOrder); + } +}