mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 15:01:07 -04:00
feat(library): redesign Edit Drawing with drop-downs, color modes and a rotation dial
Typing customer, material, grade and thickness by hand let near-duplicate spellings into the library, which then split drawings that should match. The fields are now drop-downs of the values the library and the machine material catalog already use, while still accepting new values; thicknesses already used for the material are listed ahead of the standard gauges. A library color could be set but never cleared back to the color scheme's, so Automatic and Reset are added. The rotation start/end/step boxes gave no sense of the range, so a dial shades it, ticks the angles the engines try (via RotationPolicy) and lets either end be dragged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ba5539fab
commit
6e40c2e6ed
10 files changed
+1524
-177
No files matched your search
@@ -0,0 +1,83 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The customers, materials, grades and thicknesses an edit offers in its drop-downs: those
|
||||
/// already used by library drawings, plus the materials and grades of a material catalog.
|
||||
/// Names match by <see cref="SharedListNames.Key"/> and keep the first spelling seen, so
|
||||
/// pass the newest records first. Thicknesses come only from records in the edit's units.
|
||||
/// The lists are suggestions; any typed value may still be saved.
|
||||
/// </summary>
|
||||
public sealed class DrawingLibraryChoices
|
||||
{
|
||||
private readonly List<DrawingLibraryRecord> _records;
|
||||
private readonly List<MaterialConfig> _catalog;
|
||||
|
||||
private DrawingLibraryChoices(List<DrawingLibraryRecord> records, List<MaterialConfig> catalog, Units units)
|
||||
{
|
||||
_records = records;
|
||||
Units = units;
|
||||
_catalog = catalog;
|
||||
Customers = Distinct(records.Select(r => r.Customer));
|
||||
Materials = Distinct(records.Select(r => r.MaterialName).Concat(catalog.Select(m => m.Name)));
|
||||
}
|
||||
|
||||
public static DrawingLibraryChoices Empty { get; } = new(new(), new(), Units.Inches);
|
||||
|
||||
/// <param name="records">Library drawings, newest first; archived ones are skipped.</param>
|
||||
/// <param name="units">The edit's units; thicknesses in other units are not offered.</param>
|
||||
/// <param name="catalog">Materials and grades on file, for example the machine configurations'.</param>
|
||||
public static DrawingLibraryChoices From(IEnumerable<DrawingLibraryRecord> records, Units units,
|
||||
IEnumerable<MaterialConfig>? catalog = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(records);
|
||||
return new DrawingLibraryChoices(records.Where(r => r != null && !r.Archived).ToList(),
|
||||
catalog?.Where(m => m != null).ToList() ?? new(), units);
|
||||
}
|
||||
|
||||
public Units Units { get; }
|
||||
|
||||
/// <summary>Customers, sorted.</summary>
|
||||
public IReadOnlyList<string> Customers { get; }
|
||||
|
||||
/// <summary>Material names, sorted.</summary>
|
||||
public IReadOnlyList<string> Materials { get; }
|
||||
|
||||
/// <summary>The grades used or on file for <paramref name="material"/>, sorted.</summary>
|
||||
public IReadOnlyList<string> GradesOf(string? material)
|
||||
{
|
||||
var key = SharedListNames.Key(material);
|
||||
if (key.Length == 0)
|
||||
return Array.Empty<string>();
|
||||
return Distinct(_records.Where(r => SharedListNames.Key(r.MaterialName) == key).Select(r => r.MaterialGrade)
|
||||
.Concat(_catalog.Where(m => SharedListNames.Key(m.Name) == key).Select(m => m.Grade)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The thicknesses library drawings of <paramref name="material"/> (and of
|
||||
/// <paramref name="grade"/>, when one is given) use, thinnest first.
|
||||
/// </summary>
|
||||
public IReadOnlyList<double> ThicknessesOf(string? material, string? grade)
|
||||
{
|
||||
var materialKey = SharedListNames.Key(material);
|
||||
if (materialKey.Length == 0)
|
||||
return Array.Empty<double>();
|
||||
var gradeKey = SharedListNames.Key(grade);
|
||||
return _records
|
||||
.Where(r => r.Units == Units && SharedListNames.Key(r.MaterialName) == materialKey
|
||||
&& (gradeKey.Length == 0 || SharedListNames.Key(r.MaterialGrade) == gradeKey)
|
||||
&& double.IsFinite(r.Thickness) && r.Thickness > 0)
|
||||
.Select(r => DrawingLibraryRules.NormalizeThickness(r.Thickness))
|
||||
.Distinct()
|
||||
.OrderBy(t => t)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> Distinct(IEnumerable<string?> names) =>
|
||||
names
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Select(name => MaterialListRules.Spelling(name))
|
||||
.GroupBy(SharedListNames.Key)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
@@ -99,6 +99,34 @@ public sealed class DrawingLibraryEditSession
|
||||
return matches.Where(m => m.Id != Listed.Id).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The drop-down choices for this edit: the customers, materials, grades and thicknesses
|
||||
/// of the active library drawings (the newest <see cref="DrawingLibrarySaveSession.MaxScannedDrawings"/>
|
||||
/// when there are more) and of the listed drawing, plus <paramref name="catalog"/>'s materials.
|
||||
/// </summary>
|
||||
public async Task<DrawingLibraryChoices> LoadChoicesAsync(IEnumerable<MaterialConfig>? catalog = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var records = new List<DrawingLibraryRecord>();
|
||||
var offset = 0;
|
||||
while (offset < DrawingLibrarySaveSession.MaxScannedDrawings)
|
||||
{
|
||||
var page = await _library.QueryAsync(new DrawingLibraryQuery
|
||||
{
|
||||
Sort = DrawingSortField.SavedAt,
|
||||
Descending = true,
|
||||
Offset = offset,
|
||||
Limit = DrawingLibraryQuery.MaxLimit,
|
||||
}, cancellationToken);
|
||||
records.AddRange(page.Items);
|
||||
offset += page.Items.Count;
|
||||
if (page.Items.Count == 0 || offset >= page.Total)
|
||||
break;
|
||||
}
|
||||
records.Add(Listed);
|
||||
return DrawingLibraryChoices.From(records, Listed.Units, catalog);
|
||||
}
|
||||
|
||||
// The record with the edited details and the listed notes and settings, for identity checks.
|
||||
private DrawingLibraryRecord BuildDetails(DrawingLibraryDetails details) =>
|
||||
Build(details, Listed.Notes, Listed.Settings ?? new DrawingNestingSettings());
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
public class DrawingLibraryChoicesTests
|
||||
{
|
||||
private static DrawingLibraryRecord Record(string customer, string material, string grade, double thickness,
|
||||
Units units = Units.Inches, bool archived = false) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Revision = 1,
|
||||
Name = "Synthetic part",
|
||||
Customer = customer,
|
||||
MaterialName = material,
|
||||
MaterialGrade = grade,
|
||||
Thickness = thickness,
|
||||
Units = units,
|
||||
Archived = archived,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Lists_KeepTheFirstSpellingOfEachName_Sorted_AndSkipBlanksAndArchivedDrawings()
|
||||
{
|
||||
var choices = DrawingLibraryChoices.From(new[]
|
||||
{
|
||||
Record("Synthetic Co", "Synthetic Steel", "Grade A", 0.25),
|
||||
Record("synthetic co", "SYNTHETIC STEEL", "grade a", 0.25),
|
||||
Record("", "Alpha Alloy", "", 0.5),
|
||||
Record("Archived Co", "Archived Metal", "X", 1, archived: true),
|
||||
}, Units.Inches, new[] { new MaterialConfig { Name = "Synthetic Steel", Grade = "Grade C" }, new MaterialConfig { Name = "Beta Brass" } });
|
||||
|
||||
Assert.Equal(new[] { "Synthetic Co" }, choices.Customers);
|
||||
Assert.Equal(new[] { "Alpha Alloy", "Beta Brass", "Synthetic Steel" }, choices.Materials);
|
||||
Assert.Equal(new[] { "Grade A", "Grade C" }, choices.GradesOf(" synthetic steel "));
|
||||
Assert.Empty(choices.GradesOf(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Thicknesses_AreTheMaterialsInTheEditsUnits_NarrowedByAGivenGrade()
|
||||
{
|
||||
var choices = DrawingLibraryChoices.From(new[]
|
||||
{
|
||||
Record("", "Synthetic Steel", "Grade A", 0.25),
|
||||
Record("", "Synthetic Steel", "Grade B", 0.1875),
|
||||
Record("", "Synthetic Steel", "Grade B", 0.18750001),
|
||||
Record("", "Synthetic Steel", "Grade A", 6.35, Units.Millimeters),
|
||||
Record("", "Synthetic Steel", "Grade A", 0),
|
||||
Record("", "Other", "Grade A", 1),
|
||||
}, Units.Inches);
|
||||
|
||||
Assert.Equal(new[] { 0.1875, 0.25 }, choices.ThicknessesOf("Synthetic Steel", ""));
|
||||
Assert.Equal(new[] { 0.1875 }, choices.ThicknessesOf("synthetic steel", "GRADE B"));
|
||||
Assert.Empty(choices.ThicknessesOf("", "Grade A"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditSession_LoadsChoicesFromEveryPage_AndIncludesTheListedDrawing()
|
||||
{
|
||||
var listed = Record("Listed Co", "Listed Metal", "L1", 0.5);
|
||||
var records = Enumerable.Range(0, DrawingLibraryQuery.MaxLimit + 3)
|
||||
.Select(i => Record($"Customer {i:000}", "Synthetic Steel", "Grade A", 0.25))
|
||||
.ToList();
|
||||
var library = new PagedLibrary(records);
|
||||
|
||||
var choices = await new DrawingLibraryEditSession(library, listed, "").LoadChoicesAsync();
|
||||
|
||||
Assert.Equal(2, library.Queries.Count);
|
||||
Assert.All(library.Queries, q => Assert.False(q.IncludeArchived));
|
||||
Assert.Equal(records.Count + 1, choices.Customers.Count);
|
||||
Assert.Contains("Listed Metal", choices.Materials);
|
||||
}
|
||||
|
||||
private sealed class PagedLibrary : IDrawingLibraryStore
|
||||
{
|
||||
private readonly List<DrawingLibraryRecord> _records;
|
||||
|
||||
public PagedLibrary(List<DrawingLibraryRecord> records) => _records = records;
|
||||
|
||||
public List<DrawingLibraryQuery> Queries { get; } = new();
|
||||
|
||||
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Queries.Add(query);
|
||||
return Task.FromResult(new DrawingLibraryPage
|
||||
{
|
||||
Items = _records.Skip(query.Offset).Take(query.Limit).ToArray(),
|
||||
Total = _records.Count,
|
||||
Offset = query.Offset,
|
||||
Limit = query.Limit,
|
||||
});
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord?> GetAsync(Guid id, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryPage?> GetRevisionsAsync(Guid id, int offset = 0,
|
||||
int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord> UploadAsync(DrawingLibraryRecord record, byte[] package,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord> UpdatePackageAsync(Guid id, DrawingLibraryRecord record, byte[] package,
|
||||
int expectedRevision, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DrawingLibraryRecord> UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Controls;
|
||||
|
||||
public class RotationRangeDialTests
|
||||
{
|
||||
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
[Fact]
|
||||
public void TriedAngles_FollowTheEnginesRotationPolicy()
|
||||
{
|
||||
Assert.Equal(new[] { 0d, 90d, 180d, 270d }, RotationRangeDial.TriedAngles(10, 20, 0, false));
|
||||
Assert.Equal(7, RotationRangeDial.TriedAngles(0, 90, 15, false).Count);
|
||||
Assert.Equal(14, RotationRangeDial.TriedAngles(0, 90, 15, true).Count);
|
||||
Assert.Equal(new[] { 30d }, RotationRangeDial.TriedAngles(30, 30, 360, false));
|
||||
Assert.Empty(RotationRangeDial.TriedAngles(90, 0, 15, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_KeepTheStartAtOrBeforeTheEnd_AndDoNotRaiseTheOperatorEvent()
|
||||
{
|
||||
using var dial = new RotationRangeDial();
|
||||
var raised = 0;
|
||||
dial.AnglesChanged += (_, _) => raised++;
|
||||
|
||||
dial.EndAngle = 90;
|
||||
dial.StartAngle = 120;
|
||||
Assert.Equal((120d, 120d), (dial.StartAngle, dial.EndAngle));
|
||||
dial.EndAngle = 400;
|
||||
dial.StartAngle = -5;
|
||||
Assert.Equal((0d, 360d), (dial.StartAngle, dial.EndAngle));
|
||||
Assert.Equal(0, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Keys_MoveTheActiveHandleWithinTheOtherHandle()
|
||||
{
|
||||
using var dial = new RotationRangeDial { StartAngle = 0, EndAngle = 30 };
|
||||
var raised = 0;
|
||||
dial.AnglesChanged += (_, _) => raised++;
|
||||
|
||||
dial.ActiveHandle = DialHandle.Start;
|
||||
dial.MoveActive(15);
|
||||
dial.MoveActive(15);
|
||||
dial.MoveActive(15);
|
||||
Assert.Equal((30d, 30d), (dial.StartAngle, dial.EndAngle));
|
||||
|
||||
dial.ActiveHandle = DialHandle.End;
|
||||
dial.MoveActive(-1);
|
||||
Assert.Equal(30d, dial.EndAngle);
|
||||
dial.MoveActive(1);
|
||||
Assert.Equal(31d, dial.EndAngle);
|
||||
Assert.Equal(3, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dragging_MovesTheNearerHandle_AndSweepsTheEndUpToAFullTurn()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
using var form = new Form { ClientSize = new Size(240, 240) };
|
||||
var dial = new DraggableDial { Bounds = new Rectangle(0, 0, 200, 200), StartAngle = 0, EndAngle = 90, StepAngle = 15 };
|
||||
form.Controls.Add(dial);
|
||||
form.Show();
|
||||
var raised = 0;
|
||||
dial.AnglesChanged += (_, _) => raised++;
|
||||
|
||||
// Pressing away from the handles at 180° takes the nearer (end) handle there.
|
||||
dial.Down(At(dial, 180));
|
||||
Assert.Equal((0d, 180d), (dial.StartAngle, dial.EndAngle));
|
||||
|
||||
// The drag follows the pointer past 0°, stopping at a full turn rather than wrapping to 10°.
|
||||
foreach (var angle in new[] { 270d, 300d, 340d, 10d, 40d })
|
||||
dial.Drag(At(dial, angle));
|
||||
dial.Up(At(dial, 40));
|
||||
Assert.Equal((0d, 360d), (dial.StartAngle, dial.EndAngle));
|
||||
|
||||
// Backing off from the full turn moves the end with the pointer again.
|
||||
dial.Down(At(dial, 300));
|
||||
Assert.Equal(300d, dial.EndAngle);
|
||||
dial.Up(At(dial, 300));
|
||||
Assert.True(raised > 0);
|
||||
},
|
||||
TestTimeout,
|
||||
"The rotation dial drag test did not complete.");
|
||||
}
|
||||
|
||||
// A point inside the dial at a small radius, so it is never on a handle.
|
||||
private static Point At(Control dial, double degrees)
|
||||
{
|
||||
var radians = degrees * System.Math.PI / 180;
|
||||
return new Point(
|
||||
(int)System.Math.Round(dial.Width / 2.0 + 30 * System.Math.Cos(radians)),
|
||||
(int)System.Math.Round(dial.Height / 2.0 - 30 * System.Math.Sin(radians)));
|
||||
}
|
||||
|
||||
private sealed class DraggableDial : RotationRangeDial
|
||||
{
|
||||
public void Down(Point point) => OnMouseDown(new MouseEventArgs(MouseButtons.Left, 1, point.X, point.Y, 0));
|
||||
|
||||
public void Drag(Point point) => OnMouseMove(new MouseEventArgs(MouseButtons.Left, 0, point.X, point.Y, 0));
|
||||
|
||||
public void Up(Point point) => OnMouseUp(new MouseEventArgs(MouseButtons.Left, 1, point.X, point.Y, 0));
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@ public class DrawingLibraryFormTests
|
||||
form.View.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
|
||||
form.View.EditDrawing = edit =>
|
||||
{
|
||||
using var editor = new EditLibraryDrawingForm(edit);
|
||||
using var editor = new EditLibraryDrawingForm(edit) { LoadMaterialCatalog = Array.Empty<MaterialConfig> };
|
||||
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
|
||||
editor.Show();
|
||||
var save = Field<Button>(editor, "saveButton");
|
||||
@@ -281,7 +281,7 @@ public class DrawingLibraryFormTests
|
||||
form.View.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
|
||||
form.View.EditDrawing = edit =>
|
||||
{
|
||||
using var editor = new EditLibraryDrawingForm(edit);
|
||||
using var editor = new EditLibraryDrawingForm(edit) { LoadMaterialCatalog = Array.Empty<MaterialConfig> };
|
||||
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
|
||||
editor.Show();
|
||||
var save = Field<Button>(editor, "saveButton");
|
||||
@@ -289,9 +289,9 @@ public class DrawingLibraryFormTests
|
||||
Assert.Equal("in", Field<Label>(editor, "unitsLabel").Text);
|
||||
Assert.False(save.Enabled);
|
||||
Field<TextBox>(editor, "nameBox").Text = "SYN-405B";
|
||||
Field<TextBox>(editor, "materialBox").Text = "Synthetic Stainless";
|
||||
Field<TextBox>(editor, "gradeBox").Text = "304";
|
||||
Field<NumericUpDown>(editor, "thicknessBox").Value = 0.1875m;
|
||||
Field<ComboBox>(editor, "materialBox").Text = "Synthetic Stainless";
|
||||
Field<ComboBox>(editor, "gradeBox").Text = "304";
|
||||
Field<OpenNest.Controls.ThicknessComboBox>(editor, "thicknessBox").Value = 0.1875;
|
||||
Field<ComboBox>(editor, "cutTypeBox").SelectedItem = "Waterjet";
|
||||
Assert.True(save.Enabled);
|
||||
Field<TextBox>(editor, "nameBox").Text = " ";
|
||||
@@ -330,13 +330,16 @@ public class DrawingLibraryFormTests
|
||||
var warnings = new List<string>();
|
||||
var answer = DialogResult.Cancel;
|
||||
|
||||
using var editor = new EditLibraryDrawingForm(new DrawingLibraryEditSession(library, record, "Synthetic operator"));
|
||||
using var editor = new EditLibraryDrawingForm(new DrawingLibraryEditSession(library, record, "Synthetic operator"))
|
||||
{
|
||||
LoadMaterialCatalog = Array.Empty<MaterialConfig>,
|
||||
};
|
||||
editor.ShowMessage = (text, _, _) => { warnings.Add(text); return answer; };
|
||||
editor.Show();
|
||||
Field<TextBox>(editor, "nameBox").Text = "SYN-707";
|
||||
Field<TextBox>(editor, "materialBox").Text = "Synthetic Steel";
|
||||
Field<TextBox>(editor, "gradeBox").Text = "Grade A";
|
||||
Field<NumericUpDown>(editor, "thicknessBox").Value = 6.35m;
|
||||
Field<ComboBox>(editor, "materialBox").Text = "Synthetic Steel";
|
||||
Field<ComboBox>(editor, "gradeBox").Text = "Grade A";
|
||||
Field<OpenNest.Controls.ThicknessComboBox>(editor, "thicknessBox").Value = 6.35;
|
||||
|
||||
// The new details duplicate SYN-707: cancelling the warning saves nothing.
|
||||
Run(editor.SaveAsync());
|
||||
@@ -360,6 +363,136 @@ public class DrawingLibraryFormTests
|
||||
"The edit drawing duplicate warning test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EditDrawing_OffersTheLibrarysAndCatalogsValues_WithoutChangingTheEdit()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new FakeLibrary();
|
||||
var record = library.Add("SYN-801", "Synthetic Steel", "Grade A", 0.25, Units.Inches, "");
|
||||
library.Add("SYN-802", "synthetic steel", "Grade B", 0.1875, Units.Inches, "");
|
||||
library.Add("SYN-803", "Synthetic Brass", "", 3, Units.Millimeters, "");
|
||||
|
||||
using var editor = new EditLibraryDrawingForm(new DrawingLibraryEditSession(library, record, "Synthetic operator"))
|
||||
{
|
||||
LoadMaterialCatalog = () => new[] { new MaterialConfig { Name = "Synthetic Steel", Grade = "Grade C" } },
|
||||
};
|
||||
editor.Show();
|
||||
Run(editor.LoadChoicesAsync());
|
||||
|
||||
Assert.Contains("Synthetic customer", Field<ComboBox>(editor, "customerBox").Items.Cast<string>());
|
||||
var materials = Field<ComboBox>(editor, "materialBox").Items.Cast<string>().ToList();
|
||||
Assert.Contains("Synthetic Brass", materials);
|
||||
Assert.Single(materials, m => SharedListNames.Key(m) == "SYNTHETIC STEEL");
|
||||
Assert.Equal(new[] { "Grade A", "Grade B", "Grade C" }, Field<ComboBox>(editor, "gradeBox").Items.Cast<string>());
|
||||
var thickness = Field<OpenNest.Controls.ThicknessComboBox>(editor, "thicknessBox");
|
||||
Assert.Equal(StandardThicknesses.Format(0.25, Units.Inches), thickness.Items[0]);
|
||||
|
||||
// Without a grade the material's other thicknesses are listed first too; millimeter ones never are.
|
||||
Field<ComboBox>(editor, "gradeBox").Text = "";
|
||||
Assert.Equal(new object[] { StandardThicknesses.Format(0.1875, Units.Inches), StandardThicknesses.Format(0.25, Units.Inches) },
|
||||
thickness.Items.Cast<object>().Take(2));
|
||||
Field<ComboBox>(editor, "gradeBox").Text = "Grade A";
|
||||
Assert.False(Field<Button>(editor, "saveButton").Enabled);
|
||||
Assert.Equal(record.Thickness, editor.ReadDetails().Thickness);
|
||||
},
|
||||
TestTimeout,
|
||||
"The edit drawing choices test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EditDrawing_ColorCanBeMadeAutomatic_AndResetToTheStoredColor()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new FakeLibrary();
|
||||
var record = library.Add("SYN-811", "Synthetic Steel", "Grade A", 0.25, Units.Inches, "");
|
||||
var custom = System.Drawing.Color.FromArgb(255, 40, 50, 60).ToArgb();
|
||||
record.Settings = new DrawingNestingSettings { ColorArgb = custom };
|
||||
|
||||
using var editor = new EditLibraryDrawingForm(new DrawingLibraryEditSession(library, record, "Synthetic operator"))
|
||||
{
|
||||
LoadMaterialCatalog = Array.Empty<MaterialConfig>,
|
||||
};
|
||||
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
|
||||
editor.Show();
|
||||
var save = Field<Button>(editor, "saveButton");
|
||||
var automatic = Field<Button>(editor, "automaticColorButton");
|
||||
var reset = Field<Button>(editor, "resetColorButton");
|
||||
Assert.Equal((true, false), (automatic.Enabled, reset.Enabled));
|
||||
Assert.StartsWith("Custom: #28323C", Field<Label>(editor, "colorModeLabel").Text);
|
||||
|
||||
automatic.PerformClick();
|
||||
Assert.Equal(0, editor.ReadSettings().ColorArgb);
|
||||
Assert.Equal((false, true, true), (automatic.Enabled, reset.Enabled, save.Enabled));
|
||||
Assert.StartsWith("Automatic", Field<Label>(editor, "colorModeLabel").Text);
|
||||
|
||||
reset.PerformClick();
|
||||
Assert.Equal(custom, editor.ReadSettings().ColorArgb);
|
||||
Assert.Equal((true, false, false), (automatic.Enabled, reset.Enabled, save.Enabled));
|
||||
|
||||
automatic.PerformClick();
|
||||
Run(editor.SaveAsync());
|
||||
Assert.Equal(0, Assert.Single(library.MetadataUpdates).Record.Settings.ColorArgb);
|
||||
},
|
||||
TestTimeout,
|
||||
"The edit drawing color test did not complete.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EditDrawing_RotationDialAndAngleBoxesStayInStep()
|
||||
{
|
||||
StaTestThread.Run(
|
||||
() =>
|
||||
{
|
||||
var library = new FakeLibrary();
|
||||
var record = library.Add("SYN-821", "Synthetic Steel", "Grade A", 0.25, Units.Inches, "");
|
||||
record.Settings = new DrawingNestingSettings
|
||||
{
|
||||
StartAngle = 0,
|
||||
EndAngle = System.Math.PI / 2,
|
||||
StepAngle = OpenNest.Math.Angle.ToRadians(15),
|
||||
};
|
||||
|
||||
using var editor = new EditLibraryDrawingForm(new DrawingLibraryEditSession(library, record, "Synthetic operator"))
|
||||
{
|
||||
LoadMaterialCatalog = Array.Empty<MaterialConfig>,
|
||||
};
|
||||
editor.Show();
|
||||
var dial = Field<OpenNest.Controls.RotationRangeDial>(editor, "rotationDial");
|
||||
var summary = Field<Label>(editor, "rotationSummaryLabel");
|
||||
Assert.Equal((0d, 90d, 15d), (dial.StartAngle, dial.EndAngle, dial.StepAngle));
|
||||
Assert.Contains("tries 7 angles", summary.Text);
|
||||
Assert.False(Field<Button>(editor, "saveButton").Enabled);
|
||||
|
||||
Field<NumericUpDown>(editor, "endBox").Value = 120;
|
||||
Assert.Equal(120d, dial.EndAngle);
|
||||
|
||||
dial.ActiveHandle = OpenNest.Controls.DialHandle.Start;
|
||||
dial.MoveActive(15);
|
||||
Assert.Equal(15m, Field<NumericUpDown>(editor, "startBox").Value);
|
||||
Assert.Equal(OpenNest.Math.Angle.ToRadians(15), editor.ReadSettings().StartAngle, 12);
|
||||
Assert.Contains("15° to 120° every 15°: tries 8 angles", summary.Text);
|
||||
|
||||
Field<CheckBox>(editor, "allow180CheckBox").Checked = true;
|
||||
Assert.True(dial.Allow180Equivalent);
|
||||
Assert.Contains("tries 16 angles", summary.Text);
|
||||
Assert.True(Field<Button>(editor, "saveButton").Enabled);
|
||||
},
|
||||
TestTimeout,
|
||||
"The edit drawing rotation dial test did not complete.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0, 0, false, "Automatic")]
|
||||
[InlineData(90, 0, 15, false, "end is before the start")]
|
||||
[InlineData(45, 45, 360, false, "Fixed at 45°: tries 1 angle.")]
|
||||
[InlineData(0, 90, 45, true, "0° to 90° every 45°, and 180° from each: tries 6 angles.")]
|
||||
public void DescribeRotation_SaysWhatTheEnginesTry(double start, double end, double step, bool allow180, string expected) =>
|
||||
Assert.Contains(expected, EditLibraryDrawingForm.DescribeRotation(start, end, step, allow180));
|
||||
|
||||
[Fact]
|
||||
public void WithoutANest_ListsEveryDrawing_HasNoAdd_AndEditsOnDoubleClick()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// A dial for a drawing's rotation range. The shaded wedge runs counterclockwise from the
|
||||
/// start angle to the end angle, with 0° at the right as plate rotations are measured, and
|
||||
/// a tick marks each angle the nesting engines try (<see cref="RotationPolicy.FromLegacy"/>).
|
||||
/// A zero step is the engines' automatic 0°/90°/180°/270°, so the wedge is drawn faded.
|
||||
/// Allowed 180° equivalents show as a hatched wedge opposite the range.
|
||||
/// <para>
|
||||
/// Drag a handle to move that angle in whole degrees (hold Shift for 15°); clicking the dial
|
||||
/// moves the nearer handle there. With focus, the arrow keys move the active handle and
|
||||
/// Space switches handles. Angles stay within 0° <= start <= end <= 360°.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class RotationRangeDial : Control
|
||||
{
|
||||
public const double MaxDegrees = 360;
|
||||
private const double Snap = 1;
|
||||
private const double ShiftSnap = 15;
|
||||
|
||||
private double startAngle;
|
||||
private double endAngle = MaxDegrees;
|
||||
private double stepAngle;
|
||||
private bool allow180Equivalent;
|
||||
private DialHandle active = DialHandle.Start;
|
||||
private DialHandle hover = DialHandle.None;
|
||||
private DialHandle dragging = DialHandle.None;
|
||||
private bool dragUndecided;
|
||||
private double lastPointerAngle;
|
||||
private double dragValue;
|
||||
|
||||
public RotationRangeDial()
|
||||
{
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer
|
||||
| ControlStyles.ResizeRedraw | ControlStyles.UserPaint | ControlStyles.Selectable, true);
|
||||
TabStop = true;
|
||||
}
|
||||
|
||||
/// <summary>Raised when the operator drags a handle or uses the keyboard, not when a property is set.</summary>
|
||||
public event EventHandler AnglesChanged;
|
||||
|
||||
/// <summary>The range start in degrees, 0 to <see cref="EndAngle"/>.</summary>
|
||||
[DefaultValue(0d)]
|
||||
public double StartAngle
|
||||
{
|
||||
get { return startAngle; }
|
||||
set { SetAngles(value, System.Math.Max(value, endAngle)); }
|
||||
}
|
||||
|
||||
/// <summary>The range end in degrees, <see cref="StartAngle"/> to 360.</summary>
|
||||
[DefaultValue(MaxDegrees)]
|
||||
public double EndAngle
|
||||
{
|
||||
get { return endAngle; }
|
||||
set { SetAngles(System.Math.Min(startAngle, value), value); }
|
||||
}
|
||||
|
||||
/// <summary>The step in degrees; zero means the engines' automatic rotations.</summary>
|
||||
[DefaultValue(0d)]
|
||||
public double StepAngle
|
||||
{
|
||||
get { return stepAngle; }
|
||||
set
|
||||
{
|
||||
var step = double.IsFinite(value) && value > 0 ? value : 0;
|
||||
if (step == stepAngle)
|
||||
return;
|
||||
stepAngle = step;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(false)]
|
||||
public bool Allow180Equivalent
|
||||
{
|
||||
get { return allow180Equivalent; }
|
||||
set
|
||||
{
|
||||
if (value == allow180Equivalent)
|
||||
return;
|
||||
allow180Equivalent = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The handle the keyboard moves.</summary>
|
||||
[Browsable(false)]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public DialHandle ActiveHandle
|
||||
{
|
||||
get { return active; }
|
||||
set
|
||||
{
|
||||
active = value == DialHandle.End ? DialHandle.End : DialHandle.Start;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The angles, in degrees, the nesting engines try for the current settings.</summary>
|
||||
public IReadOnlyList<double> TriedAngles() =>
|
||||
TriedAngles(startAngle, endAngle, stepAngle, allow180Equivalent);
|
||||
|
||||
/// <summary>The angles, in degrees in [0, 360), the nesting engines try for these settings.</summary>
|
||||
public static IReadOnlyList<double> TriedAngles(double start, double end, double step, bool allow180)
|
||||
{
|
||||
var angles = new List<double>();
|
||||
if (!double.IsFinite(start) || !double.IsFinite(end) || !double.IsFinite(step) || step < 0
|
||||
|| (step > 0 && end < start))
|
||||
return angles;
|
||||
var policy = RotationPolicy.FromLegacy(Angle.ToRadians(step), Angle.ToRadians(start),
|
||||
Angle.ToRadians(end), allow180);
|
||||
foreach (var radians in policy.EnumerateAngles())
|
||||
// Rounded so whole degrees survive the trip through radians.
|
||||
angles.Add(System.Math.Round(Angle.ToDegrees(radians), 9));
|
||||
return angles;
|
||||
}
|
||||
|
||||
protected override Size DefaultSize => new(150, 150);
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
base.OnPaint(e);
|
||||
var g = e.Graphics;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.Clear(BackColor);
|
||||
|
||||
var center = Center;
|
||||
var radius = Radius;
|
||||
if (radius < 4)
|
||||
return;
|
||||
var dial = new RectangleF(center.X - radius, center.Y - radius, 2 * radius, 2 * radius);
|
||||
var accent = SystemColors.Highlight;
|
||||
var automatic = stepAngle <= 0;
|
||||
|
||||
using (var face = new SolidBrush(SystemColors.Window))
|
||||
g.FillEllipse(face, dial);
|
||||
|
||||
// Quarter guides, with their angles outside the dial.
|
||||
using (var guide = new Pen(Color.FromArgb(60, SystemColors.ControlDark)) { DashStyle = DashStyle.Dot })
|
||||
using (var labelBrush = new SolidBrush(SystemColors.GrayText))
|
||||
using (var labelFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center })
|
||||
{
|
||||
for (var quarter = 0; quarter < 4; quarter++)
|
||||
{
|
||||
var angle = quarter * 90.0;
|
||||
g.DrawLine(guide, center, PointAt(angle, radius));
|
||||
g.DrawString($"{angle:0}°", Font, labelBrush, PointAt(angle, radius + LabelGap), labelFormat);
|
||||
}
|
||||
}
|
||||
|
||||
if (allow180Equivalent && !automatic)
|
||||
{
|
||||
using var hatch = new HatchBrush(HatchStyle.WideUpwardDiagonal, Color.FromArgb(110, accent), Color.Transparent);
|
||||
FillWedge(g, hatch, dial, startAngle + 180, endAngle - startAngle);
|
||||
}
|
||||
|
||||
using (var wedge = new SolidBrush(Color.FromArgb(automatic ? 35 : 90, accent)))
|
||||
FillWedge(g, wedge, dial, startAngle, endAngle - startAngle);
|
||||
|
||||
using (var edge = new Pen(Color.FromArgb(automatic ? 110 : 255, accent), 1.5f))
|
||||
{
|
||||
if (automatic)
|
||||
edge.DashStyle = DashStyle.Dash;
|
||||
g.DrawLine(edge, center, PointAt(startAngle, radius));
|
||||
g.DrawLine(edge, center, PointAt(endAngle, radius));
|
||||
}
|
||||
|
||||
using (var rim = new Pen(SystemColors.ControlDark))
|
||||
g.DrawEllipse(rim, dial);
|
||||
|
||||
using (var tick = new Pen(SystemColors.ControlText, 1.5f))
|
||||
{
|
||||
foreach (var angle in TriedAngles())
|
||||
g.DrawLine(tick, PointAt(angle, radius * 0.80f), PointAt(angle, radius));
|
||||
}
|
||||
|
||||
DrawHandle(g, DialHandle.Start, startAngle, accent);
|
||||
DrawHandle(g, DialHandle.End, endAngle, accent);
|
||||
|
||||
if (Focused && ShowFocusCues)
|
||||
{
|
||||
var focus = Rectangle.Round(dial);
|
||||
focus.Inflate(2, 2);
|
||||
ControlPaint.DrawFocusRectangle(g, focus);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHandle(Graphics g, DialHandle handle, double angle, Color accent)
|
||||
{
|
||||
var size = HandleRadius * (handle == hover || handle == dragging ? 1.25f : 1f);
|
||||
var point = PointAt(angle, Radius);
|
||||
var bounds = new RectangleF(point.X - size, point.Y - size, 2 * size, 2 * size);
|
||||
// The start handle is hollow and the end handle solid, so they read apart when close.
|
||||
using (var fill = new SolidBrush(handle == DialHandle.End ? accent : SystemColors.Window))
|
||||
g.FillEllipse(fill, bounds);
|
||||
using (var ring = new Pen(handle == active && Focused ? SystemColors.ControlText : accent, 2f))
|
||||
g.DrawEllipse(ring, bounds);
|
||||
}
|
||||
|
||||
private static void FillWedge(Graphics g, Brush brush, RectangleF dial, double from, double extent)
|
||||
{
|
||||
if (extent >= MaxDegrees - 1e-9)
|
||||
g.FillEllipse(brush, dial);
|
||||
else if (extent > 0)
|
||||
// GDI+ measures angles clockwise; a negative sweep runs counterclockwise.
|
||||
g.FillPie(brush, dial.X, dial.Y, dial.Width, dial.Height, (float)-from, (float)-extent);
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseDown(e);
|
||||
if (e.Button != MouseButtons.Left)
|
||||
return;
|
||||
Focus();
|
||||
|
||||
var pointer = AngleAt(e.Location);
|
||||
var hit = HitHandles(e.Location);
|
||||
if (hit == DialHandle.None)
|
||||
{
|
||||
// Away from the handles, jump the nearer one to the pointer and keep dragging it;
|
||||
// a tie (a full turn puts both at 0°) goes to the end.
|
||||
hit = AngularDistance(pointer, startAngle) < AngularDistance(pointer, endAngle)
|
||||
? DialHandle.Start
|
||||
: DialHandle.End;
|
||||
var target = NearestWithin(pointer, Limits(hit));
|
||||
BeginDrag(hit, pointer, target);
|
||||
Drag(target, ModifierKeys.HasFlag(Keys.Shift));
|
||||
return;
|
||||
}
|
||||
|
||||
// Overlapping handles are told apart by the direction of the first move.
|
||||
dragUndecided = hit == DialHandle.Both;
|
||||
BeginDrag(hit == DialHandle.Both ? DialHandle.End : hit, pointer,
|
||||
hit == DialHandle.Start ? startAngle : endAngle);
|
||||
}
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseMove(e);
|
||||
if (dragging == DialHandle.None)
|
||||
{
|
||||
var hit = HitHandles(e.Location);
|
||||
var over = hit == DialHandle.Both ? DialHandle.End : hit;
|
||||
Cursor = hit == DialHandle.None ? Cursors.Default : Cursors.Hand;
|
||||
if (over != hover)
|
||||
{
|
||||
hover = over;
|
||||
Invalidate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var pointer = AngleAt(e.Location);
|
||||
var delta = pointer - lastPointerAngle;
|
||||
delta -= MaxDegrees * System.Math.Round(delta / MaxDegrees);
|
||||
lastPointerAngle = pointer;
|
||||
if (delta == 0)
|
||||
return;
|
||||
|
||||
if (dragUndecided)
|
||||
{
|
||||
dragUndecided = false;
|
||||
dragging = delta < 0 ? DialHandle.Start : DialHandle.End;
|
||||
active = dragging;
|
||||
dragValue = dragging == DialHandle.Start ? startAngle : endAngle;
|
||||
}
|
||||
|
||||
// The drag follows the pointer continuously, so the end can sweep past 0° up to a full turn.
|
||||
Drag(dragValue + delta, ModifierKeys.HasFlag(Keys.Shift));
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
if (dragging == DialHandle.None)
|
||||
return;
|
||||
dragging = DialHandle.None;
|
||||
dragUndecided = false;
|
||||
Capture = false;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override void OnMouseLeave(EventArgs e)
|
||||
{
|
||||
base.OnMouseLeave(e);
|
||||
if (hover == DialHandle.None)
|
||||
return;
|
||||
hover = DialHandle.None;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override bool IsInputKey(Keys keyData) =>
|
||||
(keyData & Keys.KeyCode) is Keys.Left or Keys.Right or Keys.Up or Keys.Down || base.IsInputKey(keyData);
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
var amount = e.Shift ? ShiftSnap : Snap;
|
||||
switch (e.KeyCode)
|
||||
{
|
||||
case Keys.Right:
|
||||
case Keys.Up:
|
||||
MoveActive(amount);
|
||||
break;
|
||||
case Keys.Left:
|
||||
case Keys.Down:
|
||||
MoveActive(-amount);
|
||||
break;
|
||||
case Keys.Space:
|
||||
ActiveHandle = active == DialHandle.Start ? DialHandle.End : DialHandle.Start;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnGotFocus(EventArgs e)
|
||||
{
|
||||
base.OnGotFocus(e);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override void OnLostFocus(EventArgs e)
|
||||
{
|
||||
base.OnLostFocus(e);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>Moves the active handle by <paramref name="degrees"/>, to the next multiple of the move.</summary>
|
||||
internal void MoveActive(double degrees)
|
||||
{
|
||||
var current = active == DialHandle.Start ? startAngle : endAngle;
|
||||
var amount = System.Math.Abs(degrees);
|
||||
var next = degrees > 0
|
||||
? System.Math.Floor(current / amount + 1e-9) * amount + amount
|
||||
: System.Math.Ceiling(current / amount - 1e-9) * amount - amount;
|
||||
SetActive(next);
|
||||
}
|
||||
|
||||
private void BeginDrag(DialHandle handle, double pointer, double value)
|
||||
{
|
||||
dragging = handle;
|
||||
active = handle;
|
||||
lastPointerAngle = pointer;
|
||||
dragValue = value;
|
||||
Capture = true;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void Drag(double value, bool coarse)
|
||||
{
|
||||
var (low, high) = Limits(dragging);
|
||||
dragValue = System.Math.Clamp(value, low, high);
|
||||
var snap = coarse ? ShiftSnap : Snap;
|
||||
SetActive(System.Math.Round(dragValue / snap) * snap);
|
||||
}
|
||||
|
||||
// Keeps the handles ordered: each moves between 0°, the other handle and 360°.
|
||||
private (double Low, double High) Limits(DialHandle handle) =>
|
||||
handle == DialHandle.Start ? (0, endAngle) : (startAngle, MaxDegrees);
|
||||
|
||||
private void SetActive(double value)
|
||||
{
|
||||
var (low, high) = Limits(active);
|
||||
value = System.Math.Clamp(value, low, high);
|
||||
var changed = active == DialHandle.Start
|
||||
? SetAngles(value, endAngle)
|
||||
: SetAngles(startAngle, value);
|
||||
if (changed)
|
||||
AnglesChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private bool SetAngles(double start, double end)
|
||||
{
|
||||
start = double.IsFinite(start) ? System.Math.Clamp(start, 0, MaxDegrees) : 0;
|
||||
end = double.IsFinite(end) ? System.Math.Clamp(end, start, MaxDegrees) : start;
|
||||
if (start == startAngle && end == endAngle)
|
||||
return false;
|
||||
startAngle = start;
|
||||
endAngle = end;
|
||||
Invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
private DialHandle HitHandles(Point location)
|
||||
{
|
||||
var reach = HandleRadius + 3;
|
||||
var onStart = Distance(location, PointAt(startAngle, Radius)) <= reach;
|
||||
var onEnd = Distance(location, PointAt(endAngle, Radius)) <= reach;
|
||||
if (onStart && onEnd)
|
||||
return DialHandle.Both;
|
||||
return onStart ? DialHandle.Start : onEnd ? DialHandle.End : DialHandle.None;
|
||||
}
|
||||
|
||||
private PointF Center => new(Width / 2f, Height / 2f);
|
||||
|
||||
// Room outside the rim for the quarter labels.
|
||||
private float LabelGap => Font.Height * 0.95f;
|
||||
|
||||
private float Radius => System.Math.Min(Width, Height) / 2f - LabelGap - Font.Height * 0.6f;
|
||||
|
||||
private float HandleRadius => System.Math.Max(5f, Radius * 0.09f);
|
||||
|
||||
private PointF PointAt(double degrees, float radius)
|
||||
{
|
||||
var radians = Angle.ToRadians(degrees);
|
||||
var center = Center;
|
||||
return new PointF(center.X + radius * (float)System.Math.Cos(radians),
|
||||
center.Y - radius * (float)System.Math.Sin(radians));
|
||||
}
|
||||
|
||||
private double AngleAt(Point location)
|
||||
{
|
||||
var center = Center;
|
||||
var degrees = Angle.ToDegrees(System.Math.Atan2(center.Y - location.Y, location.X - center.X));
|
||||
return degrees < 0 ? degrees + MaxDegrees : degrees;
|
||||
}
|
||||
|
||||
// The turn of a pointer angle that falls within the limits, else the limit around the dial nearer to it.
|
||||
private static double NearestWithin(double angle, (double Low, double High) limits)
|
||||
{
|
||||
foreach (var candidate in new[] { angle, angle + MaxDegrees })
|
||||
{
|
||||
if (candidate >= limits.Low && candidate <= limits.High)
|
||||
return candidate;
|
||||
}
|
||||
return AngularDistance(angle, limits.Low) <= AngularDistance(angle, limits.High) ? limits.Low : limits.High;
|
||||
}
|
||||
|
||||
private static double AngularDistance(double a, double b)
|
||||
{
|
||||
var delta = System.Math.Abs(a - b) % MaxDegrees;
|
||||
return System.Math.Min(delta, MaxDegrees - delta);
|
||||
}
|
||||
|
||||
private static float Distance(PointF a, PointF b) =>
|
||||
(float)System.Math.Sqrt((a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y));
|
||||
}
|
||||
|
||||
public enum DialHandle
|
||||
{
|
||||
None,
|
||||
Start,
|
||||
End,
|
||||
Both,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Data;
|
||||
|
||||
@@ -13,6 +15,7 @@ namespace OpenNest.Controls
|
||||
public class ThicknessComboBox : ComboBox
|
||||
{
|
||||
private Units units = Units.Inches;
|
||||
private double[] usedValues = Array.Empty<double>();
|
||||
|
||||
public ThicknessComboBox()
|
||||
{
|
||||
@@ -55,6 +58,19 @@ namespace OpenNest.Controls
|
||||
|
||||
public bool TryGetValue(out double value) => StandardThicknesses.TryParse(Text, units, out value);
|
||||
|
||||
/// <summary>
|
||||
/// Lists <paramref name="values"/> (in <see cref="Units"/>, for example the thicknesses
|
||||
/// already used for a material) ahead of the standard entries; the text is kept.
|
||||
/// </summary>
|
||||
public void SetUsedValues(IEnumerable<double> values)
|
||||
{
|
||||
var used = (values ?? Array.Empty<double>()).Where(v => double.IsFinite(v) && v > 0).ToArray();
|
||||
if (used.SequenceEqual(usedValues))
|
||||
return;
|
||||
usedValues = used;
|
||||
FillItems();
|
||||
}
|
||||
|
||||
protected override void OnEnter(EventArgs e)
|
||||
{
|
||||
base.OnEnter(e);
|
||||
@@ -72,10 +88,20 @@ namespace OpenNest.Controls
|
||||
private void FillItems()
|
||||
{
|
||||
var text = Text;
|
||||
BeginUpdate();
|
||||
Items.Clear();
|
||||
var used = usedValues.Select(v => StandardThicknesses.Format(v, units)).Distinct().ToList();
|
||||
foreach (var label in used)
|
||||
Items.Add(label);
|
||||
foreach (var entry in StandardThicknesses.Entries)
|
||||
Items.Add(StandardThicknesses.Label(entry, units));
|
||||
Text = text;
|
||||
{
|
||||
var label = StandardThicknesses.Label(entry, units);
|
||||
if (!used.Contains(label))
|
||||
Items.Add(label);
|
||||
}
|
||||
EndUpdate();
|
||||
if (Text != text)
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
+355
-159
@@ -17,102 +17,87 @@ namespace OpenNest.Forms
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
layoutTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
detailsGroup = new System.Windows.Forms.GroupBox();
|
||||
detailsTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
nameLabel = new System.Windows.Forms.Label();
|
||||
nameBox = new System.Windows.Forms.TextBox();
|
||||
customerLabel = new System.Windows.Forms.Label();
|
||||
customerBox = new System.Windows.Forms.TextBox();
|
||||
customerBox = new System.Windows.Forms.ComboBox();
|
||||
materialLabel = new System.Windows.Forms.Label();
|
||||
materialBox = new System.Windows.Forms.TextBox();
|
||||
materialBox = new System.Windows.Forms.ComboBox();
|
||||
gradeLabel = new System.Windows.Forms.Label();
|
||||
gradeBox = new System.Windows.Forms.TextBox();
|
||||
gradeBox = new System.Windows.Forms.ComboBox();
|
||||
thicknessLabel = new System.Windows.Forms.Label();
|
||||
thicknessPanel = new System.Windows.Forms.FlowLayoutPanel();
|
||||
thicknessBox = new System.Windows.Forms.NumericUpDown();
|
||||
thicknessBox = new OpenNest.Controls.ThicknessComboBox();
|
||||
unitsLabel = new System.Windows.Forms.Label();
|
||||
cutTypeLabel = new System.Windows.Forms.Label();
|
||||
cutTypeBox = new System.Windows.Forms.ComboBox();
|
||||
settingsLabel = new System.Windows.Forms.Label();
|
||||
nestingGroup = new System.Windows.Forms.GroupBox();
|
||||
nestingTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
cutFirstCheckBox = new System.Windows.Forms.CheckBox();
|
||||
priorityLabel = new System.Windows.Forms.Label();
|
||||
priorityBox = new System.Windows.Forms.NumericUpDown();
|
||||
stepLabel = new System.Windows.Forms.Label();
|
||||
stepBox = new System.Windows.Forms.NumericUpDown();
|
||||
colorLabel = new System.Windows.Forms.Label();
|
||||
colorPanel = new System.Windows.Forms.FlowLayoutPanel();
|
||||
colorButton = new System.Windows.Forms.Button();
|
||||
automaticColorButton = new System.Windows.Forms.Button();
|
||||
resetColorButton = new System.Windows.Forms.Button();
|
||||
colorModeLabel = new System.Windows.Forms.Label();
|
||||
rotationGroup = new System.Windows.Forms.GroupBox();
|
||||
rotationTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
rotationDial = new OpenNest.Controls.RotationRangeDial();
|
||||
angleTable = new System.Windows.Forms.TableLayoutPanel();
|
||||
startLabel = new System.Windows.Forms.Label();
|
||||
startBox = new System.Windows.Forms.NumericUpDown();
|
||||
endLabel = new System.Windows.Forms.Label();
|
||||
endBox = new System.Windows.Forms.NumericUpDown();
|
||||
stepLabel = new System.Windows.Forms.Label();
|
||||
stepBox = new System.Windows.Forms.NumericUpDown();
|
||||
allow180CheckBox = new System.Windows.Forms.CheckBox();
|
||||
colorLabel = new System.Windows.Forms.Label();
|
||||
colorButton = new System.Windows.Forms.Button();
|
||||
rotationSummaryLabel = new System.Windows.Forms.Label();
|
||||
notesLabel = new System.Windows.Forms.Label();
|
||||
notesBox = new System.Windows.Forms.TextBox();
|
||||
statusLabel = new System.Windows.Forms.Label();
|
||||
buttonPanel = new System.Windows.Forms.FlowLayoutPanel();
|
||||
cancelButton = new System.Windows.Forms.Button();
|
||||
saveButton = new System.Windows.Forms.Button();
|
||||
toolTip = new System.Windows.Forms.ToolTip(components);
|
||||
layoutTable.SuspendLayout();
|
||||
detailsGroup.SuspendLayout();
|
||||
detailsTable.SuspendLayout();
|
||||
thicknessPanel.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)thicknessBox).BeginInit();
|
||||
nestingGroup.SuspendLayout();
|
||||
nestingTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)priorityBox).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)stepBox).BeginInit();
|
||||
colorPanel.SuspendLayout();
|
||||
rotationGroup.SuspendLayout();
|
||||
rotationTable.SuspendLayout();
|
||||
angleTable.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)startBox).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)endBox).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)stepBox).BeginInit();
|
||||
buttonPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// layoutTable
|
||||
//
|
||||
layoutTable.ColumnCount = 2;
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
layoutTable.ColumnCount = 1;
|
||||
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.Controls.Add(nameLabel, 0, 0);
|
||||
layoutTable.Controls.Add(nameBox, 1, 0);
|
||||
layoutTable.Controls.Add(customerLabel, 0, 1);
|
||||
layoutTable.Controls.Add(customerBox, 1, 1);
|
||||
layoutTable.Controls.Add(materialLabel, 0, 2);
|
||||
layoutTable.Controls.Add(materialBox, 1, 2);
|
||||
layoutTable.Controls.Add(gradeLabel, 0, 3);
|
||||
layoutTable.Controls.Add(gradeBox, 1, 3);
|
||||
layoutTable.Controls.Add(thicknessLabel, 0, 4);
|
||||
layoutTable.Controls.Add(thicknessPanel, 1, 4);
|
||||
layoutTable.Controls.Add(cutTypeLabel, 0, 5);
|
||||
layoutTable.Controls.Add(cutTypeBox, 1, 5);
|
||||
layoutTable.Controls.Add(settingsLabel, 0, 6);
|
||||
layoutTable.Controls.Add(cutFirstCheckBox, 1, 7);
|
||||
layoutTable.Controls.Add(priorityLabel, 0, 8);
|
||||
layoutTable.Controls.Add(priorityBox, 1, 8);
|
||||
layoutTable.Controls.Add(stepLabel, 0, 9);
|
||||
layoutTable.Controls.Add(stepBox, 1, 9);
|
||||
layoutTable.Controls.Add(startLabel, 0, 10);
|
||||
layoutTable.Controls.Add(startBox, 1, 10);
|
||||
layoutTable.Controls.Add(endLabel, 0, 11);
|
||||
layoutTable.Controls.Add(endBox, 1, 11);
|
||||
layoutTable.Controls.Add(allow180CheckBox, 1, 12);
|
||||
layoutTable.Controls.Add(colorLabel, 0, 13);
|
||||
layoutTable.Controls.Add(colorButton, 1, 13);
|
||||
layoutTable.Controls.Add(notesLabel, 0, 14);
|
||||
layoutTable.Controls.Add(notesBox, 1, 14);
|
||||
layoutTable.Controls.Add(statusLabel, 0, 15);
|
||||
layoutTable.Controls.Add(buttonPanel, 0, 16);
|
||||
layoutTable.SetColumnSpan(settingsLabel, 2);
|
||||
layoutTable.SetColumnSpan(statusLabel, 2);
|
||||
layoutTable.SetColumnSpan(buttonPanel, 2);
|
||||
layoutTable.Controls.Add(detailsGroup, 0, 0);
|
||||
layoutTable.Controls.Add(nestingGroup, 0, 1);
|
||||
layoutTable.Controls.Add(rotationGroup, 0, 2);
|
||||
layoutTable.Controls.Add(notesLabel, 0, 3);
|
||||
layoutTable.Controls.Add(notesBox, 0, 4);
|
||||
layoutTable.Controls.Add(statusLabel, 0, 5);
|
||||
layoutTable.Controls.Add(buttonPanel, 0, 6);
|
||||
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
layoutTable.Location = new System.Drawing.Point(0, 0);
|
||||
layoutTable.Name = "layoutTable";
|
||||
layoutTable.Padding = new System.Windows.Forms.Padding(8);
|
||||
layoutTable.RowCount = 17;
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.Padding = new System.Windows.Forms.Padding(10);
|
||||
layoutTable.RowCount = 7;
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
@@ -120,9 +105,49 @@ namespace OpenNest.Forms
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
layoutTable.Size = new System.Drawing.Size(464, 641);
|
||||
layoutTable.Size = new System.Drawing.Size(540, 800);
|
||||
layoutTable.TabIndex = 0;
|
||||
//
|
||||
// detailsGroup
|
||||
//
|
||||
detailsGroup.AutoSize = true;
|
||||
detailsGroup.Controls.Add(detailsTable);
|
||||
detailsGroup.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
detailsGroup.Name = "detailsGroup";
|
||||
detailsGroup.Padding = new System.Windows.Forms.Padding(8, 4, 8, 8);
|
||||
detailsGroup.TabIndex = 0;
|
||||
detailsGroup.TabStop = false;
|
||||
detailsGroup.Text = "Drawing";
|
||||
//
|
||||
// detailsTable
|
||||
//
|
||||
detailsTable.AutoSize = true;
|
||||
detailsTable.ColumnCount = 2;
|
||||
detailsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 90F));
|
||||
detailsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
detailsTable.Controls.Add(nameLabel, 0, 0);
|
||||
detailsTable.Controls.Add(nameBox, 1, 0);
|
||||
detailsTable.Controls.Add(customerLabel, 0, 1);
|
||||
detailsTable.Controls.Add(customerBox, 1, 1);
|
||||
detailsTable.Controls.Add(materialLabel, 0, 2);
|
||||
detailsTable.Controls.Add(materialBox, 1, 2);
|
||||
detailsTable.Controls.Add(gradeLabel, 0, 3);
|
||||
detailsTable.Controls.Add(gradeBox, 1, 3);
|
||||
detailsTable.Controls.Add(thicknessLabel, 0, 4);
|
||||
detailsTable.Controls.Add(thicknessPanel, 1, 4);
|
||||
detailsTable.Controls.Add(cutTypeLabel, 0, 5);
|
||||
detailsTable.Controls.Add(cutTypeBox, 1, 5);
|
||||
detailsTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
detailsTable.Name = "detailsTable";
|
||||
detailsTable.RowCount = 6;
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
detailsTable.TabIndex = 0;
|
||||
//
|
||||
// nameLabel
|
||||
//
|
||||
nameLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
@@ -147,7 +172,10 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// customerBox
|
||||
//
|
||||
customerBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend;
|
||||
customerBox.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.ListItems;
|
||||
customerBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
customerBox.MaxDropDownItems = 16;
|
||||
customerBox.MaxLength = 200;
|
||||
customerBox.Name = "customerBox";
|
||||
customerBox.TabIndex = 1;
|
||||
@@ -162,11 +190,14 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// materialBox
|
||||
//
|
||||
materialBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend;
|
||||
materialBox.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.ListItems;
|
||||
materialBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
materialBox.MaxDropDownItems = 16;
|
||||
materialBox.MaxLength = 200;
|
||||
materialBox.Name = "materialBox";
|
||||
materialBox.TabIndex = 2;
|
||||
materialBox.TextChanged += Value_Changed;
|
||||
materialBox.TextChanged += Material_Changed;
|
||||
//
|
||||
// gradeLabel
|
||||
//
|
||||
@@ -177,11 +208,14 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// gradeBox
|
||||
//
|
||||
gradeBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend;
|
||||
gradeBox.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.ListItems;
|
||||
gradeBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
gradeBox.MaxDropDownItems = 16;
|
||||
gradeBox.MaxLength = 200;
|
||||
gradeBox.Name = "gradeBox";
|
||||
gradeBox.TabIndex = 3;
|
||||
gradeBox.TextChanged += Value_Changed;
|
||||
gradeBox.TextChanged += Grade_Changed;
|
||||
//
|
||||
// thicknessLabel
|
||||
//
|
||||
@@ -202,14 +236,10 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// thicknessBox
|
||||
//
|
||||
thicknessBox.DecimalPlaces = 4;
|
||||
thicknessBox.Increment = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
thicknessBox.Maximum = new decimal(new int[] { 100000, 0, 0, 0 });
|
||||
thicknessBox.Name = "thicknessBox";
|
||||
thicknessBox.Size = new System.Drawing.Size(100, 25);
|
||||
thicknessBox.Size = new System.Drawing.Size(200, 25);
|
||||
thicknessBox.TabIndex = 4;
|
||||
thicknessBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
thicknessBox.ValueChanged += Value_Changed;
|
||||
thicknessBox.TextChanged += Value_Changed;
|
||||
//
|
||||
// unitsLabel
|
||||
//
|
||||
@@ -230,23 +260,48 @@ namespace OpenNest.Forms
|
||||
cutTypeBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
cutTypeBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
cutTypeBox.Name = "cutTypeBox";
|
||||
cutTypeBox.Size = new System.Drawing.Size(120, 25);
|
||||
cutTypeBox.Size = new System.Drawing.Size(200, 25);
|
||||
cutTypeBox.TabIndex = 5;
|
||||
cutTypeBox.SelectedIndexChanged += Value_Changed;
|
||||
//
|
||||
// settingsLabel
|
||||
// nestingGroup
|
||||
//
|
||||
settingsLabel.AutoSize = true;
|
||||
settingsLabel.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
|
||||
settingsLabel.Margin = new System.Windows.Forms.Padding(3, 12, 3, 3);
|
||||
settingsLabel.Name = "settingsLabel";
|
||||
settingsLabel.Text = "Nesting settings";
|
||||
nestingGroup.AutoSize = true;
|
||||
nestingGroup.Controls.Add(nestingTable);
|
||||
nestingGroup.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
nestingGroup.Margin = new System.Windows.Forms.Padding(3, 8, 3, 3);
|
||||
nestingGroup.Name = "nestingGroup";
|
||||
nestingGroup.Padding = new System.Windows.Forms.Padding(8, 4, 8, 8);
|
||||
nestingGroup.TabIndex = 1;
|
||||
nestingGroup.TabStop = false;
|
||||
nestingGroup.Text = "Nesting";
|
||||
//
|
||||
// nestingTable
|
||||
//
|
||||
nestingTable.AutoSize = true;
|
||||
nestingTable.ColumnCount = 2;
|
||||
nestingTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 90F));
|
||||
nestingTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
nestingTable.Controls.Add(cutFirstCheckBox, 1, 0);
|
||||
nestingTable.Controls.Add(priorityLabel, 0, 1);
|
||||
nestingTable.Controls.Add(priorityBox, 1, 1);
|
||||
nestingTable.Controls.Add(colorLabel, 0, 2);
|
||||
nestingTable.Controls.Add(colorPanel, 1, 2);
|
||||
nestingTable.Controls.Add(colorModeLabel, 1, 3);
|
||||
nestingTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
nestingTable.Name = "nestingTable";
|
||||
nestingTable.RowCount = 4;
|
||||
nestingTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
nestingTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
nestingTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
nestingTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
nestingTable.TabIndex = 0;
|
||||
//
|
||||
// cutFirstCheckBox
|
||||
//
|
||||
cutFirstCheckBox.AutoSize = true;
|
||||
cutFirstCheckBox.Name = "cutFirstCheckBox";
|
||||
cutFirstCheckBox.TabIndex = 6;
|
||||
cutFirstCheckBox.TabIndex = 0;
|
||||
cutFirstCheckBox.Text = "Cut first (sequence these parts before others)";
|
||||
cutFirstCheckBox.UseVisualStyleBackColor = true;
|
||||
cutFirstCheckBox.CheckedChanged += Value_Changed;
|
||||
@@ -265,73 +320,10 @@ namespace OpenNest.Forms
|
||||
priorityBox.Minimum = int.MinValue;
|
||||
priorityBox.Name = "priorityBox";
|
||||
priorityBox.Size = new System.Drawing.Size(100, 25);
|
||||
priorityBox.TabIndex = 7;
|
||||
priorityBox.TabIndex = 1;
|
||||
priorityBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
priorityBox.ValueChanged += Value_Changed;
|
||||
//
|
||||
// stepLabel
|
||||
//
|
||||
stepLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
stepLabel.AutoSize = true;
|
||||
stepLabel.Name = "stepLabel";
|
||||
stepLabel.Text = "Rotation step (°):";
|
||||
//
|
||||
// stepBox
|
||||
//
|
||||
stepBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
stepBox.DecimalPlaces = 2;
|
||||
stepBox.Maximum = new decimal(new int[] { 360, 0, 0, 0 });
|
||||
stepBox.Name = "stepBox";
|
||||
stepBox.Size = new System.Drawing.Size(100, 25);
|
||||
stepBox.TabIndex = 8;
|
||||
stepBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
stepBox.ValueChanged += Value_Changed;
|
||||
//
|
||||
// startLabel
|
||||
//
|
||||
startLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
startLabel.AutoSize = true;
|
||||
startLabel.Name = "startLabel";
|
||||
startLabel.Text = "Rotation start (°):";
|
||||
//
|
||||
// startBox
|
||||
//
|
||||
startBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
startBox.DecimalPlaces = 2;
|
||||
startBox.Maximum = new decimal(new int[] { 360, 0, 0, 0 });
|
||||
startBox.Name = "startBox";
|
||||
startBox.Size = new System.Drawing.Size(100, 25);
|
||||
startBox.TabIndex = 9;
|
||||
startBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
startBox.ValueChanged += Value_Changed;
|
||||
//
|
||||
// endLabel
|
||||
//
|
||||
endLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
endLabel.AutoSize = true;
|
||||
endLabel.Name = "endLabel";
|
||||
endLabel.Text = "Rotation end (°):";
|
||||
//
|
||||
// endBox
|
||||
//
|
||||
endBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
endBox.DecimalPlaces = 2;
|
||||
endBox.Maximum = new decimal(new int[] { 360, 0, 0, 0 });
|
||||
endBox.Name = "endBox";
|
||||
endBox.Size = new System.Drawing.Size(100, 25);
|
||||
endBox.TabIndex = 10;
|
||||
endBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
endBox.ValueChanged += Value_Changed;
|
||||
//
|
||||
// allow180CheckBox
|
||||
//
|
||||
allow180CheckBox.AutoSize = true;
|
||||
allow180CheckBox.Name = "allow180CheckBox";
|
||||
allow180CheckBox.TabIndex = 11;
|
||||
allow180CheckBox.Text = "Allow 180° equivalent";
|
||||
allow180CheckBox.UseVisualStyleBackColor = true;
|
||||
allow180CheckBox.CheckedChanged += Value_Changed;
|
||||
//
|
||||
// colorLabel
|
||||
//
|
||||
colorLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
@@ -339,19 +331,193 @@ namespace OpenNest.Forms
|
||||
colorLabel.Name = "colorLabel";
|
||||
colorLabel.Text = "Color:";
|
||||
//
|
||||
// colorPanel
|
||||
//
|
||||
colorPanel.AutoSize = true;
|
||||
colorPanel.Controls.Add(colorButton);
|
||||
colorPanel.Controls.Add(automaticColorButton);
|
||||
colorPanel.Controls.Add(resetColorButton);
|
||||
colorPanel.Margin = new System.Windows.Forms.Padding(0);
|
||||
colorPanel.Name = "colorPanel";
|
||||
colorPanel.TabIndex = 2;
|
||||
colorPanel.WrapContents = false;
|
||||
//
|
||||
// colorButton
|
||||
//
|
||||
colorButton.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
colorButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
colorButton.Name = "colorButton";
|
||||
colorButton.Size = new System.Drawing.Size(60, 25);
|
||||
colorButton.TabIndex = 12;
|
||||
colorButton.Size = new System.Drawing.Size(72, 27);
|
||||
colorButton.TabIndex = 0;
|
||||
toolTip.SetToolTip(colorButton, "Choose a custom color");
|
||||
colorButton.Click += ColorButton_Click;
|
||||
colorButton.Paint += ColorButton_Paint;
|
||||
//
|
||||
// automaticColorButton
|
||||
//
|
||||
automaticColorButton.AutoSize = true;
|
||||
automaticColorButton.Name = "automaticColorButton";
|
||||
automaticColorButton.TabIndex = 1;
|
||||
automaticColorButton.Text = "Automatic";
|
||||
toolTip.SetToolTip(automaticColorButton, "Use the color scheme's part color, which follows the scheme (Workshop, Dark, ...) in each nest");
|
||||
automaticColorButton.UseVisualStyleBackColor = true;
|
||||
automaticColorButton.Click += AutomaticColorButton_Click;
|
||||
//
|
||||
// resetColorButton
|
||||
//
|
||||
resetColorButton.AutoSize = true;
|
||||
resetColorButton.Name = "resetColorButton";
|
||||
resetColorButton.TabIndex = 2;
|
||||
resetColorButton.Text = "Reset";
|
||||
toolTip.SetToolTip(resetColorButton, "Return to the color this library revision has");
|
||||
resetColorButton.UseVisualStyleBackColor = true;
|
||||
resetColorButton.Click += ResetColorButton_Click;
|
||||
//
|
||||
// colorModeLabel
|
||||
//
|
||||
colorModeLabel.AutoSize = true;
|
||||
colorModeLabel.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
colorModeLabel.Margin = new System.Windows.Forms.Padding(3, 0, 3, 3);
|
||||
colorModeLabel.Name = "colorModeLabel";
|
||||
colorModeLabel.Text = "Automatic";
|
||||
//
|
||||
// rotationGroup
|
||||
//
|
||||
rotationGroup.AutoSize = true;
|
||||
rotationGroup.Controls.Add(rotationTable);
|
||||
rotationGroup.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
rotationGroup.Margin = new System.Windows.Forms.Padding(3, 8, 3, 3);
|
||||
rotationGroup.Name = "rotationGroup";
|
||||
rotationGroup.Padding = new System.Windows.Forms.Padding(8, 4, 8, 8);
|
||||
rotationGroup.TabIndex = 2;
|
||||
rotationGroup.TabStop = false;
|
||||
rotationGroup.Text = "Rotation";
|
||||
//
|
||||
// rotationTable
|
||||
//
|
||||
rotationTable.AutoSize = true;
|
||||
rotationTable.ColumnCount = 2;
|
||||
rotationTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
rotationTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
rotationTable.Controls.Add(rotationDial, 0, 0);
|
||||
rotationTable.Controls.Add(angleTable, 1, 0);
|
||||
rotationTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
rotationTable.Name = "rotationTable";
|
||||
rotationTable.RowCount = 1;
|
||||
rotationTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
rotationTable.TabIndex = 0;
|
||||
//
|
||||
// rotationDial
|
||||
//
|
||||
rotationDial.Margin = new System.Windows.Forms.Padding(0, 3, 12, 3);
|
||||
rotationDial.Name = "rotationDial";
|
||||
rotationDial.Size = new System.Drawing.Size(180, 180);
|
||||
rotationDial.TabIndex = 0;
|
||||
toolTip.SetToolTip(rotationDial, "Drag the handles to set the start (hollow) and end (solid) angles; hold Shift to snap to 15°.\r\nWith focus, the arrow keys move a handle and Space switches handles.");
|
||||
rotationDial.AnglesChanged += RotationDial_AnglesChanged;
|
||||
//
|
||||
// angleTable
|
||||
//
|
||||
angleTable.AutoSize = true;
|
||||
angleTable.ColumnCount = 2;
|
||||
angleTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
|
||||
angleTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
angleTable.Controls.Add(startLabel, 0, 0);
|
||||
angleTable.Controls.Add(startBox, 1, 0);
|
||||
angleTable.Controls.Add(endLabel, 0, 1);
|
||||
angleTable.Controls.Add(endBox, 1, 1);
|
||||
angleTable.Controls.Add(stepLabel, 0, 2);
|
||||
angleTable.Controls.Add(stepBox, 1, 2);
|
||||
angleTable.Controls.Add(allow180CheckBox, 0, 3);
|
||||
angleTable.Controls.Add(rotationSummaryLabel, 0, 4);
|
||||
angleTable.SetColumnSpan(allow180CheckBox, 2);
|
||||
angleTable.SetColumnSpan(rotationSummaryLabel, 2);
|
||||
angleTable.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
angleTable.Margin = new System.Windows.Forms.Padding(0);
|
||||
angleTable.Name = "angleTable";
|
||||
angleTable.RowCount = 5;
|
||||
angleTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
angleTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
angleTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
angleTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
|
||||
angleTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
angleTable.TabIndex = 1;
|
||||
//
|
||||
// startLabel
|
||||
//
|
||||
startLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
startLabel.AutoSize = true;
|
||||
startLabel.Name = "startLabel";
|
||||
startLabel.Text = "Start (°):";
|
||||
//
|
||||
// startBox
|
||||
//
|
||||
startBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
startBox.DecimalPlaces = 2;
|
||||
startBox.Maximum = new decimal(new int[] { 360, 0, 0, 0 });
|
||||
startBox.Name = "startBox";
|
||||
startBox.Size = new System.Drawing.Size(90, 25);
|
||||
startBox.TabIndex = 0;
|
||||
startBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
startBox.ValueChanged += Rotation_Changed;
|
||||
//
|
||||
// endLabel
|
||||
//
|
||||
endLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
endLabel.AutoSize = true;
|
||||
endLabel.Name = "endLabel";
|
||||
endLabel.Text = "End (°):";
|
||||
//
|
||||
// endBox
|
||||
//
|
||||
endBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
endBox.DecimalPlaces = 2;
|
||||
endBox.Maximum = new decimal(new int[] { 360, 0, 0, 0 });
|
||||
endBox.Name = "endBox";
|
||||
endBox.Size = new System.Drawing.Size(90, 25);
|
||||
endBox.TabIndex = 1;
|
||||
endBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
endBox.ValueChanged += Rotation_Changed;
|
||||
//
|
||||
// stepLabel
|
||||
//
|
||||
stepLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
stepLabel.AutoSize = true;
|
||||
stepLabel.Name = "stepLabel";
|
||||
stepLabel.Text = "Step (°):";
|
||||
//
|
||||
// stepBox
|
||||
//
|
||||
stepBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
stepBox.DecimalPlaces = 2;
|
||||
stepBox.Maximum = new decimal(new int[] { 360, 0, 0, 0 });
|
||||
stepBox.Name = "stepBox";
|
||||
stepBox.Size = new System.Drawing.Size(90, 25);
|
||||
stepBox.TabIndex = 2;
|
||||
stepBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
|
||||
toolTip.SetToolTip(stepBox, "0 lets the nesting engines choose (0°, 90°, 180° and 270°)");
|
||||
stepBox.ValueChanged += Rotation_Changed;
|
||||
//
|
||||
// allow180CheckBox
|
||||
//
|
||||
allow180CheckBox.AutoSize = true;
|
||||
allow180CheckBox.Name = "allow180CheckBox";
|
||||
allow180CheckBox.TabIndex = 3;
|
||||
allow180CheckBox.Text = "Allow 180° equivalent";
|
||||
allow180CheckBox.UseVisualStyleBackColor = true;
|
||||
allow180CheckBox.CheckedChanged += Rotation_Changed;
|
||||
//
|
||||
// rotationSummaryLabel
|
||||
//
|
||||
rotationSummaryLabel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
rotationSummaryLabel.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
rotationSummaryLabel.Margin = new System.Windows.Forms.Padding(3, 8, 3, 0);
|
||||
rotationSummaryLabel.Name = "rotationSummaryLabel";
|
||||
rotationSummaryLabel.Size = new System.Drawing.Size(200, 60);
|
||||
//
|
||||
// notesLabel
|
||||
//
|
||||
notesLabel.AutoSize = true;
|
||||
notesLabel.Margin = new System.Windows.Forms.Padding(3, 6, 3, 0);
|
||||
notesLabel.Margin = new System.Windows.Forms.Padding(3, 8, 3, 0);
|
||||
notesLabel.Name = "notesLabel";
|
||||
notesLabel.Text = "Notes:";
|
||||
//
|
||||
@@ -360,10 +526,11 @@ namespace OpenNest.Forms
|
||||
notesBox.AcceptsReturn = true;
|
||||
notesBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
notesBox.MaxLength = 4000;
|
||||
notesBox.MinimumSize = new System.Drawing.Size(0, 60);
|
||||
notesBox.Multiline = true;
|
||||
notesBox.Name = "notesBox";
|
||||
notesBox.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
||||
notesBox.TabIndex = 13;
|
||||
notesBox.TabIndex = 3;
|
||||
notesBox.TextChanged += Value_Changed;
|
||||
//
|
||||
// statusLabel
|
||||
@@ -383,7 +550,7 @@ namespace OpenNest.Forms
|
||||
buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft;
|
||||
buttonPanel.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
|
||||
buttonPanel.Name = "buttonPanel";
|
||||
buttonPanel.TabIndex = 14;
|
||||
buttonPanel.TabIndex = 4;
|
||||
//
|
||||
// cancelButton
|
||||
//
|
||||
@@ -409,66 +576,95 @@ namespace OpenNest.Forms
|
||||
AcceptButton = saveButton;
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
CancelButton = cancelButton;
|
||||
ClientSize = new System.Drawing.Size(464, 641);
|
||||
ClientSize = new System.Drawing.Size(540, 800);
|
||||
Controls.Add(layoutTable);
|
||||
Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
MinimumSize = new System.Drawing.Size(420, 600);
|
||||
MinimumSize = new System.Drawing.Size(520, 780);
|
||||
Name = "EditLibraryDrawingForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Edit Drawing";
|
||||
Shown += EditLibraryDrawingForm_Shown;
|
||||
thicknessPanel.ResumeLayout(false);
|
||||
thicknessPanel.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)thicknessBox).EndInit();
|
||||
layoutTable.ResumeLayout(false);
|
||||
layoutTable.PerformLayout();
|
||||
detailsTable.ResumeLayout(false);
|
||||
detailsTable.PerformLayout();
|
||||
detailsGroup.ResumeLayout(false);
|
||||
detailsGroup.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)priorityBox).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)stepBox).EndInit();
|
||||
colorPanel.ResumeLayout(false);
|
||||
colorPanel.PerformLayout();
|
||||
nestingTable.ResumeLayout(false);
|
||||
nestingTable.PerformLayout();
|
||||
nestingGroup.ResumeLayout(false);
|
||||
nestingGroup.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)startBox).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)endBox).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)stepBox).EndInit();
|
||||
angleTable.ResumeLayout(false);
|
||||
angleTable.PerformLayout();
|
||||
rotationTable.ResumeLayout(false);
|
||||
rotationTable.PerformLayout();
|
||||
rotationGroup.ResumeLayout(false);
|
||||
rotationGroup.PerformLayout();
|
||||
buttonPanel.ResumeLayout(false);
|
||||
buttonPanel.PerformLayout();
|
||||
layoutTable.ResumeLayout(false);
|
||||
layoutTable.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.TableLayoutPanel layoutTable;
|
||||
private System.Windows.Forms.GroupBox detailsGroup;
|
||||
private System.Windows.Forms.TableLayoutPanel detailsTable;
|
||||
private System.Windows.Forms.Label nameLabel;
|
||||
private System.Windows.Forms.TextBox nameBox;
|
||||
private System.Windows.Forms.Label customerLabel;
|
||||
private System.Windows.Forms.TextBox customerBox;
|
||||
private System.Windows.Forms.ComboBox customerBox;
|
||||
private System.Windows.Forms.Label materialLabel;
|
||||
private System.Windows.Forms.TextBox materialBox;
|
||||
private System.Windows.Forms.ComboBox materialBox;
|
||||
private System.Windows.Forms.Label gradeLabel;
|
||||
private System.Windows.Forms.TextBox gradeBox;
|
||||
private System.Windows.Forms.ComboBox gradeBox;
|
||||
private System.Windows.Forms.Label thicknessLabel;
|
||||
private System.Windows.Forms.FlowLayoutPanel thicknessPanel;
|
||||
private System.Windows.Forms.NumericUpDown thicknessBox;
|
||||
private OpenNest.Controls.ThicknessComboBox thicknessBox;
|
||||
private System.Windows.Forms.Label unitsLabel;
|
||||
private System.Windows.Forms.Label cutTypeLabel;
|
||||
private System.Windows.Forms.ComboBox cutTypeBox;
|
||||
private System.Windows.Forms.Label settingsLabel;
|
||||
private System.Windows.Forms.GroupBox nestingGroup;
|
||||
private System.Windows.Forms.TableLayoutPanel nestingTable;
|
||||
private System.Windows.Forms.CheckBox cutFirstCheckBox;
|
||||
private System.Windows.Forms.Label priorityLabel;
|
||||
private System.Windows.Forms.NumericUpDown priorityBox;
|
||||
private System.Windows.Forms.Label stepLabel;
|
||||
private System.Windows.Forms.NumericUpDown stepBox;
|
||||
private System.Windows.Forms.Label colorLabel;
|
||||
private System.Windows.Forms.FlowLayoutPanel colorPanel;
|
||||
private System.Windows.Forms.Button colorButton;
|
||||
private System.Windows.Forms.Button automaticColorButton;
|
||||
private System.Windows.Forms.Button resetColorButton;
|
||||
private System.Windows.Forms.Label colorModeLabel;
|
||||
private System.Windows.Forms.GroupBox rotationGroup;
|
||||
private System.Windows.Forms.TableLayoutPanel rotationTable;
|
||||
private OpenNest.Controls.RotationRangeDial rotationDial;
|
||||
private System.Windows.Forms.TableLayoutPanel angleTable;
|
||||
private System.Windows.Forms.Label startLabel;
|
||||
private System.Windows.Forms.NumericUpDown startBox;
|
||||
private System.Windows.Forms.Label endLabel;
|
||||
private System.Windows.Forms.NumericUpDown endBox;
|
||||
private System.Windows.Forms.Label stepLabel;
|
||||
private System.Windows.Forms.NumericUpDown stepBox;
|
||||
private System.Windows.Forms.CheckBox allow180CheckBox;
|
||||
private System.Windows.Forms.Label colorLabel;
|
||||
private System.Windows.Forms.Button colorButton;
|
||||
private System.Windows.Forms.Label rotationSummaryLabel;
|
||||
private System.Windows.Forms.Label notesLabel;
|
||||
private System.Windows.Forms.TextBox notesBox;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
private System.Windows.Forms.FlowLayoutPanel buttonPanel;
|
||||
private System.Windows.Forms.Button cancelButton;
|
||||
private System.Windows.Forms.Button saveButton;
|
||||
private System.Windows.Forms.ToolTip toolTip;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,13 @@ namespace OpenNest.Forms
|
||||
/// keeps the same package; nests that already hold the drawing keep their revision. When
|
||||
/// the new details would duplicate another active library drawing (D4), the operator is
|
||||
/// warned before saving.
|
||||
/// <para>
|
||||
/// Customer, material, grade and thickness are drop-downs of the values the library and
|
||||
/// the machine material catalog already use (<see cref="DrawingLibraryChoices"/>); any
|
||||
/// value may still be typed. The rotation range is shown and dragged on a
|
||||
/// <see cref="OpenNest.Controls.RotationRangeDial"/> kept in step with the angle boxes. The color is either
|
||||
/// custom or automatic, where each nest's color scheme picks it.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public partial class EditLibraryDrawingForm : Form
|
||||
{
|
||||
@@ -29,12 +36,21 @@ namespace OpenNest.Forms
|
||||
private readonly decimal initialStep;
|
||||
private readonly decimal initialStart;
|
||||
private readonly decimal initialEnd;
|
||||
private readonly string initialThickness;
|
||||
private readonly Color initialColor;
|
||||
private DrawingLibraryChoices choices = DrawingLibraryChoices.Empty;
|
||||
private Color color;
|
||||
private bool saving;
|
||||
|
||||
/// <summary>Shows a message; replaced by tests.</summary>
|
||||
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
|
||||
|
||||
/// <summary>The materials and grades on file for the drop-downs; replaced by tests.</summary>
|
||||
internal Func<IEnumerable<MaterialConfig>> LoadMaterialCatalog { get; set; } = MachineSettings.LoadMaterials;
|
||||
|
||||
/// <summary>The library's drop-down choices once loaded; replaced by tests.</summary>
|
||||
internal Task ChoicesLoaded { get; private set; } = Task.CompletedTask;
|
||||
|
||||
/// <summary>The saved revision once the dialog closes with OK; otherwise null.</summary>
|
||||
public DrawingLibraryRecord Saved { get; private set; }
|
||||
|
||||
@@ -49,8 +65,9 @@ namespace OpenNest.Forms
|
||||
customerBox.Text = listed.Customer;
|
||||
materialBox.Text = listed.MaterialName;
|
||||
gradeBox.Text = listed.MaterialGrade;
|
||||
thicknessBox.Value = System.Math.Clamp((decimal)DrawingLibraryRules.NormalizeThickness(listed.Thickness),
|
||||
thicknessBox.Minimum, thicknessBox.Maximum);
|
||||
thicknessBox.Units = listed.Units;
|
||||
thicknessBox.Value = DrawingLibraryRules.NormalizeThickness(listed.Thickness);
|
||||
initialThickness = thicknessBox.Text;
|
||||
unitsLabel.Text = UnitsHelper.GetShortString(listed.Units);
|
||||
cutTypeBox.Items.AddRange(Enum.GetNames<CutType>());
|
||||
cutTypeBox.SelectedItem = listed.CutType.ToString();
|
||||
@@ -61,23 +78,101 @@ namespace OpenNest.Forms
|
||||
initialStart = SetDegrees(startBox, settings.StartAngle);
|
||||
initialEnd = SetDegrees(endBox, settings.EndAngle);
|
||||
allow180CheckBox.Checked = settings.Allow180Equivalent;
|
||||
color = Color.FromArgb(settings.ColorArgb);
|
||||
colorButton.BackColor = color.A == 0 ? SystemColors.Control : color;
|
||||
initialColor = Color.FromArgb(settings.ColorArgb);
|
||||
color = initialColor;
|
||||
notesBox.Text = listed.Notes;
|
||||
|
||||
Text = $"Edit '{listed.Name}' — library revision {listed.Revision}";
|
||||
UpdateRotation();
|
||||
UpdateColor();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void EditLibraryDrawingForm_Shown(object sender, EventArgs e) => ChoicesLoaded = LoadChoicesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fills the drop-downs from the material catalog at once, then from the library;
|
||||
/// a library that cannot be read leaves the catalog's lists.
|
||||
/// </summary>
|
||||
internal async Task LoadChoicesAsync()
|
||||
{
|
||||
var catalog = (LoadMaterialCatalog?.Invoke() ?? Enumerable.Empty<MaterialConfig>())
|
||||
.Concat(PostProcessorMaterials.Names.Select(name => new MaterialConfig { Name = name }))
|
||||
.ToList();
|
||||
ApplyChoices(DrawingLibraryChoices.From(new[] { session.Listed }, session.Listed.Units, catalog));
|
||||
try
|
||||
{
|
||||
var loaded = await session.LoadChoicesAsync(catalog);
|
||||
if (!IsDisposed)
|
||||
ApplyChoices(loaded);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException
|
||||
|| ex is ArgumentException)
|
||||
{
|
||||
// The drop-downs are suggestions; typing still works without them.
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyChoices(DrawingLibraryChoices loaded)
|
||||
{
|
||||
choices = loaded;
|
||||
SetItems(customerBox, choices.Customers);
|
||||
SetItems(materialBox, choices.Materials);
|
||||
RefreshGrades();
|
||||
}
|
||||
|
||||
private void Material_Changed(object sender, EventArgs e)
|
||||
{
|
||||
RefreshGrades();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void Grade_Changed(object sender, EventArgs e)
|
||||
{
|
||||
thicknessBox.SetUsedValues(choices.ThicknessesOf(materialBox.Text, gradeBox.Text));
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
/// <summary>Lists the grades and thicknesses used for the material as typed.</summary>
|
||||
private void RefreshGrades()
|
||||
{
|
||||
SetItems(gradeBox, choices.GradesOf(materialBox.Text));
|
||||
thicknessBox.SetUsedValues(choices.ThicknessesOf(materialBox.Text, gradeBox.Text));
|
||||
}
|
||||
|
||||
// Replaces a drop-down's list, keeping what is typed and where the caret is.
|
||||
private static void SetItems(ComboBox box, IReadOnlyList<string> items)
|
||||
{
|
||||
if (box.Items.Cast<string>().SequenceEqual(items))
|
||||
return;
|
||||
var text = box.Text;
|
||||
var (start, length) = (box.SelectionStart, box.SelectionLength);
|
||||
box.BeginUpdate();
|
||||
box.Items.Clear();
|
||||
box.Items.AddRange(items.ToArray<object>());
|
||||
box.EndUpdate();
|
||||
if (box.Text != text)
|
||||
{
|
||||
box.Text = text;
|
||||
box.Select(start, length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The details as currently entered.</summary>
|
||||
internal DrawingLibraryDetails ReadDetails() => new(
|
||||
nameBox.Text,
|
||||
customerBox.Text,
|
||||
materialBox.Text,
|
||||
gradeBox.Text,
|
||||
(double)thicknessBox.Value,
|
||||
ReadThickness(),
|
||||
Enum.TryParse<CutType>(cutTypeBox.SelectedItem as string, out var cutType) ? cutType : session.Listed.CutType);
|
||||
|
||||
// Untouched text keeps the stored thickness exactly; unreadable text fails the rules instead of reading as 0.
|
||||
private double ReadThickness() =>
|
||||
thicknessBox.Text == initialThickness ? session.Listed.Thickness
|
||||
: thicknessBox.TryGetValue(out var thickness) ? thickness
|
||||
: double.NaN;
|
||||
|
||||
/// <summary>The settings as currently entered; unchanged angles keep their exact stored radians.</summary>
|
||||
internal DrawingNestingSettings ReadSettings()
|
||||
{
|
||||
@@ -96,17 +191,103 @@ namespace OpenNest.Forms
|
||||
|
||||
private void Value_Changed(object sender, EventArgs e) => UpdateCommands();
|
||||
|
||||
private void Rotation_Changed(object sender, EventArgs e)
|
||||
{
|
||||
UpdateRotation();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void RotationDial_AnglesChanged(object sender, EventArgs e)
|
||||
{
|
||||
// Rotation_Changed redraws the dial from the boxes, which round to their decimal places.
|
||||
startBox.Value = System.Math.Clamp((decimal)rotationDial.StartAngle, startBox.Minimum, startBox.Maximum);
|
||||
endBox.Value = System.Math.Clamp((decimal)rotationDial.EndAngle, endBox.Minimum, endBox.Maximum);
|
||||
}
|
||||
|
||||
private void UpdateRotation()
|
||||
{
|
||||
rotationDial.StepAngle = (double)stepBox.Value;
|
||||
rotationDial.Allow180Equivalent = allow180CheckBox.Checked;
|
||||
rotationDial.StartAngle = (double)startBox.Value;
|
||||
rotationDial.EndAngle = (double)endBox.Value;
|
||||
rotationSummaryLabel.Text = DescribeRotation((double)startBox.Value, (double)endBox.Value,
|
||||
(double)stepBox.Value, allow180CheckBox.Checked);
|
||||
}
|
||||
|
||||
/// <summary>What the nesting engines do with these rotation settings, in degrees.</summary>
|
||||
internal static string DescribeRotation(double start, double end, double step, bool allow180)
|
||||
{
|
||||
if (step <= 0)
|
||||
return "Automatic: the nesting engines try 0°, 90°, 180° and 270°. Set a step to limit rotation to the range.";
|
||||
if (end < start)
|
||||
return "The end is before the start, so the nesting engines cannot use this range.";
|
||||
|
||||
var count = OpenNest.Controls.RotationRangeDial.TriedAngles(start, end, step, allow180).Count;
|
||||
var angles = count == 1 ? "1 angle" : $"{count.ToString(CultureInfo.CurrentCulture)} angles";
|
||||
var range = start == end || step >= end - start
|
||||
? $"Fixed at {Degrees(start)}"
|
||||
: $"{Degrees(start)} to {Degrees(end)} every {Degrees(step)}";
|
||||
return $"{range}{(allow180 ? ", and 180° from each" : "")}: tries {angles}.";
|
||||
}
|
||||
|
||||
private static string Degrees(double degrees) => $"{degrees.ToString("0.##", CultureInfo.CurrentCulture)}°";
|
||||
|
||||
private void ColorButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new ColorDialog { Color = color, FullOpen = true };
|
||||
using var dialog = new ColorDialog { Color = color.A == 0 ? Drawing.GetPartColor(0) : color, FullOpen = true };
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
// Opaque, as nest drawings are.
|
||||
color = Color.FromArgb(255, dialog.Color);
|
||||
colorButton.BackColor = color;
|
||||
UpdateColor();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void AutomaticColorButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
// Any transparent value means automatic; keep the stored one so the edit stays unchanged.
|
||||
color = initialColor.A == 0 ? initialColor : Color.FromArgb(0);
|
||||
UpdateColor();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void ResetColorButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
color = initialColor;
|
||||
UpdateColor();
|
||||
UpdateCommands();
|
||||
}
|
||||
|
||||
private void UpdateColor()
|
||||
{
|
||||
var automatic = color.A == 0;
|
||||
colorButton.BackColor = automatic ? SystemColors.Window : color;
|
||||
colorButton.Invalidate();
|
||||
colorModeLabel.Text = automatic
|
||||
? "Automatic: each nest's color scheme picks the part color."
|
||||
: $"Custom: #{color.R:X2}{color.G:X2}{color.B:X2} in every color scheme.";
|
||||
automaticColorButton.Enabled = !automatic;
|
||||
resetColorButton.Enabled = color.ToArgb() != initialColor.ToArgb();
|
||||
}
|
||||
|
||||
// An automatic color shows a sample of the current scheme's part colors.
|
||||
private void ColorButton_Paint(object sender, PaintEventArgs e)
|
||||
{
|
||||
if (color.A != 0)
|
||||
return;
|
||||
var area = colorButton.ClientRectangle;
|
||||
area.Inflate(-3, -3);
|
||||
const int samples = 4;
|
||||
var width = area.Width / (float)samples;
|
||||
for (var i = 0; i < samples; i++)
|
||||
{
|
||||
using var brush = new SolidBrush(Drawing.GetPartColor(i));
|
||||
e.Graphics.FillRectangle(brush, area.X + i * width, area.Y, width + 1, area.Height);
|
||||
}
|
||||
TextRenderer.DrawText(e.Graphics, "Auto", colorButton.Font, area, Color.Black,
|
||||
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
|
||||
}
|
||||
|
||||
private async void SaveButton_Click(object sender, EventArgs e) => await SaveAsync();
|
||||
|
||||
/// <summary>Saves the edit as a new library revision; closes with OK on success.</summary>
|
||||
|
||||
@@ -406,6 +406,26 @@ rotation step/start/end (degrees), 180° equivalent and color, then the notes.
|
||||
Text is trimmed and thickness rounded as the server stores it; Save stays
|
||||
disabled until something changed and every value passes the library rules.
|
||||
|
||||
Customer, material, grade and thickness are drop-downs that still accept any
|
||||
typed value (`OpenNest.Data.DrawingLibraryChoices`). They list what the newest
|
||||
5,000 active library drawings use, plus the machine configurations' and post
|
||||
processors' materials and grades; grades follow the typed material, and the
|
||||
thicknesses that material (and grade) already uses in the drawing's units are
|
||||
listed ahead of the standard gauges and fractions. A library that cannot be read
|
||||
leaves the catalog's lists.
|
||||
|
||||
The color is **Custom** (the swatch opens the color picker) or **Automatic**
|
||||
(stored as 0), where each nest's color scheme picks the part color, so it
|
||||
changes with the scheme. **Reset** returns to the revision's stored color.
|
||||
|
||||
The rotation dial beside the angle boxes shades the range counterclockwise from
|
||||
start (hollow handle) to end (solid handle), 0° at the right, and ticks each
|
||||
angle the nesting engines try (`RotationPolicy.FromLegacy`); allowed 180°
|
||||
equivalents are hatched. A zero step is the engines' automatic 0°/90°/180°/270°,
|
||||
so the range is drawn faded. Dragging a handle moves it in whole degrees (Shift:
|
||||
15°) within 0° ≤ start ≤ end ≤ 360°; the end can be dragged past 0° to a full
|
||||
turn. With focus, the arrow keys move a handle and Space switches handles.
|
||||
|
||||
When the new details would give the drawing another identity (D4: normalized
|
||||
customer, name, material and grade, exact thickness, units and cut type), Save
|
||||
first looks up active library drawings that already have them and, if any do,
|
||||
|
||||
Reference in new issue
Block a user