mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-05 11:21:35 -05:00
feat(desktop): draw and edit a plate's sheet outline
The plate view fills the outline, hatches obstacles and draws the usable area; Edit Plate imports an outline from a CAD file or clears it, with the size read-only while the sheet has one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
360c7136dd
commit
662d3daf8c
5 files changed
+257
-1
No files matched your search
@@ -0,0 +1,45 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Controls;
|
||||
|
||||
/// <summary>The plate view draws an outlined sheet as its outline: the missing corner shows the
|
||||
/// background and an obstacle is hatched, in the plate's quadrant.</summary>
|
||||
public class OutlinedPlateDisplayTests
|
||||
{
|
||||
// A 100 x 100 quadrant-4 sheet missing its upper-right 50 x 50 corner, with a 10 x 10 burned
|
||||
// patch at (10..20, 70..80). PointWorldToGraph flips Y, so plate (x, y) lands on graph (x, 100 - y).
|
||||
private static PlateView BuildView()
|
||||
{
|
||||
var view = new PlateView(ColorSchemeRegistry.Get("Workshop"))
|
||||
{
|
||||
Size = new System.Drawing.Size(100, 100),
|
||||
};
|
||||
view.Matrix = new Matrix();
|
||||
view.Plate.Quadrant = 4;
|
||||
view.Plate.Outline = new SheetOutline(
|
||||
new[] { new Vector(0, 0), new Vector(100, 0), new Vector(100, 50), new Vector(50, 50), new Vector(50, 100), new Vector(0, 100) },
|
||||
new[] { new[] { new Vector(10, 70), new Vector(20, 70), new Vector(20, 80), new Vector(10, 80) } });
|
||||
return view;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingCornerShowsTheBackgroundAndTheRestIsFilled() => StaTestThread.Run(() =>
|
||||
{
|
||||
using var view = BuildView();
|
||||
using var image = new Bitmap(100, 100);
|
||||
using var g = Graphics.FromImage(image);
|
||||
g.Clear(Color.White);
|
||||
|
||||
view.Renderer.DrawPlate(g);
|
||||
|
||||
var fill = ((SolidBrush)view.ColorScheme.LayoutFillBrush).Color;
|
||||
Assert.Equal(Color.White.ToArgb(), image.GetPixel(75, 25).ToArgb()); // the missing corner
|
||||
Assert.Equal(fill.ToArgb(), image.GetPixel(25, 75).ToArgb()); // the sheet
|
||||
Assert.Equal(fill.ToArgb(), image.GetPixel(75, 75).ToArgb()); // the arm
|
||||
Assert.NotEqual(fill.ToArgb(), image.GetPixel(15, 25).ToArgb()); // the hatched obstacle
|
||||
Assert.NotEqual(Color.White.ToArgb(), image.GetPixel(15, 25).ToArgb());
|
||||
}, TimeSpan.FromMinutes(1), "Outlined plate display test timed out.");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
/// <summary>Edit Plate with a sheet outline: the size follows the outline and is read-only
|
||||
/// while there is one, Done applies the outline, and Clear makes the sheet a rectangle again.</summary>
|
||||
public class EditPlateFormOutlineTests
|
||||
{
|
||||
private static SheetOutline Ell() => new(new[]
|
||||
{
|
||||
new Vector(0, 0), new Vector(96, 0), new Vector(96, 24), new Vector(48, 24), new Vector(48, 48), new Vector(0, 48),
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void OutlineSetsTheSizeAndDoneAppliesIt() => RunSta(() =>
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
using var form = new EditPlateForm(plate);
|
||||
var size = (TextBox)form.Controls.Find("textBoxSize", true)[0];
|
||||
var clear = (Button)form.Controls.Find("buttonClearOutline", true)[0];
|
||||
var done = (Button)form.Controls.Find("applyButton", true)[0];
|
||||
Assert.False(size.ReadOnly);
|
||||
Assert.False(clear.Enabled);
|
||||
|
||||
form.Outline = Ell();
|
||||
|
||||
Assert.True(size.ReadOnly);
|
||||
Assert.True(clear.Enabled);
|
||||
Assert.Equal(new Size(48, 96).ToString(), form.SizeString);
|
||||
Assert.Contains("6 vertices", ((Label)form.Controls.Find("labelOutline", true)[0]).Text);
|
||||
Assert.True(done.Enabled);
|
||||
|
||||
done.PerformClick();
|
||||
Assert.Equal(Ell(), plate.Outline);
|
||||
Assert.Equal(48, plate.Size.Width);
|
||||
Assert.Equal(96, plate.Size.Length);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ClearMakesTheSheetARectangleOfTheSameSize() => RunSta(() =>
|
||||
{
|
||||
var plate = new Plate { Outline = Ell(), Quadrant = 2 };
|
||||
using var form = new EditPlateForm(plate);
|
||||
var size = (TextBox)form.Controls.Find("textBoxSize", true)[0];
|
||||
Assert.True(size.ReadOnly);
|
||||
|
||||
((Button)form.Controls.Find("buttonClearOutline", true)[0]).PerformClick();
|
||||
Assert.False(size.ReadOnly);
|
||||
((Button)form.Controls.Find("applyButton", true)[0]).PerformClick();
|
||||
|
||||
Assert.Null(plate.Outline);
|
||||
Assert.Equal(48, plate.Size.Width);
|
||||
Assert.Equal(96, plate.Size.Length);
|
||||
Assert.Equal(2, plate.Quadrant);
|
||||
});
|
||||
|
||||
private static void RunSta(Action action) =>
|
||||
StaTestThread.Run(action, TimeSpan.FromSeconds(30), "Edit Plate outline test timed out.");
|
||||
}
|
||||
@@ -25,6 +25,12 @@ namespace OpenNest.Controls
|
||||
public void DrawPlate(Graphics g)
|
||||
{
|
||||
var plate = view.DisplayPlate;
|
||||
if (plate.Outline != null)
|
||||
{
|
||||
DrawOutlinedPlate(g, plate);
|
||||
return;
|
||||
}
|
||||
|
||||
var plateRect = new RectangleF
|
||||
{
|
||||
Width = view.LengthWorldToGui(plate.Size.Length),
|
||||
@@ -235,6 +241,50 @@ namespace OpenNest.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Color ObstacleHatchColor = Color.FromArgb(170, 120, 60, 60);
|
||||
private static readonly Color ObstacleBackColor = Color.FromArgb(60, 200, 80, 80);
|
||||
|
||||
/// <summary>
|
||||
/// An irregular sheet: its outline filled as the sheet, obstacles hatched, and the
|
||||
/// usable area (the outline eroded by the largest edge spacing) in the edge-spacing pen,
|
||||
/// all placed in the plate's quadrant like the rectangle.
|
||||
/// </summary>
|
||||
private void DrawOutlinedPlate(Graphics g, Plate plate)
|
||||
{
|
||||
var outline = plate.Outline;
|
||||
var origin = outline.PlacementOrigin(plate.Quadrant);
|
||||
|
||||
using var sheet = new GraphicsPath(FillMode.Alternate);
|
||||
sheet.AddPolygon(ToGraph(outline.Points, origin));
|
||||
foreach (var obstacle in outline.Obstacles)
|
||||
sheet.AddPolygon(ToGraph(obstacle, origin));
|
||||
g.FillPath(view.ColorScheme.LayoutFillBrush, sheet);
|
||||
|
||||
if (outline.Obstacles.Count > 0)
|
||||
{
|
||||
using var hatch = new HatchBrush(HatchStyle.WideUpwardDiagonal, ObstacleHatchColor, ObstacleBackColor);
|
||||
using var pen = new Pen(ObstacleHatchColor, 1.5f);
|
||||
foreach (var obstacle in outline.Obstacles)
|
||||
{
|
||||
var points = ToGraph(obstacle, origin);
|
||||
g.FillPolygon(hatch, points);
|
||||
g.DrawPolygon(pen, points);
|
||||
}
|
||||
}
|
||||
|
||||
var edge = System.Math.Max(System.Math.Max(plate.EdgeSpacing.Left, plate.EdgeSpacing.Right),
|
||||
System.Math.Max(plate.EdgeSpacing.Top, plate.EdgeSpacing.Bottom));
|
||||
if (edge > 0)
|
||||
foreach (var path in outline.Usable(edge, origin))
|
||||
g.DrawPolygon(view.ColorScheme.EdgeSpacingPen,
|
||||
path.Select(p => view.PointWorldToGraph(p.x, p.y)).ToArray());
|
||||
|
||||
g.DrawPath(view.ColorScheme.LayoutOutlinePen, sheet);
|
||||
}
|
||||
|
||||
private PointF[] ToGraph(IReadOnlyList<Vector> points, Vector origin) =>
|
||||
points.Select(p => view.PointWorldToGraph(p.X + origin.X, p.Y + origin.Y)).ToArray();
|
||||
|
||||
// D8: a clamp taken off this sheet, by Auto Nest as a last resort or by hand (right-click).
|
||||
// Distinct from an active clamp - a
|
||||
// dashed grey outline and a small "remove" marker, still display + exclusion only
|
||||
|
||||
Generated
+39
@@ -44,6 +44,9 @@
|
||||
this.cancelButton = new System.Windows.Forms.Button();
|
||||
this.bottomPanel1 = new OpenNest.Controls.BottomPanel();
|
||||
this.quadrantSelect1 = new OpenNest.Controls.QuadrantSelect();
|
||||
this.labelOutline = new System.Windows.Forms.Label();
|
||||
this.buttonImportOutline = new System.Windows.Forms.Button();
|
||||
this.buttonClearOutline = new System.Windows.Forms.Button();
|
||||
this.numericUpDownEdgeSpacingLeft = new OpenNest.Controls.NumericUpDown();
|
||||
this.numericUpDownEdgeSpacingTop = new OpenNest.Controls.NumericUpDown();
|
||||
this.numericUpDownEdgeSpacingRight = new OpenNest.Controls.NumericUpDown();
|
||||
@@ -251,6 +254,36 @@
|
||||
this.bottomPanel1.Size = new System.Drawing.Size(550, 50);
|
||||
this.bottomPanel1.TabIndex = 7;
|
||||
//
|
||||
// labelOutline
|
||||
//
|
||||
this.labelOutline.AutoSize = true;
|
||||
this.labelOutline.Location = new System.Drawing.Point(21, 131);
|
||||
this.labelOutline.Name = "labelOutline";
|
||||
this.labelOutline.Size = new System.Drawing.Size(103, 16);
|
||||
this.labelOutline.TabIndex = 8;
|
||||
this.labelOutline.Text = "Rectangular sheet";
|
||||
//
|
||||
// buttonImportOutline
|
||||
//
|
||||
this.buttonImportOutline.Location = new System.Drawing.Point(130, 125);
|
||||
this.buttonImportOutline.Name = "buttonImportOutline";
|
||||
this.buttonImportOutline.Size = new System.Drawing.Size(70, 28);
|
||||
this.buttonImportOutline.TabIndex = 9;
|
||||
this.buttonImportOutline.Text = "Outline...";
|
||||
this.buttonImportOutline.UseVisualStyleBackColor = true;
|
||||
this.buttonImportOutline.Click += new System.EventHandler(this.buttonImportOutline_Click);
|
||||
//
|
||||
// buttonClearOutline
|
||||
//
|
||||
this.buttonClearOutline.Enabled = false;
|
||||
this.buttonClearOutline.Location = new System.Drawing.Point(204, 125);
|
||||
this.buttonClearOutline.Name = "buttonClearOutline";
|
||||
this.buttonClearOutline.Size = new System.Drawing.Size(56, 28);
|
||||
this.buttonClearOutline.TabIndex = 10;
|
||||
this.buttonClearOutline.Text = "Clear";
|
||||
this.buttonClearOutline.UseVisualStyleBackColor = true;
|
||||
this.buttonClearOutline.Click += new System.EventHandler(this.buttonClearOutline_Click);
|
||||
//
|
||||
// quadrantSelect1
|
||||
//
|
||||
this.quadrantSelect1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
@@ -361,6 +394,9 @@
|
||||
this.Controls.Add(this.groupBox2);
|
||||
this.Controls.Add(this.groupBox1);
|
||||
this.Controls.Add(this.tableLayoutPanel1);
|
||||
this.Controls.Add(this.labelOutline);
|
||||
this.Controls.Add(this.buttonImportOutline);
|
||||
this.Controls.Add(this.buttonClearOutline);
|
||||
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
||||
this.MaximizeBox = false;
|
||||
@@ -392,6 +428,9 @@
|
||||
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
|
||||
private System.Windows.Forms.Label labelSize;
|
||||
private System.Windows.Forms.Button applyButton;
|
||||
private System.Windows.Forms.Label labelOutline;
|
||||
private System.Windows.Forms.Button buttonImportOutline;
|
||||
private System.Windows.Forms.Button buttonClearOutline;
|
||||
private System.Windows.Forms.TextBox textBoxSize;
|
||||
private System.Windows.Forms.GroupBox groupBox1;
|
||||
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel2;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.IO;
|
||||
using Timer = System.Windows.Forms.Timer;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
@@ -10,6 +12,7 @@ namespace OpenNest.Forms
|
||||
private readonly Timer timer;
|
||||
|
||||
private Units units;
|
||||
private SheetOutline outline;
|
||||
|
||||
public EditPlateForm(Plate plate)
|
||||
{
|
||||
@@ -123,6 +126,28 @@ namespace OpenNest.Forms
|
||||
set { quadrantSelect1.Quadrant = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sheet outline the plate gets when the dialog is accepted: the plate's own until
|
||||
/// one is imported or cleared. An outlined sheet's size is its outline's bounds, so the
|
||||
/// size box is read-only while there is one.
|
||||
/// </summary>
|
||||
public SheetOutline Outline
|
||||
{
|
||||
get { return outline; }
|
||||
set
|
||||
{
|
||||
outline = value;
|
||||
textBoxSize.ReadOnly = value != null;
|
||||
buttonClearOutline.Enabled = value != null;
|
||||
if (value != null)
|
||||
textBoxSize.Text = value.Size.ToString();
|
||||
labelOutline.Text = value == null
|
||||
? "Rectangular sheet"
|
||||
: $"{value.Points.Count} vertices" + (value.Obstacles.Count == 0 ? "" : $", {value.Obstacles.Count} obstacle(s)");
|
||||
EnableCheck();
|
||||
}
|
||||
}
|
||||
|
||||
public void EnableCheck()
|
||||
{
|
||||
OpenNest.Geometry.Size size;
|
||||
@@ -158,11 +183,18 @@ namespace OpenNest.Forms
|
||||
PartSpacing = plate.PartSpacing;
|
||||
Quantity = plate.Quantity;
|
||||
Quadrant = plate.Quadrant;
|
||||
Outline = plate.Outline;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
plate.Size = OpenNest.Geometry.Size.Parse(SizeString);
|
||||
// The outline carries its own size; a parsed size would drop it.
|
||||
if (Outline != null)
|
||||
plate.Outline = Outline;
|
||||
else
|
||||
plate.Size = OpenNest.Geometry.Size.Parse(SizeString);
|
||||
if (Outline == null)
|
||||
plate.Outline = null;
|
||||
plate.EdgeSpacing.Left = LeftSpacing;
|
||||
plate.EdgeSpacing.Top = TopSpacing;
|
||||
plate.EdgeSpacing.Right = RightSpacing;
|
||||
@@ -187,5 +219,34 @@ namespace OpenNest.Forms
|
||||
{
|
||||
EnableCheck();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a sheet outline from a CAD file: the largest closed contour is the sheet and
|
||||
/// every other closed contour an obstacle, in the file's units (as drawings import).
|
||||
/// </summary>
|
||||
private void buttonImportOutline_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new OpenFileDialog
|
||||
{
|
||||
Title = "Import Sheet Outline",
|
||||
Filter = "CAD Files (*.dxf;*.dwg)|*.dxf;*.dwg|DXF Files (*.dxf)|*.dxf|DWG Files (*.dwg)|*.dwg",
|
||||
};
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
try
|
||||
{
|
||||
Outline = SheetOutline.FromGeometry(CadImporter.Import(dialog.FileName).Entities);
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or IOException or InvalidOperationException)
|
||||
{
|
||||
MessageBox.Show(this, $"The file does not hold a sheet outline: {exception.Message}",
|
||||
"Import Sheet Outline", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonClearOutline_Click(object sender, EventArgs e)
|
||||
{
|
||||
Outline = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user