[verified] feat: add configurable results length units
Build CutList image / build-and-push (push) Successful in 31s

Default cut results to total-inch mixed fractions, allow switching to feet and inches, and separate part names from lengths visually.
This commit is contained in:
aj
2026-08-03 01:31:09 +00:00
parent 79bf56afd8
commit 3ccaddef0c
6 changed files with 85 additions and 5 deletions
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,27 @@
using CutList.Core.Formatting;
using Xunit;
namespace CutList.Core.Tests;
public class ResultsLengthDisplayTests
{
[Theory]
[InlineData(150, "150\"")]
[InlineData(150.375, "150-3/8\"")]
public void FormatInches_uses_total_inches_without_converting_to_feet(double inches, string expected)
{
Assert.Equal(expected, ArchUnits.FormatInches(inches));
}
[Fact]
public void Results_markup_includes_unit_toggle_and_divider_between_part_and_length()
{
var sourcePath = Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, "../../../../CutList.Web/Components/Pages/Jobs/Edit.razor"));
var markup = File.ReadAllText(sourcePath);
Assert.Contains("FormatResultLength", markup);
Assert.Contains("cut-part-badge-divider", markup);
Assert.Contains("Show feet + inches", markup);
}
}
+8
View File
@@ -80,5 +80,13 @@ namespace CutList.Core.Formatting
return $"{inches}\"";
}
}
/// <summary>
/// Formats a measurement as a mixed fraction of total inches without converting to feet.
/// </summary>
public static string FormatInches(double totalInches)
{
return $"{FormatHelper.ConvertToMixedFraction(totalInches)}\"";
}
}
}
+14 -5
View File
@@ -503,6 +503,7 @@ else
private MultiMaterialPackingSummary? summary;
private bool optimizing;
private bool lockingJob;
private bool showLengthsInInches = true;
private IEnumerable<MaterialShape> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
private IEnumerable<Material> FilteredMaterials => !selectedShape.HasValue
@@ -511,6 +512,9 @@ else
private bool IsNew => !Id.HasValue;
private bool CanOptimize => job.Parts.Count > 0 && job.CuttingToolId != null;
private string FormatResultLength(double inches) => showLengthsInInches
? ArchUnits.FormatInches(inches)
: ArchUnits.FormatFromInches(inches);
private async Task UnlockJob()
{
@@ -987,6 +991,10 @@ else
<button class="btn btn-outline-secondary ms-2" @onclick="PrintReport">
<i class="bi bi-printer me-1"></i> Print Report
</button>
<button class="btn btn-outline-secondary ms-2" @onclick="() => showLengthsInInches = !showLengthsInInches"
aria-pressed="@showLengthsInInches">
@(showLengthsInInches ? "Show feet + inches" : "Show all inches")
</button>
@if (!job.IsLocked)
{
<button class="btn btn-warning ms-2" @onclick="LockJob" disabled="@lockingJob">
@@ -1037,7 +1045,7 @@ else
<div class="col-md-3 col-6 mb-3">
<div class="card text-center">
<div class="card-body">
<h2 class="card-title mb-0">@ArchUnits.FormatFromInches(summary.TotalWaste)</h2>
<h2 class="card-title mb-0">@FormatResultLength(summary.TotalWaste)</h2>
<p class="card-text text-muted">Total Waste</p>
</div>
</div>
@@ -1084,7 +1092,7 @@ else
{
<tr>
<td>@materialResult.Material.DisplayName</td>
<td>@ArchUnits.FormatFromInches(group.Key)</td>
<td>@FormatResultLength(group.Key)</td>
<td class="text-end">@group.Count()</td>
</tr>
}
@@ -1158,7 +1166,7 @@ else
{
<tr>
<td>@binNum</td>
<td style="white-space: nowrap;">@ArchUnits.FormatFromInches(entry.Bin.Length)</td>
<td style="white-space: nowrap;">@FormatResultLength(entry.Bin.Length)</td>
<td>
@foreach (var item in entry.Bin.Items)
{
@@ -1166,12 +1174,13 @@ else
@if (!string.IsNullOrWhiteSpace(item.Name))
{
<span class="cut-part-badge-name">@item.Name</span>
<span class="cut-part-badge-divider" aria-hidden="true"></span>
}
<span class="cut-part-badge-length">@ArchUnits.FormatFromInches(item.Length)</span>
<span class="cut-part-badge-length">@FormatResultLength(item.Length)</span>
</span>
}
</td>
<td style="white-space: nowrap;">@ArchUnits.FormatFromInches(entry.Bin.RemainingLength)</td>
<td style="white-space: nowrap;">@FormatResultLength(entry.Bin.RemainingLength)</td>
</tr>
binNum++;
}
+1
View File
@@ -275,6 +275,7 @@ a, .btn-link { color: var(--accent-dark); }
/* Results cut badges keep the part number and its length visually distinct. */
.cut-part-badge { background-color: #6c8ebf; display: inline-flex; font-weight: 500; overflow: hidden; padding: 0; }
.cut-part-badge-name { padding: .35em .55em; }
.cut-part-badge-divider { border-left: 1px solid rgba(255, 255, 255, .75); margin: .2em 0; }
.cut-part-badge-length { background-color: #4c6680; padding: .35em .55em; }
/* Overview */
+14
View File
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Mcp", "CutList.Mcp\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Web", "CutList.Web\CutList.Web.csproj", "{E3B33DE6-803C-4557-BF40-D8A5DB154144}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Core.Tests", "CutList.Core.Tests\CutList.Core.Tests.csproj", "{95E386F9-C906-423D-9869-BB1D2EA755E5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +71,18 @@ Global
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|x64.Build.0 = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|x86.ActiveCfg = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|x86.Build.0 = Release|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Debug|x64.ActiveCfg = Debug|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Debug|x64.Build.0 = Debug|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Debug|x86.ActiveCfg = Debug|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Debug|x86.Build.0 = Debug|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Release|Any CPU.Build.0 = Release|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Release|x64.ActiveCfg = Release|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Release|x64.Build.0 = Release|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Release|x86.ActiveCfg = Release|Any CPU
{95E386F9-C906-423D-9869-BB1D2EA755E5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE