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:
ajandClaude Opus 5.5 committed 2026-10-26 09:46:43 -04:00
1 parent 0ba5539fab
commit 6e40c2e6ed
10 files changed
+1524 -177

No files matched your search

+83
View File
@@ -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()
{
+457
View File
@@ -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° &lt;= start &lt;= end &lt;= 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,
}
}
+28 -2
View File
@@ -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
View File
@@ -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;
}
}
+188 -7
View File
@@ -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>
+20
View File
@@ -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,