Compare commits

...

3 Commits

Author SHA1 Message Date
046976c429 refactor: Replace hash code magic number with named constant
Add HashMultiplier constant to BinComparer, BinItem, MultiBin, and Tool

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:07:52 -05:00
4d208f6411 feat: Add CutList.Mcp project for MCP server integration
Add new MCP (Model Context Protocol) server project that exposes cut
list optimization tools for AI assistants. Implements tools for:
- create_cutlist: Optimized bin packing with parts and stock bins
- parse_length: Parse architectural format to decimal inches
- format_length: Format inches to feet/inches/fractions
- create_cutlist_report: Generate formatted printable text report

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:11:22 -05:00
88d67336d9 refactor: Relocate BinFileSaver to CutList.Core with report generation
Move BinFileSaver from CutList/Services to CutList.Core namespace for
reuse by MCP server. Add GenerateReport() method that returns formatted
text instead of only writing to file. Refactor to use TextWriter base
class for flexibility.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:11:12 -05:00
10 changed files with 513 additions and 42 deletions

View File

@@ -36,23 +36,25 @@ namespace CutList.Core
unchecked unchecked
{ {
// Prime multiplier reduces collisions in hash-based collections
const int HashMultiplier = 23;
int hash = 17; int hash = 17;
hash = hash * 23 + bin.Length.GetHashCode(); hash = hash * HashMultiplier + bin.Length.GetHashCode();
hash = hash * 23 + bin.Spacing.GetHashCode(); hash = hash * HashMultiplier + bin.Spacing.GetHashCode();
hash = hash * 23 + bin.Items.Count.GetHashCode(); hash = hash * HashMultiplier + bin.Items.Count.GetHashCode();
// Include first and last item in hash for better distribution // Include first and last item in hash for better distribution
if (bin.Items.Count > 0) if (bin.Items.Count > 0)
{ {
var firstItem = bin.Items[0]; var firstItem = bin.Items[0];
hash = hash * 23 + (firstItem.Name?.GetHashCode() ?? 0); hash = hash * HashMultiplier + (firstItem.Name?.GetHashCode() ?? 0);
hash = hash * 23 + firstItem.Length.GetHashCode(); hash = hash * HashMultiplier + firstItem.Length.GetHashCode();
if (bin.Items.Count > 1) if (bin.Items.Count > 1)
{ {
var lastItem = bin.Items[bin.Items.Count - 1]; var lastItem = bin.Items[bin.Items.Count - 1];
hash = hash * 23 + (lastItem.Name?.GetHashCode() ?? 0); hash = hash * HashMultiplier + (lastItem.Name?.GetHashCode() ?? 0);
hash = hash * 23 + lastItem.Length.GetHashCode(); hash = hash * HashMultiplier + lastItem.Length.GetHashCode();
} }
} }

View File

@@ -1,8 +1,7 @@
using CutList.Core;
using CutList.Core.Formatting; using CutList.Core.Formatting;
using System.Diagnostics; using System.Diagnostics;
namespace CutList.Services namespace CutList.Core
{ {
public class BinFileSaver public class BinFileSaver
{ {
@@ -22,7 +21,24 @@ namespace CutList.Services
using (var writer = new StreamWriter(file)) using (var writer = new StreamWriter(file))
{ {
writer.AutoFlush = true; writer.AutoFlush = true;
WriteToWriter(writer);
}
if (OpenFileAfterSave)
{
OpenFile(file);
}
}
public string GenerateReport()
{
using var writer = new StringWriter();
WriteToWriter(writer);
return writer.ToString();
}
private void WriteToWriter(TextWriter writer)
{
PaddingWidthOfItemLength = _bins PaddingWidthOfItemLength = _bins
.SelectMany(b => b.Items) .SelectMany(b => b.Items)
.Select(i => FormatHelper.ConvertToMixedFraction(i.Length).Length) .Select(i => FormatHelper.ConvertToMixedFraction(i.Length).Length)
@@ -40,12 +56,6 @@ namespace CutList.Services
WriteFinalSummary(writer); WriteFinalSummary(writer);
} }
if (OpenFileAfterSave)
{
OpenFile(file);
}
}
private void OpenFile(string file) private void OpenFile(string file)
{ {
try try
@@ -58,7 +68,7 @@ namespace CutList.Services
} }
} }
private void WriteHeader(StreamWriter writer) private void WriteHeader(TextWriter writer)
{ {
var totalBars = _bins.Count(); var totalBars = _bins.Count();
var totalItems = _bins.Sum(b => b.Items.Count); var totalItems = _bins.Sum(b => b.Items.Count);
@@ -70,7 +80,7 @@ namespace CutList.Services
writer.WriteLine(); writer.WriteLine();
} }
private void WriteBinSummary(StreamWriter writer, Bin bin, int id) private void WriteBinSummary(TextWriter writer, Bin bin, int id)
{ {
var stockLength = FormatHelper.ConvertToMixedFraction(bin.Length); var stockLength = FormatHelper.ConvertToMixedFraction(bin.Length);
var dropLength = FormatHelper.ConvertToMixedFraction(bin.RemainingLength); var dropLength = FormatHelper.ConvertToMixedFraction(bin.RemainingLength);
@@ -87,7 +97,7 @@ namespace CutList.Services
writer.WriteLine(); writer.WriteLine();
} }
private void WriteBinItems(StreamWriter writer, Bin bin) private void WriteBinItems(TextWriter writer, Bin bin)
{ {
var groups = bin.Items var groups = bin.Items
.GroupBy(i => new { i.Name, i.Length }) .GroupBy(i => new { i.Name, i.Length })
@@ -109,7 +119,7 @@ namespace CutList.Services
} }
} }
private void WriteFinalSummary(StreamWriter writer) private void WriteFinalSummary(TextWriter writer)
{ {
var totalBars = _bins.Count(); var totalBars = _bins.Count();
var totalItems = _bins.Sum(b => b.Items.Count); var totalItems = _bins.Sum(b => b.Items.Count);

View File

@@ -77,9 +77,11 @@
{ {
unchecked unchecked
{ {
// Prime multiplier reduces collisions in hash-based collections
const int HashMultiplier = 23;
int hash = 17; int hash = 17;
hash = hash * 23 + (Name?.GetHashCode() ?? 0); hash = hash * HashMultiplier + (Name?.GetHashCode() ?? 0);
hash = hash * 23 + Length.GetHashCode(); hash = hash * HashMultiplier + Length.GetHashCode();
return hash; return hash;
} }
} }

View File

@@ -112,10 +112,12 @@
{ {
unchecked unchecked
{ {
// Prime multiplier reduces collisions in hash-based collections
const int HashMultiplier = 23;
int hash = 17; int hash = 17;
hash = hash * 23 + Quantity.GetHashCode(); hash = hash * HashMultiplier + Quantity.GetHashCode();
hash = hash * 23 + Length.GetHashCode(); hash = hash * HashMultiplier + Length.GetHashCode();
hash = hash * 23 + Priority.GetHashCode(); hash = hash * HashMultiplier + Priority.GetHashCode();
return hash; return hash;
} }
} }

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
<PackageReference Include="ModelContextProtocol" Version="0.7.0-preview.1" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

388
CutList.Mcp/CutListTools.cs Normal file
View File

@@ -0,0 +1,388 @@
using System.ComponentModel;
using CutList.Core;
using CutList.Core.Formatting;
using CutList.Core.Nesting;
using ModelContextProtocol.Server;
namespace CutList.Mcp;
/// <summary>
/// MCP tools for cut list optimization.
/// </summary>
[McpServerToolType]
public static class CutListTools
{
/// <summary>
/// Creates an optimized cut list by packing parts into stock bins using a first-fit decreasing algorithm.
/// Returns the optimal arrangement of cuts to minimize waste.
/// </summary>
/// <param name="parts">List of parts to cut. Each part has a name, length (in inches or architectural format like 12' 6"), and quantity.</param>
/// <param name="stockBins">List of available stock material. Each has a length, quantity (-1 for unlimited), and priority (lower = used first).</param>
/// <param name="kerf">Blade/cutting width in inches. Default is 0.125 (1/8"). This accounts for material lost to the cut itself.</param>
/// <returns>Optimized cut list showing which parts go in which bins, utilization percentages, and any parts that couldn't fit.</returns>
[McpServerTool(Name = "create_cutlist"), Description("Creates an optimized cut list by packing parts into stock bins to minimize waste.")]
public static CutListResult CreateCutList(
[Description("Parts to cut. Each object needs: name (string), length (string like \"12'\" or \"36\\\"\" or \"12.5\"), quantity (int)")]
PartInput[] parts,
[Description("Stock bins available. Each needs: length (string), quantity (int, use -1 for unlimited), priority (int, lower = used first, default 25)")]
StockBinInput[] stockBins,
[Description("Blade kerf/width in inches (default 0.125)")]
double kerf = 0.125)
{
try
{
// Convert parts to BinItems
var binItems = new List<BinItem>();
foreach (var part in parts)
{
double length = ParseLength(part.Length);
if (length <= 0)
{
return new CutListResult
{
Success = false,
Error = $"Invalid part length: {part.Length} for part '{part.Name}'"
};
}
for (int i = 0; i < part.Quantity; i++)
{
binItems.Add(new BinItem(part.Name, length));
}
}
// Convert stock bins to MultiBins
var multiBins = new List<MultiBin>();
foreach (var bin in stockBins)
{
double length = ParseLength(bin.Length);
if (length <= 0)
{
return new CutListResult
{
Success = false,
Error = $"Invalid bin length: {bin.Length}"
};
}
multiBins.Add(new MultiBin(length, bin.Quantity, bin.Priority));
}
// Run the packing algorithm
var engine = new MultiBinEngine();
engine.SetBins(multiBins);
engine.Spacing = kerf;
var packResult = engine.Pack(binItems);
// Convert results
var resultBins = new List<ResultBin>();
foreach (var bin in packResult.Bins)
{
var resultBin = new ResultBin
{
Length = FormatLength(bin.Length),
LengthInches = bin.Length,
UsedLength = FormatLength(bin.UsedLength),
UsedLengthInches = bin.UsedLength,
RemainingLength = FormatLength(bin.RemainingLength),
RemainingLengthInches = bin.RemainingLength,
Utilization = Math.Round(bin.Utilization * 100, 2),
Items = bin.Items.Select(item => new ResultItem
{
Name = item.Name,
Length = FormatLength(item.Length),
LengthInches = item.Length
}).ToList()
};
resultBins.Add(resultBin);
}
var unusedItems = packResult.ItemsNotUsed.Select(item => new ResultItem
{
Name = item.Name,
Length = FormatLength(item.Length),
LengthInches = item.Length
}).ToList();
// Calculate summary statistics
double totalStockUsed = packResult.Bins.Sum(b => b.Length);
double totalMaterialUsed = packResult.Bins.Sum(b => b.UsedLength);
double totalWaste = packResult.Bins.Sum(b => b.RemainingLength);
double overallUtilization = totalStockUsed > 0 ? (totalMaterialUsed / totalStockUsed) * 100 : 0;
return new CutListResult
{
Success = true,
Bins = resultBins,
UnusedItems = unusedItems,
Summary = new CutListSummary
{
TotalBinsUsed = packResult.Bins.Count,
TotalPartsPlaced = binItems.Count - packResult.ItemsNotUsed.Count,
TotalPartsNotPlaced = packResult.ItemsNotUsed.Count,
TotalStockLength = FormatLength(totalStockUsed),
TotalStockLengthInches = totalStockUsed,
TotalWaste = FormatLength(totalWaste),
TotalWasteInches = totalWaste,
OverallUtilization = Math.Round(overallUtilization, 2)
}
};
}
catch (Exception ex)
{
return new CutListResult
{
Success = false,
Error = ex.Message
};
}
}
/// <summary>
/// Parses a length string in architectural format (feet/inches/fractions) to inches.
/// </summary>
/// <param name="input">Length string like "12'", "6\"", "12' 6\"", "12.5", "6 1/2\"", etc.</param>
/// <returns>The length in inches, or an error message if parsing fails.</returns>
[McpServerTool(Name = "parse_length"), Description("Parses an architectural length string (feet/inches/fractions) to decimal inches.")]
public static ParseLengthResult ParseLengthString(
[Description("Length string like \"12'\", \"6\\\"\", \"12' 6\\\"\", \"12.5\", \"6 1/2\\\"\"")]
string input)
{
try
{
double inches = ParseLength(input);
return new ParseLengthResult
{
Success = true,
Inches = inches,
Formatted = FormatLength(inches)
};
}
catch (Exception ex)
{
return new ParseLengthResult
{
Success = false,
Error = ex.Message
};
}
}
/// <summary>
/// Formats a length in inches to a human-readable string with feet and fractional inches.
/// </summary>
/// <param name="inches">Length in inches.</param>
/// <returns>Formatted string like "12' 6 1/2\"".</returns>
[McpServerTool(Name = "format_length"), Description("Formats a length in inches to feet and fractional inches.")]
public static FormatLengthResult FormatLengthString(
[Description("Length in inches")]
double inches)
{
try
{
return new FormatLengthResult
{
Success = true,
Formatted = FormatLength(inches),
Inches = inches
};
}
catch (Exception ex)
{
return new FormatLengthResult
{
Success = false,
Error = ex.Message
};
}
}
/// <summary>
/// Creates an optimized cut list and saves a formatted text report to a file.
/// </summary>
[McpServerTool(Name = "create_cutlist_report"), Description("Creates an optimized cut list and saves a formatted printable text report to a file. Returns the file path.")]
public static CutListReportResult CreateCutListReport(
[Description("Parts to cut. Each object needs: name (string), length (string like \"12'\" or \"36\\\"\" or \"12.5\"), quantity (int)")]
PartInput[] parts,
[Description("Stock bins available. Each needs: length (string), quantity (int, use -1 for unlimited), priority (int, lower = used first, default 25)")]
StockBinInput[] stockBins,
[Description("Blade kerf/width in inches (default 0.125)")]
double kerf = 0.125,
[Description("File path to save the report. If not provided, saves to a temp file.")]
string? filePath = null)
{
try
{
// Convert parts to BinItems
var binItems = new List<BinItem>();
foreach (var part in parts)
{
double length = ParseLength(part.Length);
if (length <= 0)
{
return new CutListReportResult
{
Success = false,
Error = $"Invalid part length: {part.Length} for part '{part.Name}'"
};
}
for (int i = 0; i < part.Quantity; i++)
{
binItems.Add(new BinItem(part.Name, length));
}
}
// Convert stock bins to MultiBins
var multiBins = new List<MultiBin>();
foreach (var bin in stockBins)
{
double length = ParseLength(bin.Length);
if (length <= 0)
{
return new CutListReportResult
{
Success = false,
Error = $"Invalid bin length: {bin.Length}"
};
}
multiBins.Add(new MultiBin(length, bin.Quantity, bin.Priority));
}
// Run the packing algorithm
var engine = new MultiBinEngine();
engine.SetBins(multiBins);
engine.Spacing = kerf;
var packResult = engine.Pack(binItems);
// Determine file path
var outputPath = string.IsNullOrWhiteSpace(filePath)
? Path.Combine(Path.GetTempPath(), $"cutlist_{DateTime.Now:yyyyMMdd_HHmmss}.txt")
: filePath;
// Save using BinFileSaver
var saver = new BinFileSaver(packResult.Bins);
saver.SaveBinsToFile(outputPath);
return new CutListReportResult
{
Success = true,
FilePath = Path.GetFullPath(outputPath),
TotalBins = packResult.Bins.Count,
TotalParts = binItems.Count - packResult.ItemsNotUsed.Count,
PartsNotPlaced = packResult.ItemsNotUsed.Count
};
}
catch (Exception ex)
{
return new CutListReportResult
{
Success = false,
Error = ex.Message
};
}
}
private static double ParseLength(string input)
{
if (string.IsNullOrWhiteSpace(input))
return 0;
// Try parsing as a plain number first
if (double.TryParse(input.Trim(), out double plainNumber))
return plainNumber;
// Try architectural format
return ArchUnits.ParseToInches(input);
}
private static string FormatLength(double inches)
{
return ArchUnits.FormatFromInches(inches);
}
}
// Input models
public class PartInput
{
public string Name { get; set; } = string.Empty;
public string Length { get; set; } = string.Empty;
public int Quantity { get; set; } = 1;
}
public class StockBinInput
{
public string Length { get; set; } = string.Empty;
public int Quantity { get; set; } = -1; // -1 = unlimited
public int Priority { get; set; } = 25; // Lower = used first
}
// Output models
public class CutListResult
{
public bool Success { get; set; }
public string? Error { get; set; }
public List<ResultBin> Bins { get; set; } = new();
public List<ResultItem> UnusedItems { get; set; } = new();
public CutListSummary? Summary { get; set; }
}
public class ResultBin
{
public string Length { get; set; } = string.Empty;
public double LengthInches { get; set; }
public string UsedLength { get; set; } = string.Empty;
public double UsedLengthInches { get; set; }
public string RemainingLength { get; set; } = string.Empty;
public double RemainingLengthInches { get; set; }
public double Utilization { get; set; }
public List<ResultItem> Items { get; set; } = new();
}
public class ResultItem
{
public string Name { get; set; } = string.Empty;
public string Length { get; set; } = string.Empty;
public double LengthInches { get; set; }
}
public class CutListSummary
{
public int TotalBinsUsed { get; set; }
public int TotalPartsPlaced { get; set; }
public int TotalPartsNotPlaced { get; set; }
public string TotalStockLength { get; set; } = string.Empty;
public double TotalStockLengthInches { get; set; }
public string TotalWaste { get; set; } = string.Empty;
public double TotalWasteInches { get; set; }
public double OverallUtilization { get; set; }
}
public class ParseLengthResult
{
public bool Success { get; set; }
public string? Error { get; set; }
public double Inches { get; set; }
public string? Formatted { get; set; }
}
public class FormatLengthResult
{
public bool Success { get; set; }
public string? Error { get; set; }
public string? Formatted { get; set; }
public double Inches { get; set; }
}
public class CutListReportResult
{
public bool Success { get; set; }
public string? Error { get; set; }
public string? FilePath { get; set; }
public int TotalBins { get; set; }
public int TotalParts { get; set; }
public int PartsNotPlaced { get; set; }
}

13
CutList.Mcp/Program.cs Normal file
View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly(typeof(Program).Assembly);
var app = builder.Build();
await app.RunAsync();

View File

@@ -7,20 +7,54 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList", "CutList\CutList.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Core", "CutList.Core\CutList.Core.csproj", "{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Core", "CutList.Core\CutList.Core.csproj", "{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Mcp", "CutList.Mcp\CutList.Mcp.csproj", "{3B53377F-E012-42BA-82C8-322815D661B3}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.Build.0 = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x64.ActiveCfg = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x64.Build.0 = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x86.ActiveCfg = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Debug|x86.Build.0 = Debug|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|Any CPU.Build.0 = Release|Any CPU {3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|Any CPU.Build.0 = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x64.ActiveCfg = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x64.Build.0 = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x86.ActiveCfg = Release|Any CPU
{3E82A1E3-07A8-40C4-ABC4-DF24C5120073}.Release|x86.Build.0 = Release|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x64.Build.0 = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Debug|x86.Build.0 = Debug|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|Any CPU.Build.0 = Release|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x64.ActiveCfg = Release|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x64.Build.0 = Release|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x86.ActiveCfg = Release|Any CPU
{3D873FF0-6930-4BCE-A5A9-DA5C20354DEE}.Release|x86.Build.0 = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x64.ActiveCfg = Debug|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x64.Build.0 = Debug|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x86.ActiveCfg = Debug|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Debug|x86.Build.0 = Debug|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|Any CPU.Build.0 = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|x64.ActiveCfg = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|x64.Build.0 = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|x86.ActiveCfg = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -1,5 +1,4 @@
using CutList.Core; using CutList.Core;
using CutList.Services;
namespace CutList.Forms namespace CutList.Forms
{ {

View File

@@ -96,10 +96,12 @@
{ {
unchecked unchecked
{ {
// Prime multiplier reduces collisions in hash-based collections
const int HashMultiplier = 23;
int hash = 17; int hash = 17;
hash = hash * 23 + (Name?.GetHashCode() ?? 0); hash = hash * HashMultiplier + (Name?.GetHashCode() ?? 0);
hash = hash * 23 + Kerf.GetHashCode(); hash = hash * HashMultiplier + Kerf.GetHashCode();
hash = hash * 23 + AllowUserToChange.GetHashCode(); hash = hash * HashMultiplier + AllowUserToChange.GetHashCode();
return hash; return hash;
} }
} }