Rename SawCut library to CutList.Core
Rename the core library project from SawCut to CutList.Core for consistent branding across the solution. This includes: - Rename project folder and .csproj file - Update namespace from SawCut to CutList.Core - Update all using statements and project references Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
75
CutList.Core/ArchUnits.cs
Normal file
75
CutList.Core/ArchUnits.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
public static class ArchUnits
|
||||
{
|
||||
public static double ParseToInches(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return 0;
|
||||
|
||||
var sb = new StringBuilder(input.Trim().ToLower());
|
||||
|
||||
// replace all units with their equivelant symbols
|
||||
sb.Replace("ft", "'");
|
||||
sb.Replace("feet", "'");
|
||||
sb.Replace("foot", "'");
|
||||
sb.Replace("inches", "\"");
|
||||
sb.Replace("inch", "\"");
|
||||
sb.Replace("in", "\"");
|
||||
|
||||
var regex = new Regex("^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$");
|
||||
|
||||
// input manipulation is done, put the value back
|
||||
input = Fraction.ReplaceFractionsWithDecimals(sb.ToString());
|
||||
|
||||
var match2 = regex.Match(input);
|
||||
|
||||
if (!match2.Success)
|
||||
throw new Exception("Input is not in a valid format.");
|
||||
|
||||
var feet = match2.Groups["Feet"];
|
||||
var inches = match2.Groups["Inches"];
|
||||
|
||||
var totalInches = 0.0;
|
||||
|
||||
if (feet.Success)
|
||||
{
|
||||
var x = double.Parse(feet.Value.Remove(feet.Length - 1));
|
||||
totalInches += x * 12;
|
||||
}
|
||||
|
||||
if (inches.Success)
|
||||
{
|
||||
var x = double.Parse(inches.Value.Remove(inches.Length - 1));
|
||||
totalInches += x;
|
||||
}
|
||||
|
||||
return Math.Round(totalInches, 8);
|
||||
}
|
||||
|
||||
public static double ParseToFeet(string input)
|
||||
{
|
||||
var inches = ParseToInches(input);
|
||||
return Math.Round(inches / 12.0, 8);
|
||||
}
|
||||
|
||||
public static string FormatFromInches(double totalInches)
|
||||
{
|
||||
var feet = Math.Floor(totalInches / 12.0);
|
||||
var inches = FormatHelper.ConvertToMixedFraction(totalInches - (feet * 12.0));
|
||||
|
||||
if (feet > 0)
|
||||
{
|
||||
return $"{feet}' {inches}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{inches}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
CutList.Core/BestCombination.cs
Normal file
112
CutList.Core/BestCombination.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
public static class BestCombination
|
||||
{
|
||||
public static BestComboResult FindFrom2(double length1, double length2, double stockLength)
|
||||
{
|
||||
var result = InitializeResult(length1, length2);
|
||||
|
||||
bool item1Fits = length1 <= stockLength;
|
||||
bool item2Fits = length2 <= stockLength;
|
||||
|
||||
if (!item1Fits && !item2Fits)
|
||||
return null;
|
||||
|
||||
if (!item1Fits)
|
||||
return CalculateSingleItemResult(result, length2, stockLength, false);
|
||||
|
||||
if (!item2Fits)
|
||||
return CalculateSingleItemResult(result, length1, stockLength, true);
|
||||
|
||||
return CalculateOptimalCombination(result, length1, length2, stockLength);
|
||||
}
|
||||
|
||||
private static BestComboResult InitializeResult(double length1, double length2)
|
||||
{
|
||||
return new BestComboResult
|
||||
{
|
||||
Item1Length = length1,
|
||||
Item2Length = length2
|
||||
};
|
||||
}
|
||||
|
||||
private static BestComboResult CalculateSingleItemResult(BestComboResult result, double length, double stockLength, bool isItem1)
|
||||
{
|
||||
if (isItem1)
|
||||
{
|
||||
result.Item1Count = (int)Math.Floor(stockLength / length);
|
||||
result.Item2Count = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Item1Count = 0;
|
||||
result.Item2Count = (int)Math.Floor(stockLength / length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static BestComboResult CalculateOptimalCombination(BestComboResult result, double length1, double length2, double stockLength)
|
||||
{
|
||||
var maxCountLength1 = (int)Math.Floor(stockLength / length1);
|
||||
result.Item1Count = maxCountLength1;
|
||||
result.Item2Count = 0;
|
||||
|
||||
var remnant = stockLength - maxCountLength1 * length1;
|
||||
|
||||
for (int countLength1 = 0; countLength1 <= maxCountLength1; ++countLength1)
|
||||
{
|
||||
var remnant1 = stockLength - countLength1 * length1;
|
||||
|
||||
if (remnant1 >= length2)
|
||||
remnant = UpdateResultForTwoItems(result, length2, remnant1, remnant, countLength1);
|
||||
else
|
||||
remnant = UpdateResultForOneItem(result, remnant1, remnant, countLength1);
|
||||
|
||||
if (remnant.IsEqualTo(0))
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static double UpdateResultForTwoItems(BestComboResult result, double length2, double remnant1, double currentRemnant, int countLength1)
|
||||
{
|
||||
var countLength2 = (int)Math.Floor(remnant1 / length2);
|
||||
var remnant2 = remnant1 - countLength2 * length2;
|
||||
|
||||
if (remnant2 < currentRemnant)
|
||||
{
|
||||
result.Item1Count = countLength1;
|
||||
result.Item2Count = countLength2;
|
||||
return remnant2;
|
||||
}
|
||||
|
||||
return currentRemnant;
|
||||
}
|
||||
|
||||
private static double UpdateResultForOneItem(BestComboResult result, double remnant1, double currentRemnant, int countLength1)
|
||||
{
|
||||
if (remnant1 < currentRemnant)
|
||||
{
|
||||
result.Item1Count = countLength1;
|
||||
result.Item2Count = 0;
|
||||
return remnant1;
|
||||
}
|
||||
|
||||
return currentRemnant;
|
||||
}
|
||||
}
|
||||
|
||||
public class BestComboResult
|
||||
{
|
||||
public double Item1Length { get; set; }
|
||||
public int Item1Count { get; set; }
|
||||
public double Item1TotalLength => Item1Length * Item1Count;
|
||||
|
||||
public double Item2Length { get; set; }
|
||||
public int Item2Count { get; set; }
|
||||
public double Item2TotalLength => Item2Length * Item2Count;
|
||||
}
|
||||
}
|
||||
79
CutList.Core/Bin.cs
Normal file
79
CutList.Core/Bin.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
public class Bin
|
||||
{
|
||||
private readonly List<BinItem> _items;
|
||||
|
||||
public Bin(double length)
|
||||
{
|
||||
_items = new List<BinItem>();
|
||||
Length = length;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BinItem> Items => _items;
|
||||
|
||||
public void AddItem(BinItem item)
|
||||
{
|
||||
_items.Add(item);
|
||||
}
|
||||
|
||||
public void AddItems(IEnumerable<BinItem> items)
|
||||
{
|
||||
_items.AddRange(items);
|
||||
}
|
||||
|
||||
public void RemoveItem(BinItem item)
|
||||
{
|
||||
_items.Remove(item);
|
||||
}
|
||||
|
||||
public void SortItems(Comparison<BinItem> comparison)
|
||||
{
|
||||
_items.Sort(comparison);
|
||||
}
|
||||
|
||||
public double Spacing { get; set; }
|
||||
|
||||
public double Length { get; set; }
|
||||
|
||||
public double UsedLength
|
||||
{
|
||||
get
|
||||
{
|
||||
var usedLength = Math.Round(Items.Sum(i => i.Length) + Spacing * Items.Count, 8);
|
||||
|
||||
if (usedLength > Length && (usedLength - Length) <= Spacing)
|
||||
return Length;
|
||||
|
||||
return Math.Round(Items.Sum(i => i.Length) + Spacing * Items.Count, 8);
|
||||
}
|
||||
}
|
||||
|
||||
public double RemainingLength
|
||||
{
|
||||
get { return Math.Round(Length - UsedLength, 8); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a ratio of UsedLength to TotalLength
|
||||
/// 1.0 = 100% utilization
|
||||
/// </summary>
|
||||
public double Utilization
|
||||
{
|
||||
get { return UsedLength / Length; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var totalLength = FormatHelper.ConvertToMixedFraction(Math.Round(Length, 4));
|
||||
var remainingLength = FormatHelper.ConvertToMixedFraction(Math.Round(RemainingLength, 4));
|
||||
var utilitation = Math.Round(Utilization * 100, 2);
|
||||
|
||||
return $"Length: {totalLength}, {remainingLength} remaining, {Items.Count} items, {utilitation}% utilization";
|
||||
}
|
||||
}
|
||||
}
|
||||
113
CutList.Core/BinComparer.cs
Normal file
113
CutList.Core/BinComparer.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares bins to determine if they are identical (same items in same order).
|
||||
/// </summary>
|
||||
public class BinComparer : IEqualityComparer<Bin>
|
||||
{
|
||||
public bool Equals(Bin x, Bin y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return true;
|
||||
if (x == null || y == null) return false;
|
||||
|
||||
// Check basic properties
|
||||
if (Math.Abs(x.Length - y.Length) > 0.0001) return false;
|
||||
if (Math.Abs(x.Spacing - y.Spacing) > 0.0001) return false;
|
||||
|
||||
// Check item count
|
||||
if (x.Items.Count != y.Items.Count) return false;
|
||||
|
||||
// Check each item in order
|
||||
for (int i = 0; i < x.Items.Count; i++)
|
||||
{
|
||||
var itemX = x.Items[i];
|
||||
var itemY = y.Items[i];
|
||||
|
||||
if (itemX.Name != itemY.Name) return false;
|
||||
if (Math.Abs(itemX.Length - itemY.Length) > 0.0001) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(Bin bin)
|
||||
{
|
||||
if (bin == null) return 0;
|
||||
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 23 + bin.Length.GetHashCode();
|
||||
hash = hash * 23 + bin.Spacing.GetHashCode();
|
||||
hash = hash * 23 + bin.Items.Count.GetHashCode();
|
||||
|
||||
// Include first and last item in hash for better distribution
|
||||
if (bin.Items.Count > 0)
|
||||
{
|
||||
var firstItem = bin.Items[0];
|
||||
hash = hash * 23 + (firstItem.Name?.GetHashCode() ?? 0);
|
||||
hash = hash * 23 + firstItem.Length.GetHashCode();
|
||||
|
||||
if (bin.Items.Count > 1)
|
||||
{
|
||||
var lastItem = bin.Items[bin.Items.Count - 1];
|
||||
hash = hash * 23 + (lastItem.Name?.GetHashCode() ?? 0);
|
||||
hash = hash * 23 + lastItem.Length.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for grouping bins.
|
||||
/// </summary>
|
||||
public static class BinGroupingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Groups identical bins together and returns a list of BinGroup objects.
|
||||
/// </summary>
|
||||
public static List<BinGroup> GroupIdenticalBins(List<Bin> bins)
|
||||
{
|
||||
if (bins == null || bins.Count == 0)
|
||||
return new List<BinGroup>();
|
||||
|
||||
var comparer = new BinComparer();
|
||||
var groups = new List<BinGroup>();
|
||||
var processed = new HashSet<int>();
|
||||
|
||||
for (int i = 0; i < bins.Count; i++)
|
||||
{
|
||||
if (processed.Contains(i))
|
||||
continue;
|
||||
|
||||
var currentBin = bins[i];
|
||||
int count = 1;
|
||||
|
||||
// Find all identical bins
|
||||
for (int j = i + 1; j < bins.Count; j++)
|
||||
{
|
||||
if (processed.Contains(j))
|
||||
continue;
|
||||
|
||||
if (comparer.Equals(currentBin, bins[j]))
|
||||
{
|
||||
count++;
|
||||
processed.Add(j);
|
||||
}
|
||||
}
|
||||
|
||||
processed.Add(i);
|
||||
groups.Add(new BinGroup(currentBin, count));
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
CutList.Core/BinGroup.cs
Normal file
51
CutList.Core/BinGroup.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a group of identical bins (bins with the same items in the same order).
|
||||
/// Used for displaying consolidated results.
|
||||
/// </summary>
|
||||
public class BinGroup
|
||||
{
|
||||
public BinGroup(Bin representativeBin, int count)
|
||||
{
|
||||
if (representativeBin == null)
|
||||
throw new ArgumentNullException(nameof(representativeBin));
|
||||
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("Count must be greater than zero", nameof(count));
|
||||
|
||||
RepresentativeBin = representativeBin;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A representative bin from this group (all bins in the group are identical).
|
||||
/// </summary>
|
||||
public Bin RepresentativeBin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of identical bins in this group.
|
||||
/// </summary>
|
||||
public int Count { get; }
|
||||
|
||||
// Properties that delegate to the representative bin for data binding
|
||||
public double Spacing => RepresentativeBin.Spacing;
|
||||
public double Length => RepresentativeBin.Length;
|
||||
public double UsedLength => RepresentativeBin.UsedLength;
|
||||
public double RemainingLength => RepresentativeBin.RemainingLength;
|
||||
public double Utilization => RepresentativeBin.Utilization;
|
||||
public IReadOnlyList<BinItem> Items => RepresentativeBin.Items;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Count == 1)
|
||||
return RepresentativeBin.ToString();
|
||||
|
||||
return $"{RepresentativeBin} (x{Count})";
|
||||
}
|
||||
}
|
||||
}
|
||||
97
CutList.Core/BinItem.cs
Normal file
97
CutList.Core/BinItem.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an item to be placed in a bin.
|
||||
/// Enforces business rules for valid items.
|
||||
/// </summary>
|
||||
public class BinItem
|
||||
{
|
||||
private string _name;
|
||||
private double _length;
|
||||
|
||||
public BinItem(string name, double length)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Item name cannot be empty", nameof(name));
|
||||
|
||||
if (length <= 0)
|
||||
throw new ArgumentException("Item length must be greater than zero", nameof(length));
|
||||
|
||||
_name = name;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for serialization only.
|
||||
/// Use the parameterized constructor for creating valid instances.
|
||||
/// </summary>
|
||||
public BinItem()
|
||||
{
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new ArgumentException("Item name cannot be empty", nameof(value));
|
||||
_name = value;
|
||||
}
|
||||
}
|
||||
|
||||
public double Length
|
||||
{
|
||||
get => _length;
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentException("Item length must be greater than zero", nameof(value));
|
||||
_length = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this item can fit in the given available length.
|
||||
/// </summary>
|
||||
public bool CanFitIn(double availableLength)
|
||||
{
|
||||
return Length <= availableLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this item can fit in the given available length with spacing.
|
||||
/// </summary>
|
||||
public bool CanFitInWithSpacing(double availableLength, double spacing)
|
||||
{
|
||||
return Length + spacing <= availableLength;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} ({Length}\")";
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is BinItem other)
|
||||
{
|
||||
return Name == other.Name && Length.IsEqualTo(other.Length);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 23 + (Name?.GetHashCode() ?? 0);
|
||||
hash = hash * 23 + Length.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
CutList.Core/CutList.Core.csproj
Normal file
13
CutList.Core/CutList.Core.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- Disable auto-generation of assembly attributes to avoid conflicts with AssemblyInfo.cs -->
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
68
CutList.Core/FormatHelper.cs
Normal file
68
CutList.Core/FormatHelper.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides formatting utilities for displaying measurements and values.
|
||||
/// </summary>
|
||||
public static class FormatHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a decimal measurement to a mixed fraction string representation.
|
||||
/// </summary>
|
||||
/// <param name="input">The decimal value to convert</param>
|
||||
/// <param name="precision">The denominator precision (default 16 for 1/16")</param>
|
||||
/// <returns>A string in the format "whole-numerator/denominator"</returns>
|
||||
public static string ConvertToMixedFraction(decimal input, int precision = 16)
|
||||
{
|
||||
// Get the whole number part
|
||||
int wholeNumber = (int)input;
|
||||
|
||||
// Get the fractional part
|
||||
decimal fractionalPart = Math.Abs(input - wholeNumber);
|
||||
|
||||
if (fractionalPart == 0)
|
||||
{
|
||||
return wholeNumber.ToString();
|
||||
}
|
||||
|
||||
// Convert the fractional part to a fraction
|
||||
int numerator = (int)(fractionalPart * precision);
|
||||
int denominator = precision;
|
||||
|
||||
// Simplify the fraction
|
||||
int gcd = GetGreatestCommonDivisor(numerator, denominator);
|
||||
numerator /= gcd;
|
||||
denominator /= gcd;
|
||||
|
||||
// If rounding wiped out the fraction → return whole number only
|
||||
if (numerator == 0)
|
||||
{
|
||||
return wholeNumber.ToString();
|
||||
}
|
||||
|
||||
return $"{wholeNumber}-{numerator}/{denominator}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a double measurement to a mixed fraction string representation.
|
||||
/// </summary>
|
||||
/// <param name="input">The double value to convert</param>
|
||||
/// <returns>A string in the format "whole-numerator/denominator"</returns>
|
||||
public static string ConvertToMixedFraction(double input)
|
||||
{
|
||||
return ConvertToMixedFraction((decimal)input);
|
||||
}
|
||||
|
||||
private static int GetGreatestCommonDivisor(int a, int b)
|
||||
{
|
||||
while (b != 0)
|
||||
{
|
||||
int temp = b;
|
||||
b = a % b;
|
||||
a = temp;
|
||||
}
|
||||
return Math.Abs(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
CutList.Core/Fraction.cs
Normal file
85
CutList.Core/Fraction.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
public static class Fraction
|
||||
{
|
||||
public static readonly Regex FractionRegex = new Regex(@"((?<WholeNum>\d+)(\ |-))?(?<Fraction>\d+\/\d+)");
|
||||
|
||||
public static bool IsValid(string s)
|
||||
{
|
||||
return FractionRegex.IsMatch(s);
|
||||
}
|
||||
|
||||
public static double Parse(string s)
|
||||
{
|
||||
var match = FractionRegex.Match(s);
|
||||
|
||||
if (!match.Success)
|
||||
throw new Exception("Invalid format.");
|
||||
|
||||
var value = 0.0;
|
||||
|
||||
var wholeNumGroup = match.Groups["WholeNum"];
|
||||
var fractionGroup = match.Groups["Fraction"];
|
||||
|
||||
if (wholeNumGroup.Success)
|
||||
{
|
||||
value = double.Parse(wholeNumGroup.Value);
|
||||
}
|
||||
|
||||
if (fractionGroup.Success)
|
||||
{
|
||||
var parts = fractionGroup.Value.Split('/');
|
||||
var numerator = double.Parse(parts[0]);
|
||||
var denominator = double.Parse(parts[1]);
|
||||
|
||||
value += Math.Round(numerator / denominator, 8);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static string ReplaceFractionsWithDecimals(string input)
|
||||
{
|
||||
var sb = new StringBuilder(input);
|
||||
|
||||
// find all matches and sort by descending index number to avoid
|
||||
// changing all previous index numbers when the fraction is replaced
|
||||
// with the decimal equivalent.
|
||||
var fractionMatches = FractionRegex.Matches(sb.ToString())
|
||||
.Cast<Match>()
|
||||
.OrderByDescending(m => m.Index);
|
||||
|
||||
foreach (var fractionMatch in fractionMatches)
|
||||
{
|
||||
// convert the fraction to a decimal value
|
||||
var decimalValue = Parse(fractionMatch.Value);
|
||||
|
||||
// remove the fraction and insert the decimal value in its place.
|
||||
sb.Remove(fractionMatch.Index, fractionMatch.Length);
|
||||
sb.Insert(fractionMatch.Index, decimalValue);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static bool TryParse(string s, out double fraction)
|
||||
{
|
||||
try
|
||||
{
|
||||
fraction = Parse(s);
|
||||
}
|
||||
catch
|
||||
{
|
||||
fraction = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
CutList.Core/MultiBin.cs
Normal file
125
CutList.Core/MultiBin.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a type of bin with quantity and priority.
|
||||
/// Enforces business rules for valid bin configurations.
|
||||
/// </summary>
|
||||
public class MultiBin
|
||||
{
|
||||
private int _quantity;
|
||||
private double _length;
|
||||
private int _priority;
|
||||
|
||||
public MultiBin(double length, int quantity = 1, int priority = 25)
|
||||
{
|
||||
if (length <= 0)
|
||||
throw new ArgumentException("Bin length must be greater than zero", nameof(length));
|
||||
|
||||
if (quantity < -1 || quantity == 0)
|
||||
throw new ArgumentException("Quantity must be positive or -1 for unlimited", nameof(quantity));
|
||||
|
||||
_length = length;
|
||||
_quantity = quantity;
|
||||
_priority = priority;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for serialization only.
|
||||
/// Use the parameterized constructor for creating valid instances.
|
||||
/// </summary>
|
||||
public MultiBin()
|
||||
{
|
||||
_quantity = 1;
|
||||
_priority = 25;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quantity of bins available. Use -1 for unlimited bins.
|
||||
/// </summary>
|
||||
public int Quantity
|
||||
{
|
||||
get => _quantity;
|
||||
set
|
||||
{
|
||||
if (value < -1 || value == 0)
|
||||
throw new ArgumentException("Quantity must be positive or -1 for unlimited", nameof(value));
|
||||
_quantity = value;
|
||||
}
|
||||
}
|
||||
|
||||
public double Length
|
||||
{
|
||||
get => _length;
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentException("Bin length must be greater than zero", nameof(value));
|
||||
_length = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lower value priority will be used first. Default is 25.
|
||||
/// </summary>
|
||||
public int Priority
|
||||
{
|
||||
get => _priority;
|
||||
set => _priority = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this bin type has unlimited quantity.
|
||||
/// </summary>
|
||||
public bool IsUnlimited => Quantity == -1;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an item of the given length can fit in this bin.
|
||||
/// </summary>
|
||||
public bool CanFitItem(double itemLength)
|
||||
{
|
||||
return itemLength <= Length && itemLength > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the waste for a single bin if the given length is used.
|
||||
/// </summary>
|
||||
public double CalculateWaste(double usedLength)
|
||||
{
|
||||
if (usedLength < 0 || usedLength > Length)
|
||||
throw new ArgumentException("Used length must be between 0 and bin length", nameof(usedLength));
|
||||
|
||||
return Length - usedLength;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var quantityStr = IsUnlimited ? "Unlimited" : Quantity.ToString();
|
||||
return $"Bin {Length}\" (Qty: {quantityStr}, Priority: {Priority})";
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is MultiBin other)
|
||||
{
|
||||
return Quantity == other.Quantity &&
|
||||
Length.IsEqualTo(other.Length) &&
|
||||
Priority == other.Priority;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 23 + Quantity.GetHashCode();
|
||||
hash = hash * 23 + Length.GetHashCode();
|
||||
hash = hash * 23 + Priority.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
CutList.Core/Nesting/AdvancedFitEngine.cs
Normal file
253
CutList.Core/Nesting/AdvancedFitEngine.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using CutList.Core.Nesting;
|
||||
using CutList.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
public class AdvancedFitEngine : IEngine
|
||||
{
|
||||
public AdvancedFitEngine()
|
||||
{
|
||||
Bins = new List<Bin>();
|
||||
}
|
||||
|
||||
public double StockLength { get; set; }
|
||||
|
||||
public double Spacing { get; set; }
|
||||
|
||||
public int MaxBinCount { get; set; } = int.MaxValue;
|
||||
|
||||
private List<BinItem> Items { get; set; }
|
||||
|
||||
private List<Bin> Bins { get; set; }
|
||||
|
||||
public Result Pack(List<BinItem> items)
|
||||
{
|
||||
if (StockLength <= 0)
|
||||
throw new Exception("Stock length must be greater than 0");
|
||||
|
||||
Items = items.OrderByDescending(i => i.Length).ToList();
|
||||
|
||||
var result = new Result();
|
||||
var itemsTooLarge = Items.Where(i => i.Length > StockLength).ToList();
|
||||
result.AddItemsNotUsed(itemsTooLarge);
|
||||
|
||||
Items.RemoveAll(item => itemsTooLarge.Contains(item));
|
||||
|
||||
CreateBins();
|
||||
|
||||
var finalItemsTooLarge = Items.Where(i => i.Length > StockLength).ToList();
|
||||
result.AddItemsNotUsed(finalItemsTooLarge);
|
||||
result.AddBins(Bins);
|
||||
|
||||
foreach (var bin in result.Bins)
|
||||
{
|
||||
foreach (var item in bin.Items)
|
||||
{
|
||||
Items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
result.AddItemsNotUsed(Items);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CreateBins()
|
||||
{
|
||||
while (Items.Count > 0 && CanAddMoreBins())
|
||||
{
|
||||
var bin = new Bin(StockLength)
|
||||
{
|
||||
Spacing = Spacing
|
||||
};
|
||||
|
||||
FillBin(bin);
|
||||
|
||||
while (TryImprovePacking(bin))
|
||||
{
|
||||
}
|
||||
|
||||
bin.SortItems((a, b) =>
|
||||
{
|
||||
int comparison = b.Length.CompareTo(a.Length);
|
||||
return comparison != 0 ? comparison : a.Length.CompareTo(b.Length);
|
||||
});
|
||||
|
||||
Bins.Add(bin);
|
||||
|
||||
CreateDuplicateBins(bin);
|
||||
}
|
||||
|
||||
Bins = Bins
|
||||
.OrderByDescending(b => b.Utilization)
|
||||
.ThenBy(b => b.Items.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private bool CanAddMoreBins()
|
||||
{
|
||||
if (MaxBinCount == -1)
|
||||
return true;
|
||||
|
||||
if (Bins.Count < MaxBinCount)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void FillBin(Bin bin)
|
||||
{
|
||||
for (int i = 0; i < Items.Count; i++)
|
||||
{
|
||||
if (bin.RemainingLength >= Items[i].Length)
|
||||
{
|
||||
bin.AddItem(Items[i]);
|
||||
Items.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateDuplicateBins(Bin originalBin)
|
||||
{
|
||||
// Count how many times the bin can be duplicated
|
||||
int duplicateCount = GetDuplicateCount(originalBin);
|
||||
|
||||
for (int i = 0; i < duplicateCount; i++)
|
||||
{
|
||||
if (!CanAddMoreBins())
|
||||
break;
|
||||
|
||||
var newBin = new Bin(originalBin.Length)
|
||||
{
|
||||
Spacing = Spacing
|
||||
};
|
||||
|
||||
foreach (var item in originalBin.Items)
|
||||
{
|
||||
var newItem = Items.FirstOrDefault(a => a.Length == item.Length);
|
||||
newBin.AddItem(newItem);
|
||||
Items.Remove(newItem);
|
||||
}
|
||||
|
||||
Bins.Add(newBin);
|
||||
}
|
||||
}
|
||||
|
||||
private int GetDuplicateCount(Bin bin)
|
||||
{
|
||||
int count = int.MaxValue;
|
||||
|
||||
foreach (var item in bin.Items.GroupBy(i => i.Length))
|
||||
{
|
||||
int availableCount = Items.Count(i => i.Length == item.Key);
|
||||
count = Math.Min(count, availableCount / item.Count());
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private bool TryImprovePacking(Bin bin)
|
||||
{
|
||||
if (bin.Items.Count == 0)
|
||||
return false;
|
||||
|
||||
if (Items.Count < 2)
|
||||
return false;
|
||||
|
||||
var lengthGroups = GroupItemsByLength(bin.Items);
|
||||
|
||||
var shortestLengthItemAvailable = Items.Min(i => i.Length);
|
||||
|
||||
foreach (var group in lengthGroups)
|
||||
{
|
||||
var minRemainingLength = bin.RemainingLength;
|
||||
var firstItem = group.Items.FirstOrDefault();
|
||||
bin.RemoveItem(firstItem);
|
||||
|
||||
for (int i = 0; i < Items.Count; i++)
|
||||
{
|
||||
var item1 = Items[i];
|
||||
|
||||
if (Items[i].Length > bin.RemainingLength)
|
||||
continue;
|
||||
|
||||
var bin2 = new Bin(bin.RemainingLength);
|
||||
bin2.Spacing = bin.Spacing;
|
||||
bin2.AddItem(item1);
|
||||
|
||||
for (int j = i + 1; j < Items.Count; j++)
|
||||
{
|
||||
if (bin2.RemainingLength < shortestLengthItemAvailable)
|
||||
break;
|
||||
|
||||
var item2 = Items[j];
|
||||
|
||||
if (item2.Length > bin2.RemainingLength)
|
||||
continue;
|
||||
|
||||
bin2.AddItem(item2);
|
||||
}
|
||||
|
||||
if (bin2.RemainingLength < minRemainingLength)
|
||||
{
|
||||
Items.Add(firstItem);
|
||||
bin.AddItems(bin2.Items);
|
||||
|
||||
foreach (var item in bin2.Items)
|
||||
{
|
||||
Items.Remove(item);
|
||||
}
|
||||
|
||||
// improvement made
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bin.AddItem(firstItem);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<LengthGroup> GroupItemsByLength(IEnumerable<BinItem> items)
|
||||
{
|
||||
var groups = new List<LengthGroup>();
|
||||
var groupMap = new Dictionary<double, LengthGroup>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!groupMap.TryGetValue(item.Length, out var group))
|
||||
{
|
||||
group = new LengthGroup
|
||||
{
|
||||
Length = item.Length,
|
||||
Items = new List<BinItem>()
|
||||
};
|
||||
groupMap[item.Length] = group;
|
||||
groups.Add(group);
|
||||
}
|
||||
group.Items.Add(item);
|
||||
}
|
||||
|
||||
groups.Sort((a, b) => b.Length.CompareTo(a.Length));
|
||||
if (groups.Count > 0)
|
||||
{
|
||||
groups.RemoveAt(0); // Remove the largest length group
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
internal class LengthGroup
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public List<BinItem> Items { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
111
CutList.Core/Nesting/BestFitEngine.cs
Normal file
111
CutList.Core/Nesting/BestFitEngine.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
public class BestFitEngine : IEngine
|
||||
{
|
||||
public double StockLength { get; set; }
|
||||
|
||||
public double Spacing { get; set; }
|
||||
|
||||
public int MaxBinCount { get; set; } = int.MaxValue;
|
||||
|
||||
private List<BinItem> Items { get; set; }
|
||||
|
||||
public Result Pack(List<BinItem> items)
|
||||
{
|
||||
if (StockLength <= 0)
|
||||
throw new Exception("Stock length must be greater than 0");
|
||||
|
||||
Items = items.OrderByDescending(i => i.Length).ToList();
|
||||
|
||||
var result = new Result();
|
||||
var itemsTooLarge = Items.Where(i => i.Length > StockLength).ToList();
|
||||
result.AddItemsNotUsed(itemsTooLarge);
|
||||
|
||||
foreach (var item in itemsTooLarge)
|
||||
{
|
||||
Items.Remove(item);
|
||||
}
|
||||
|
||||
var bins = GetBins();
|
||||
result.AddBins(bins);
|
||||
|
||||
foreach (var bin in bins)
|
||||
{
|
||||
foreach (var item in bin.Items)
|
||||
{
|
||||
Items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
result.AddItemsNotUsed(Items);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Bin> GetBins()
|
||||
{
|
||||
var bins = new List<Bin>();
|
||||
|
||||
foreach (var item in Items)
|
||||
{
|
||||
Bin best_bin;
|
||||
|
||||
if (!FindBin(bins.ToArray(), item.Length, out best_bin))
|
||||
{
|
||||
if (item.Length > StockLength)
|
||||
continue;
|
||||
|
||||
if (bins.Count < MaxBinCount)
|
||||
{
|
||||
best_bin = CreateBin();
|
||||
bins.Add(best_bin);
|
||||
}
|
||||
}
|
||||
|
||||
if (best_bin != null)
|
||||
best_bin.AddItem(item);
|
||||
|
||||
|
||||
}
|
||||
|
||||
return bins
|
||||
.OrderByDescending(b => b.Utilization)
|
||||
.ThenBy(b => b.Items.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private Bin CreateBin()
|
||||
{
|
||||
var length = StockLength;
|
||||
|
||||
return new Bin(length)
|
||||
{
|
||||
Spacing = Spacing
|
||||
};
|
||||
}
|
||||
|
||||
private static bool FindBin(IEnumerable<Bin> bins, double length, out Bin found)
|
||||
{
|
||||
found = null;
|
||||
|
||||
foreach (var bin in bins)
|
||||
{
|
||||
if (bin.RemainingLength < length)
|
||||
continue;
|
||||
|
||||
if (found == null)
|
||||
found = bin;
|
||||
|
||||
if (bin.RemainingLength < found.RemainingLength)
|
||||
found = bin;
|
||||
}
|
||||
|
||||
return (found != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
CutList.Core/Nesting/EngineFactory.cs
Normal file
19
CutList.Core/Nesting/EngineFactory.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of IEngineFactory that creates AdvancedFitEngine instances.
|
||||
/// Can be extended to support different engine types based on configuration.
|
||||
/// </summary>
|
||||
public class EngineFactory : IEngineFactory
|
||||
{
|
||||
public IEngine CreateEngine(double stockLength, double spacing, int maxBinCount)
|
||||
{
|
||||
return new AdvancedFitEngine
|
||||
{
|
||||
StockLength = stockLength,
|
||||
Spacing = spacing,
|
||||
MaxBinCount = maxBinCount
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
9
CutList.Core/Nesting/IEngine.cs
Normal file
9
CutList.Core/Nesting/IEngine.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
public interface IEngine
|
||||
{
|
||||
Result Pack(List<BinItem> items);
|
||||
}
|
||||
}
|
||||
18
CutList.Core/Nesting/IEngineFactory.cs
Normal file
18
CutList.Core/Nesting/IEngineFactory.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory interface for creating bin packing engines.
|
||||
/// Allows for dependency injection and testing without hard-coded engine types.
|
||||
/// </summary>
|
||||
public interface IEngineFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a configured engine instance for bin packing.
|
||||
/// </summary>
|
||||
/// <param name="stockLength">The length of stock bins</param>
|
||||
/// <param name="spacing">The spacing/kerf between items</param>
|
||||
/// <param name="maxBinCount">Maximum number of bins to create</param>
|
||||
/// <returns>A configured IEngine instance</returns>
|
||||
IEngine CreateEngine(double stockLength, double spacing, int maxBinCount);
|
||||
}
|
||||
}
|
||||
68
CutList.Core/Nesting/MultiBinEngine.cs
Normal file
68
CutList.Core/Nesting/MultiBinEngine.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
public class MultiBinEngine : IEngine
|
||||
{
|
||||
private readonly IEngineFactory _engineFactory;
|
||||
|
||||
public MultiBinEngine() : this(new EngineFactory())
|
||||
{
|
||||
}
|
||||
|
||||
public MultiBinEngine(IEngineFactory engineFactory)
|
||||
{
|
||||
_engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory));
|
||||
_bins = new List<MultiBin>();
|
||||
}
|
||||
|
||||
private readonly List<MultiBin> _bins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the read-only collection of bins.
|
||||
/// Use SetBins() to configure bins for packing.
|
||||
/// </summary>
|
||||
public IReadOnlyList<MultiBin> Bins => _bins.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the bins to use for packing.
|
||||
/// </summary>
|
||||
public void SetBins(IEnumerable<MultiBin> bins)
|
||||
{
|
||||
_bins.Clear();
|
||||
if (bins != null)
|
||||
{
|
||||
_bins.AddRange(bins);
|
||||
}
|
||||
}
|
||||
|
||||
public double Spacing { get; set; }
|
||||
|
||||
public Result Pack(List<BinItem> items)
|
||||
{
|
||||
var bins = _bins
|
||||
.Where(b => b.Length > 0)
|
||||
.OrderBy(b => b.Priority)
|
||||
.ThenBy(b => b.Length)
|
||||
.ToList();
|
||||
|
||||
var result = new Result();
|
||||
var remainingItems = new List<BinItem>(items);
|
||||
|
||||
foreach (var bin in bins)
|
||||
{
|
||||
var engine = _engineFactory.CreateEngine(bin.Length, Spacing, bin.Quantity);
|
||||
var r = engine.Pack(remainingItems);
|
||||
|
||||
result.AddBins(r.Bins);
|
||||
remainingItems = r.ItemsNotUsed.ToList();
|
||||
}
|
||||
|
||||
result.AddItemsNotUsed(remainingItems);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
CutList.Core/Nesting/Result.cs
Normal file
40
CutList.Core/Nesting/Result.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CutList.Core.Nesting
|
||||
{
|
||||
public class Result
|
||||
{
|
||||
private readonly List<BinItem> _itemsNotUsed;
|
||||
private readonly List<Bin> _bins;
|
||||
|
||||
public Result()
|
||||
{
|
||||
_itemsNotUsed = new List<BinItem>();
|
||||
_bins = new List<Bin>();
|
||||
}
|
||||
|
||||
public IReadOnlyList<BinItem> ItemsNotUsed => _itemsNotUsed;
|
||||
|
||||
public IReadOnlyList<Bin> Bins => _bins;
|
||||
|
||||
public void AddItemNotUsed(BinItem item)
|
||||
{
|
||||
_itemsNotUsed.Add(item);
|
||||
}
|
||||
|
||||
public void AddItemsNotUsed(IEnumerable<BinItem> items)
|
||||
{
|
||||
_itemsNotUsed.AddRange(items);
|
||||
}
|
||||
|
||||
public void AddBin(Bin bin)
|
||||
{
|
||||
_bins.Add(bin);
|
||||
}
|
||||
|
||||
public void AddBins(IEnumerable<Bin> bins)
|
||||
{
|
||||
_bins.AddRange(bins);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
CutList.Core/Properties/AssemblyInfo.cs
Normal file
35
CutList.Core/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("CutList.Core")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("CutList.Core")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2021")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("3d873ff0-6930-4bce-a5a9-da5c20354dee")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
14
CutList.Core/Tolerance.cs
Normal file
14
CutList.Core/Tolerance.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace CutList.Core
|
||||
{
|
||||
public static class Tolerance
|
||||
{
|
||||
public const double Epsilon = 0.00001;
|
||||
|
||||
public static bool IsEqualTo(this double a, double b, double tolerance = Epsilon)
|
||||
{
|
||||
return Math.Abs(b - a) <= tolerance;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user