fix(ui): reject timed-out stock and shape input
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.WinForms.Tests;
|
||||||
|
|
||||||
|
public class ArchUnitsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ArchitecturalRegexHasExplicitTimeout()
|
||||||
|
{
|
||||||
|
var regex = Assert.IsType<Regex>(typeof(ArchUnits)
|
||||||
|
.GetField("UnitRegex", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||||
|
Assert.Equal(TimeSpan.FromMilliseconds(250), regex.MatchTimeout);
|
||||||
|
Assert.Equal(RegexOptions.None, regex.Options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("5' 6.5\"", 66.5)]
|
||||||
|
[InlineData("2 feet 3 1/2 inches", 27.5)]
|
||||||
|
[InlineData("1-1/2\"", 1.5)]
|
||||||
|
[InlineData("48", 48)]
|
||||||
|
[InlineData("", 0)]
|
||||||
|
public void OrdinaryArchitecturalInputsKeepTheirMeaning(string input, double expected)
|
||||||
|
{
|
||||||
|
var original = CultureInfo.CurrentCulture;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||||
|
Assert.Equal(expected, ArchUnits.ParseToInches(input));
|
||||||
|
}
|
||||||
|
finally { CultureInfo.CurrentCulture = original; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidFieldIsRedAndNaNAndCorrectionRestoresIt() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var field = new TextBox { Text = "invalid dimension" };
|
||||||
|
Assert.True(double.IsNaN(ArchUnits.GetLengthInches(field)));
|
||||||
|
Assert.Equal(Color.Red, field.ForeColor);
|
||||||
|
field.Text = "12\"";
|
||||||
|
Assert.Equal(12, ArchUnits.GetLengthInches(field));
|
||||||
|
Assert.Equal(SystemColors.WindowText, field.ForeColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actual timeout execution is covered by the shared Regex mechanism in FractionTests;
|
||||||
|
// these desktop tests deliberately avoid timing-sensitive oversized-input fixtures.
|
||||||
|
internal static void RunSta(Action action)
|
||||||
|
{
|
||||||
|
Exception? error = null;
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try { action(); }
|
||||||
|
catch (Exception ex) { error = ex; }
|
||||||
|
});
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
Assert.True(thread.Join(TimeSpan.FromSeconds(15)));
|
||||||
|
if (error != null)
|
||||||
|
ExceptionDispatchInfo.Capture(error).Throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.ExceptionServices;
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using OpenNest.Forms;
|
using OpenNest.Forms;
|
||||||
|
|
||||||
@@ -73,6 +75,117 @@ public class AutoNestFormStockOptionsTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StockSizeRegexHasExplicitTimeout()
|
||||||
|
{
|
||||||
|
var regex = Assert.IsType<Regex>(typeof(AutoNestForm)
|
||||||
|
.GetField("SizePattern", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||||
|
Assert.Equal(TimeSpan.FromMilliseconds(250), regex.MatchTimeout);
|
||||||
|
Assert.Equal(RegexOptions.None, regex.Options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("48 x")]
|
||||||
|
[InlineData("abc")]
|
||||||
|
[InlineData("0 x 96")]
|
||||||
|
[InlineData("-1 x 96")]
|
||||||
|
[InlineData("١ x 96")]
|
||||||
|
public void NonblankInvalidRowRejectsWholeCollection(string invalid) => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var form = new StockTestForm();
|
||||||
|
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96, Cost = 25 } }, 0.5);
|
||||||
|
var grid = GetGrid(form);
|
||||||
|
var rows = Assert.IsAssignableFrom<IBindingList>(grid.DataSource);
|
||||||
|
var row = rows.AddNew()!;
|
||||||
|
SetValue(row, "Size", invalid);
|
||||||
|
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||||
|
|
||||||
|
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||||
|
Assert.Empty(options);
|
||||||
|
Assert.Contains(invalid, error);
|
||||||
|
Assert.Contains("row 2", error);
|
||||||
|
Assert.Throws<FormatException>(() => form.GetPlateOptions());
|
||||||
|
Assert.NotEmpty(grid.Rows[1].ErrorText);
|
||||||
|
|
||||||
|
SetValue(row, "Size", "72.5 × 144.25");
|
||||||
|
Assert.True(form.TryGetPlateOptions(out options, out error));
|
||||||
|
Assert.Null(error);
|
||||||
|
Assert.Equal(2, options.Count);
|
||||||
|
Assert.Equal(72.5, options[1].Width);
|
||||||
|
Assert.Equal(144.25, options[1].Length);
|
||||||
|
Assert.Equal(25, options[0].Cost);
|
||||||
|
Assert.Empty(grid.Rows[1].ErrorText);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TimeoutAfterValidRowPublishesNoPrefixAndDoesNotAcceptDialog() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var form = new StockTestForm();
|
||||||
|
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96 } }, 0.5);
|
||||||
|
var grid = GetGrid(form);
|
||||||
|
var rows = Assert.IsAssignableFrom<IBindingList>(grid.DataSource);
|
||||||
|
var row = rows.AddNew()!;
|
||||||
|
SetValue(row, "Size", "timeout row");
|
||||||
|
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||||
|
form.FailOn = "timeout row";
|
||||||
|
|
||||||
|
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||||
|
Assert.Empty(options);
|
||||||
|
Assert.Contains("timeout row", error);
|
||||||
|
Assert.True(form.TryClose(DialogResult.OK));
|
||||||
|
Assert.Equal(DialogResult.None, form.DialogResult);
|
||||||
|
Assert.Contains("timeout row", Assert.Single(form.Notices));
|
||||||
|
Assert.True(form.MatchCalls > 1);
|
||||||
|
|
||||||
|
form.Notices.Clear();
|
||||||
|
Assert.False(form.TryClose(DialogResult.Cancel));
|
||||||
|
Assert.Empty(form.Notices);
|
||||||
|
Assert.False(Assert.IsType<Button>(form.Controls.Find("cancelButton", true).Single()).CausesValidation);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidPendingEditCannotPublishOldBoundValue() => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var form = new StockTestForm();
|
||||||
|
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96 } }, 0.5);
|
||||||
|
var grid = GetGrid(form);
|
||||||
|
form.Show();
|
||||||
|
grid.CurrentCell = grid.Rows[0].Cells[0];
|
||||||
|
Assert.True(grid.BeginEdit(false));
|
||||||
|
grid.EditingControl.Text = "48 x";
|
||||||
|
|
||||||
|
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||||
|
Assert.Empty(options);
|
||||||
|
Assert.Contains("48 x", error);
|
||||||
|
grid.CancelEdit();
|
||||||
|
});
|
||||||
|
|
||||||
|
private sealed class StockTestForm : AutoNestForm
|
||||||
|
{
|
||||||
|
public StockTestForm() : base(new Nest()) { }
|
||||||
|
public string? FailOn { get; set; }
|
||||||
|
public int MatchCalls { get; private set; }
|
||||||
|
public List<string> Notices { get; } = new();
|
||||||
|
|
||||||
|
internal override Match MatchStockSize(string value)
|
||||||
|
{
|
||||||
|
MatchCalls++;
|
||||||
|
if (value == FailOn)
|
||||||
|
throw new RegexMatchTimeoutException();
|
||||||
|
return base.MatchStockSize(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override void ReportStockValidationFailure(string error) => Notices.Add(error);
|
||||||
|
|
||||||
|
public bool TryClose(DialogResult result)
|
||||||
|
{
|
||||||
|
DialogResult = result;
|
||||||
|
var closing = new FormClosingEventArgs(CloseReason.UserClosing, false);
|
||||||
|
OnFormClosing(closing);
|
||||||
|
return closing.Cancel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static DataGridView GetGrid(AutoNestForm form)
|
private static DataGridView GetGrid(AutoNestForm form)
|
||||||
{
|
{
|
||||||
// Binding needs a context even when the test does not show the form.
|
// Binding needs a context even when the test does not show the form.
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using OpenNest.Controls;
|
||||||
|
using OpenNest.Forms;
|
||||||
|
using OpenNest.Shapes;
|
||||||
|
|
||||||
|
namespace OpenNest.WinForms.Tests.Forms;
|
||||||
|
|
||||||
|
public class ShapeLibraryFormTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FriendlyNameRegexHasExplicitTimeout()
|
||||||
|
{
|
||||||
|
var regex = Assert.IsType<Regex>(typeof(ShapeLibraryForm)
|
||||||
|
.GetField("FriendlyNamePattern", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||||
|
Assert.Equal(TimeSpan.FromMilliseconds(100), regex.MatchTimeout);
|
||||||
|
Assert.Equal(RegexOptions.None, regex.Options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("PipeFlangeShape", "Pipe Flange")]
|
||||||
|
[InlineData("HolePatternDiameter", "Hole Pattern Diameter")]
|
||||||
|
[InlineData("NgonShape", "Ngon")]
|
||||||
|
[InlineData("ABCShape", "ABC")]
|
||||||
|
public void FriendlyNamesKeepSuffixAndWordBoundaryBehavior(string input, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, ShapeLibraryForm.FriendlyName(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("PipeFlangeShape", "PipeFlange")]
|
||||||
|
[InlineData("HolePatternDiameter", "HolePatternDiameter")]
|
||||||
|
public void FriendlyNameTimeoutDiscardsPartialFormatting(string input, string expected)
|
||||||
|
{
|
||||||
|
var notices = new List<string>();
|
||||||
|
var result = ShapeLibraryForm.FriendlyName(input, name =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, name);
|
||||||
|
// Simulate a replacement failing after it has transformed a prefix internally.
|
||||||
|
var partial = name.Insert(4, " ");
|
||||||
|
Assert.NotEqual(expected, partial);
|
||||||
|
throw new RegexMatchTimeoutException();
|
||||||
|
}, notices.Add);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
Assert.Contains(expected, Assert.Single(notices));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidShapeInputClearsOldPreviewAndBlocksAddUntilCorrected() => ArchUnitsTests.RunSta(() =>
|
||||||
|
{
|
||||||
|
using var form = new ShapeLibraryForm();
|
||||||
|
var list = Find<ListBox>(form, "shapeListBox");
|
||||||
|
list.SelectedItem = list.Items.Cast<object>().Single(entry =>
|
||||||
|
(Type)entry.GetType().GetProperty("ShapeType")!.GetValue(entry)! == typeof(RectangleShape));
|
||||||
|
var preview = Find<ShapePreviewControl>(form, "previewBox");
|
||||||
|
var add = Find<Button>(form, "addButton");
|
||||||
|
var input = Find<Panel>(form, "parametersPanel").Controls.OfType<TextBox>().First();
|
||||||
|
var valid = input.Text;
|
||||||
|
Assert.NotEmpty(preview.Plate.Parts);
|
||||||
|
Assert.True(add.Enabled);
|
||||||
|
|
||||||
|
input.Text = "invalid dimension";
|
||||||
|
Assert.Equal(Color.Red, input.ForeColor);
|
||||||
|
Assert.Empty(preview.Plate.Parts);
|
||||||
|
Assert.False(add.Enabled);
|
||||||
|
InvokeAdd(form);
|
||||||
|
Assert.Empty(form.GetDrawings());
|
||||||
|
|
||||||
|
input.Text = valid;
|
||||||
|
Assert.Equal(SystemColors.WindowText, input.ForeColor);
|
||||||
|
Assert.NotEmpty(preview.Plate.Parts);
|
||||||
|
Assert.True(add.Enabled);
|
||||||
|
InvokeAdd(form);
|
||||||
|
Assert.Single(form.GetDrawings());
|
||||||
|
});
|
||||||
|
|
||||||
|
private static T Find<T>(Control form, string name) where T : Control =>
|
||||||
|
Assert.IsType<T>(form.Controls.Find(name, true).Single());
|
||||||
|
|
||||||
|
private static void InvokeAdd(ShapeLibraryForm form) => typeof(ShapeLibraryForm)
|
||||||
|
.GetMethod("AddButton_Click", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.Invoke(form, new object?[] { null, EventArgs.Empty });
|
||||||
|
}
|
||||||
@@ -10,7 +10,9 @@ namespace OpenNest
|
|||||||
public static class ArchUnits
|
public static class ArchUnits
|
||||||
{
|
{
|
||||||
private static readonly Regex UnitRegex = new Regex(
|
private static readonly Regex UnitRegex = new Regex(
|
||||||
"^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$"
|
"^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$",
|
||||||
|
RegexOptions.None,
|
||||||
|
TimeSpan.FromMilliseconds(250)
|
||||||
);
|
);
|
||||||
|
|
||||||
public static double ParseToInches(string input)
|
public static double ParseToInches(string input)
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
public partial class AutoNestForm : Form
|
public partial class AutoNestForm : Form
|
||||||
{
|
{
|
||||||
private static readonly Regex SizePattern = new(@"^(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)$");
|
private static readonly Regex SizePattern = new(
|
||||||
|
@"^(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)$",
|
||||||
|
RegexOptions.None,
|
||||||
|
TimeSpan.FromMilliseconds(250)
|
||||||
|
);
|
||||||
|
|
||||||
public AutoNestForm(Nest nest)
|
public AutoNestForm(Nest nest)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
cancelButton.CausesValidation = false;
|
||||||
SetupPartsGrid();
|
SetupPartsGrid();
|
||||||
SetupPlateGrid();
|
SetupPlateGrid();
|
||||||
LoadEngines();
|
LoadEngines();
|
||||||
@@ -195,30 +200,65 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
public List<PlateOption> GetPlateOptions()
|
public List<PlateOption> GetPlateOptions()
|
||||||
{
|
{
|
||||||
plateGrid.EndEdit();
|
if (!TryGetPlateOptions(out var options, out var error))
|
||||||
var result = new List<PlateOption>();
|
throw new FormatException(error);
|
||||||
var gridItems = plateGrid.DataSource as BindingList<PlateOptionItem>;
|
return options;
|
||||||
if (gridItems == null)
|
}
|
||||||
return result;
|
|
||||||
|
|
||||||
foreach (var item in gridItems)
|
public bool TryGetPlateOptions(out List<PlateOption> options, out string error)
|
||||||
|
{
|
||||||
|
options = new List<PlateOption>();
|
||||||
|
error = null;
|
||||||
|
if (!plateGrid.EndEdit())
|
||||||
{
|
{
|
||||||
if (!TryParseSize(item.Size, out var width, out var length))
|
var value = plateGrid.EditingControl?.Text ?? plateGrid.CurrentCell?.Value?.ToString();
|
||||||
continue;
|
error = $"Invalid stock size '{value}'. Enter positive dimensions as W x L.";
|
||||||
if (width <= 0 || length <= 0)
|
return false;
|
||||||
continue;
|
|
||||||
|
|
||||||
result.Add(
|
|
||||||
new PlateOption
|
|
||||||
{
|
|
||||||
Width = width,
|
|
||||||
Length = length,
|
|
||||||
Cost = item.Cost,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
var gridItems = plateGrid.DataSource as BindingList<PlateOptionItem>;
|
||||||
|
if (gridItems == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var validated = new List<PlateOption>();
|
||||||
|
for (var index = 0; index < gridItems.Count; index++)
|
||||||
|
{
|
||||||
|
var item = gridItems[index];
|
||||||
|
if (string.IsNullOrWhiteSpace(item.Size))
|
||||||
|
continue;
|
||||||
|
if (!TryParseSize(item.Size, out var width, out var length))
|
||||||
|
{
|
||||||
|
error = $"Invalid stock size '{item.Size}' in row {index + 1}. Enter positive dimensions as W x L.";
|
||||||
|
if (index < plateGrid.Rows.Count)
|
||||||
|
plateGrid.Rows[index].ErrorText = error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < plateGrid.Rows.Count)
|
||||||
|
plateGrid.Rows[index].ErrorText = "";
|
||||||
|
validated.Add(new PlateOption { Width = width, Length = length, Cost = item.Cost });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish only a completely validated collection, never a valid prefix.
|
||||||
|
options = validated;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||||
|
{
|
||||||
|
if (DialogResult == DialogResult.OK && OptimizePlateSize
|
||||||
|
&& !TryGetPlateOptions(out _, out var error))
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
DialogResult = DialogResult.None;
|
||||||
|
ReportStockValidationFailure(error);
|
||||||
|
}
|
||||||
|
base.OnFormClosing(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal virtual void ReportStockValidationFailure(string error)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, error, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadPlateOptions(List<PlateOption> options, double salvageRate)
|
public void LoadPlateOptions(List<PlateOption> options, double salvageRate)
|
||||||
@@ -300,7 +340,10 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
var value = e.FormattedValue?.ToString();
|
var value = e.FormattedValue?.ToString();
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
plateGrid.Rows[e.RowIndex].ErrorText = "";
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryParseSize(value, out _, out _))
|
if (!TryParseSize(value, out _, out _))
|
||||||
{
|
{
|
||||||
@@ -313,26 +356,44 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryParseSize(string value, out double width, out double length)
|
private bool TryParseSize(string value, out double width, out double length)
|
||||||
{
|
{
|
||||||
width = 0;
|
width = 0;
|
||||||
length = 0;
|
length = 0;
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
return false;
|
return false;
|
||||||
var match = SizePattern.Match(value.Trim());
|
try
|
||||||
if (!match.Success)
|
{
|
||||||
|
var match = MatchStockSize(value.Trim());
|
||||||
|
if (!match.Success)
|
||||||
|
return false;
|
||||||
|
var parsedWidth = double.Parse(
|
||||||
|
match.Groups[1].Value,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture
|
||||||
|
);
|
||||||
|
var parsedLength = double.Parse(
|
||||||
|
match.Groups[2].Value,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture
|
||||||
|
);
|
||||||
|
if (!double.IsFinite(parsedWidth) || !double.IsFinite(parsedLength)
|
||||||
|
|| parsedWidth <= 0 || parsedLength <= 0)
|
||||||
|
return false;
|
||||||
|
width = parsedWidth;
|
||||||
|
length = parsedLength;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (RegexMatchTimeoutException)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
width = double.Parse(
|
}
|
||||||
match.Groups[1].Value,
|
catch (FormatException)
|
||||||
System.Globalization.CultureInfo.InvariantCulture
|
{
|
||||||
);
|
return false;
|
||||||
length = double.Parse(
|
}
|
||||||
match.Groups[2].Value,
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal virtual Match MatchStockSize(string value) => SizePattern.Match(value);
|
||||||
|
|
||||||
private static string FormatSize(double width, double length)
|
private static string FormatSize(double width, double length)
|
||||||
{
|
{
|
||||||
return $"{width:G} x {length:G}";
|
return $"{width:G} x {length:G}";
|
||||||
|
|||||||
@@ -1381,10 +1381,15 @@ namespace OpenNest.Forms
|
|||||||
var items = form.GetNestItems();
|
var items = form.GetNestItems();
|
||||||
if (!items.Any(it => it.Quantity > 0))
|
if (!items.Any(it => it.Quantity > 0))
|
||||||
return;
|
return;
|
||||||
|
var plateOptions = default(List<PlateOption>);
|
||||||
|
if (form.OptimizePlateSize && !form.TryGetPlateOptions(out plateOptions, out var stockError))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, stockError, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var engineName = form.EngineName ?? EngineSelection.EngineName;
|
var engineName = form.EngineName ?? EngineSelection.EngineName;
|
||||||
EngineSelection.EngineName = engineName;
|
EngineSelection.EngineName = engineName;
|
||||||
engineComboBox.SelectedItem = engineName;
|
engineComboBox.SelectedItem = engineName;
|
||||||
var plateOptions = form.OptimizePlateSize ? form.GetPlateOptions() : null;
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
nestingCts = cts;
|
nestingCts = cts;
|
||||||
using var progressForm = new NestProgressForm(cts, showPlateRow: true)
|
using var progressForm = new NestProgressForm(cts, showPlateRow: true)
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
public partial class ShapeLibraryForm : Form
|
public partial class ShapeLibraryForm : Form
|
||||||
{
|
{
|
||||||
|
private static readonly Regex FriendlyNamePattern = new(
|
||||||
|
@"(?<=[a-z0-9])([A-Z])",
|
||||||
|
RegexOptions.None,
|
||||||
|
TimeSpan.FromMilliseconds(100)
|
||||||
|
);
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
@@ -272,9 +278,13 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
var shape = CreateShapeFromInputs();
|
var shape = CreateShapeFromInputs();
|
||||||
if (shape == null)
|
if (shape == null)
|
||||||
|
{
|
||||||
|
InvalidatePreview();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
|
addButton.Enabled = drawing?.Program != null;
|
||||||
nameTextBox.Text = shape.GenerateName();
|
nameTextBox.Text = shape.GenerateName();
|
||||||
previewBox.ShowDrawing(drawing);
|
previewBox.ShowDrawing(drawing);
|
||||||
|
|
||||||
@@ -298,10 +308,17 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
previewBox.ShowDrawing(null);
|
InvalidatePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InvalidatePreview()
|
||||||
|
{
|
||||||
|
addButton.Enabled = false;
|
||||||
|
previewBox.ShowDrawing(null);
|
||||||
|
previewBox.SetInfo("Invalid shape input");
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdatePipeSizeFilter()
|
private void UpdatePipeSizeFilter()
|
||||||
{
|
{
|
||||||
// Find the PipeSize combo and the numeric inputs it depends on.
|
// Find the PipeSize combo and the numeric inputs it depends on.
|
||||||
@@ -424,7 +441,10 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
var shape = CreateShapeFromInputs();
|
var shape = CreateShapeFromInputs();
|
||||||
if (shape == null)
|
if (shape == null)
|
||||||
|
{
|
||||||
|
InvalidatePreview();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
drawing.Name = GetUniqueName(drawing.Name);
|
drawing.Name = GetUniqueName(drawing.Name);
|
||||||
@@ -461,12 +481,28 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FriendlyName(string name)
|
internal static string FriendlyName(
|
||||||
|
string name,
|
||||||
|
Func<string, string> replace = null,
|
||||||
|
Action<string> report = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (name.EndsWith("Shape"))
|
if (name.EndsWith("Shape"))
|
||||||
name = name.Substring(0, name.Length - 5);
|
name = name.Substring(0, name.Length - 5);
|
||||||
|
|
||||||
return Regex.Replace(name, @"(?<=[a-z0-9])([A-Z])", " $1");
|
try
|
||||||
|
{
|
||||||
|
return replace != null ? replace(name) : FriendlyNamePattern.Replace(name, " $1");
|
||||||
|
}
|
||||||
|
catch (RegexMatchTimeoutException)
|
||||||
|
{
|
||||||
|
var diagnostic = $"Friendly-name formatting timed out for '{name}'; using the unchanged name.";
|
||||||
|
if (report != null)
|
||||||
|
report(diagnostic);
|
||||||
|
else
|
||||||
|
System.Diagnostics.Trace.TraceWarning(diagnostic);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ShapeEntry
|
private class ShapeEntry
|
||||||
|
|||||||
Reference in New Issue
Block a user