feat: Add material dimensions with typed properties

Implement TPH inheritance for material dimensions:
- MaterialShape enum with display names and parsing
- MaterialType enum (Steel, Aluminum, Stainless, etc.)
- MaterialDimensions base class with derived types per shape
- Auto-generate size strings from typed dimensions
- SortOrder field for numeric dimension sorting

Each shape has specific dimension properties:
- RoundBar: Diameter
- RoundTube: OuterDiameter, Wall
- FlatBar: Width, Thickness
- SquareBar/Tube: Size, Wall
- RectangularTube: Width, Height, Wall
- Angle: Leg1, Leg2, Thickness
- Channel: Height, Flange, Web
- IBeam: Height, WeightPerFoot
- Pipe: NominalSize, Wall, Schedule

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 23:37:43 -05:00
parent 254066c989
commit 4f6d986dc9
15 changed files with 5451 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
namespace CutList.Web.Data.Entities;
/// <summary>
/// Base class for material dimensions. Each shape has its own derived class with specific properties.
/// </summary>
public abstract class MaterialDimensions
{
public int Id { get; set; }
public int MaterialId { get; set; }
public Material Material { get; set; } = null!;
/// <summary>
/// Generates a display string for the size based on the dimensions.
/// </summary>
public abstract string GenerateSizeString();
/// <summary>
/// Gets the primary dimension value for sorting (in thousandths of an inch).
/// </summary>
public abstract int GetSortOrder();
}
/// <summary>
/// Dimensions for Round Bar: solid round stock.
/// </summary>
public class RoundBarDimensions : MaterialDimensions
{
public decimal Diameter { get; set; }
public override string GenerateSizeString() =>
FormatDimension(Diameter);
public override int GetSortOrder() => (int)(Diameter * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for Round Tube: hollow round stock.
/// </summary>
public class RoundTubeDimensions : MaterialDimensions
{
public decimal OuterDiameter { get; set; }
public decimal Wall { get; set; }
public override string GenerateSizeString() =>
$"{FormatDimension(OuterDiameter)} OD x {FormatDimension(Wall)} wall";
public override int GetSortOrder() => (int)(OuterDiameter * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for Flat Bar: rectangular solid stock.
/// </summary>
public class FlatBarDimensions : MaterialDimensions
{
public decimal Width { get; set; }
public decimal Thickness { get; set; }
public override string GenerateSizeString() =>
$"{FormatDimension(Width)} x {FormatDimension(Thickness)}";
public override int GetSortOrder() => (int)(Width * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for Square Bar: solid square stock.
/// </summary>
public class SquareBarDimensions : MaterialDimensions
{
public decimal Size { get; set; }
public override string GenerateSizeString() =>
FormatDimension(Size);
public override int GetSortOrder() => (int)(Size * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for Square Tube: hollow square stock.
/// </summary>
public class SquareTubeDimensions : MaterialDimensions
{
public decimal Size { get; set; }
public decimal Wall { get; set; }
public override string GenerateSizeString() =>
$"{FormatDimension(Size)} x {FormatDimension(Wall)} wall";
public override int GetSortOrder() => (int)(Size * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for Rectangular Tube: hollow rectangular stock.
/// </summary>
public class RectangularTubeDimensions : MaterialDimensions
{
public decimal Width { get; set; }
public decimal Height { get; set; }
public decimal Wall { get; set; }
public override string GenerateSizeString() =>
$"{FormatDimension(Width)} x {FormatDimension(Height)} x {FormatDimension(Wall)} wall";
public override int GetSortOrder() => (int)(Width * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for Angle: L-shaped stock.
/// </summary>
public class AngleDimensions : MaterialDimensions
{
public decimal Leg1 { get; set; }
public decimal Leg2 { get; set; }
public decimal Thickness { get; set; }
public override string GenerateSizeString() =>
$"{FormatDimension(Leg1)} x {FormatDimension(Leg2)} x {FormatDimension(Thickness)}";
public override int GetSortOrder() => (int)(Leg1 * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for Channel: C-shaped stock.
/// </summary>
public class ChannelDimensions : MaterialDimensions
{
public decimal Height { get; set; }
public decimal Flange { get; set; }
public decimal Web { get; set; }
public override string GenerateSizeString() =>
$"{FormatDimension(Height)} x {FormatDimension(Flange)} x {FormatDimension(Web)}";
public override int GetSortOrder() => (int)(Height * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}
/// <summary>
/// Dimensions for I-Beam: wide flange beam.
/// </summary>
public class IBeamDimensions : MaterialDimensions
{
public decimal Height { get; set; }
public decimal WeightPerFoot { get; set; }
public override string GenerateSizeString() =>
$"W{Height:0.##} x {WeightPerFoot:0.##}";
public override int GetSortOrder() => (int)(Height * 1000);
}
/// <summary>
/// Dimensions for Pipe: nominal pipe size.
/// </summary>
public class PipeDimensions : MaterialDimensions
{
public decimal NominalSize { get; set; }
public decimal? Wall { get; set; }
public string? Schedule { get; set; }
public override string GenerateSizeString() =>
!string.IsNullOrEmpty(Schedule)
? $"{FormatDimension(NominalSize)} NPS Sch {Schedule}"
: $"{FormatDimension(NominalSize)} NPS x {FormatDimension(Wall ?? 0)} wall";
public override int GetSortOrder() => (int)(NominalSize * 1000);
private static string FormatDimension(decimal value) =>
CutList.Core.Formatting.ArchUnits.FormatFromInches((double)value);
}

View File

@@ -0,0 +1,89 @@
namespace CutList.Web.Data.Entities;
/// <summary>
/// Enumeration of supported material shapes.
/// </summary>
public enum MaterialShape
{
RoundBar,
RoundTube,
FlatBar,
SquareBar,
SquareTube,
RectangularTube,
Angle,
Channel,
IBeam,
Pipe
}
/// <summary>
/// Extension methods for MaterialShape enum.
/// </summary>
public static class MaterialShapeExtensions
{
/// <summary>
/// Gets the display name for a material shape.
/// </summary>
public static string GetDisplayName(this MaterialShape shape) => shape switch
{
MaterialShape.RoundBar => "Round Bar",
MaterialShape.RoundTube => "Round Tube",
MaterialShape.FlatBar => "Flat Bar",
MaterialShape.SquareBar => "Square Bar",
MaterialShape.SquareTube => "Square Tube",
MaterialShape.RectangularTube => "Rectangular Tube",
MaterialShape.Angle => "Angle",
MaterialShape.Channel => "Channel",
MaterialShape.IBeam => "I-Beam",
MaterialShape.Pipe => "Pipe",
_ => shape.ToString()
};
/// <summary>
/// Parses a display name or enum value string to a MaterialShape.
/// </summary>
public static MaterialShape? ParseShape(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
// Try exact enum parse first
if (Enum.TryParse<MaterialShape>(input, ignoreCase: true, out var result))
return result;
// Try display name matching
return input.Trim().ToLowerInvariant() switch
{
"round bar" => MaterialShape.RoundBar,
"round tube" => MaterialShape.RoundTube,
"flat bar" => MaterialShape.FlatBar,
"square bar" => MaterialShape.SquareBar,
"square tube" => MaterialShape.SquareTube,
"rectangular tube" or "rect tube" => MaterialShape.RectangularTube,
"angle" => MaterialShape.Angle,
"channel" => MaterialShape.Channel,
"i-beam" or "ibeam" or "i beam" => MaterialShape.IBeam,
"pipe" => MaterialShape.Pipe,
_ => null
};
}
/// <summary>
/// Gets the dimension field names used by a given shape.
/// </summary>
public static string[] GetDimensionFields(this MaterialShape shape) => shape switch
{
MaterialShape.RoundBar => new[] { "Diameter" },
MaterialShape.RoundTube => new[] { "OuterDiameter", "Wall" },
MaterialShape.FlatBar => new[] { "Width", "Thickness" },
MaterialShape.SquareBar => new[] { "Size" },
MaterialShape.SquareTube => new[] { "Size", "Wall" },
MaterialShape.RectangularTube => new[] { "Width", "Height", "Wall" },
MaterialShape.Angle => new[] { "Leg1", "Leg2", "Thickness" },
MaterialShape.Channel => new[] { "Height", "Flange", "Web" },
MaterialShape.IBeam => new[] { "Height", "WeightPerFoot" },
MaterialShape.Pipe => new[] { "NominalSize", "Wall", "Schedule" },
_ => Array.Empty<string>()
};
}

View File

@@ -0,0 +1,13 @@
namespace CutList.Web.Data.Entities;
/// <summary>
/// Type of material (metal).
/// </summary>
public enum MaterialType
{
Steel,
Aluminum,
Stainless,
Brass,
Copper
}