From f894ffd27c24687458172c40fc20fbfe1f1d0ab2 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 21 Mar 2026 12:36:04 -0400 Subject: [PATCH] feat: add IFillComparer interface and DefaultFillComparer Extracts the fill result scoring contract into IFillComparer with a DefaultFillComparer implementation that preserves the existing count-then-density lexicographic ranking via FillScore. Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Engine/Fill/DefaultFillComparer.cs | 23 ++++++++ OpenNest.Engine/IFillComparer.cs | 14 +++++ OpenNest.Tests/FillComparerTests.cs | 65 +++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 OpenNest.Engine/Fill/DefaultFillComparer.cs create mode 100644 OpenNest.Engine/IFillComparer.cs create mode 100644 OpenNest.Tests/FillComparerTests.cs diff --git a/OpenNest.Engine/Fill/DefaultFillComparer.cs b/OpenNest.Engine/Fill/DefaultFillComparer.cs new file mode 100644 index 0000000..cb6e627 --- /dev/null +++ b/OpenNest.Engine/Fill/DefaultFillComparer.cs @@ -0,0 +1,23 @@ +using OpenNest.Geometry; +using System.Collections.Generic; + +namespace OpenNest.Engine.Fill +{ + /// + /// Ranks fill results by count first, then density. + /// This is the original scoring logic used by DefaultNestEngine. + /// + public class DefaultFillComparer : IFillComparer + { + public bool IsBetter(List candidate, List current, Box workArea) + { + if (candidate == null || candidate.Count == 0) + return false; + + if (current == null || current.Count == 0) + return true; + + return FillScore.Compute(candidate, workArea) > FillScore.Compute(current, workArea); + } + } +} diff --git a/OpenNest.Engine/IFillComparer.cs b/OpenNest.Engine/IFillComparer.cs new file mode 100644 index 0000000..5d38294 --- /dev/null +++ b/OpenNest.Engine/IFillComparer.cs @@ -0,0 +1,14 @@ +using OpenNest.Geometry; +using System.Collections.Generic; + +namespace OpenNest.Engine +{ + /// + /// Determines whether a candidate fill result is better than the current best. + /// Implementations must be stateless and thread-safe. + /// + public interface IFillComparer + { + bool IsBetter(List candidate, List current, Box workArea); + } +} diff --git a/OpenNest.Tests/FillComparerTests.cs b/OpenNest.Tests/FillComparerTests.cs new file mode 100644 index 0000000..da58eb8 --- /dev/null +++ b/OpenNest.Tests/FillComparerTests.cs @@ -0,0 +1,65 @@ +using OpenNest.Engine; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; + +namespace OpenNest.Tests; + +public class DefaultFillComparerTests +{ + private readonly IFillComparer comparer = new DefaultFillComparer(); + private readonly Box workArea = new(0, 0, 100, 100); + + [Fact] + public void NullCandidate_ReturnsFalse() + { + var current = new List { TestHelpers.MakePartAt(0, 0, 10) }; + Assert.False(comparer.IsBetter(null, current, workArea)); + } + + [Fact] + public void EmptyCandidate_ReturnsFalse() + { + var current = new List { TestHelpers.MakePartAt(0, 0, 10) }; + Assert.False(comparer.IsBetter(new List(), current, workArea)); + } + + [Fact] + public void NullCurrent_ReturnsTrue() + { + var candidate = new List { TestHelpers.MakePartAt(0, 0, 10) }; + Assert.True(comparer.IsBetter(candidate, null, workArea)); + } + + [Fact] + public void HigherCount_Wins() + { + var candidate = new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(20, 0, 10), + TestHelpers.MakePartAt(40, 0, 10) + }; + var current = new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(20, 0, 10) + }; + Assert.True(comparer.IsBetter(candidate, current, workArea)); + } + + [Fact] + public void SameCount_HigherDensityWins() + { + var candidate = new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(12, 0, 10) + }; + var current = new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(50, 0, 10) + }; + Assert.True(comparer.IsBetter(candidate, current, workArea)); + } +}