feat: add Shape Library UI with configurable shapes and flange presets

Add a Shape Library dialog (Nest > Shape Library) for creating drawings
from built-in parametric shapes. Supports configuration presets loaded
from JSON files — ships with 136 standard pipe flanges. Parameters use
TextBox inputs with architectural unit parsing (feet/inches, fractions).

- ShapeLibraryForm with split layout: shape list, preview, parameters
- ShapePreviewControl for auto-zoom rendering with info overlay
- ArchUnits utility for parsing architectural measurements
- SetPreviewDefaults() on all ShapeDefinition subclasses
- Convention-based config discovery (Configurations/{ShapeName}.json)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 07:44:03 -04:00
parent 3dca25c601
commit b2a723ca60
21 changed files with 2138 additions and 2 deletions

View File

@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
{
public double Diameter { get; set; }
public override void SetPreviewDefaults()
{
Diameter = 8;
}
public override Drawing GetDrawing()
{
var entities = new List<Entity>

View File

@@ -11,6 +11,15 @@ namespace OpenNest.Shapes
public double HolePatternDiameter { get; set; }
public int HoleCount { get; set; }
public override void SetPreviewDefaults()
{
NominalPipeSize = 2;
OD = 7.5;
HoleDiameter = 0.875;
HolePatternDiameter = 5.5;
HoleCount = 8;
}
public override Drawing GetDrawing()
{
var entities = new List<Entity>();

View File

@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double Base { get; set; }
public double Height { get; set; }
public override void SetPreviewDefaults()
{
Base = 8;
Height = 10;
}
public override Drawing GetDrawing()
{
var midX = Base / 2.0;

View File

@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
public double LegWidth { get; set; }
public double LegHeight { get; set; }
public override void SetPreviewDefaults()
{
Width = 8;
Height = 10;
LegWidth = 3;
LegHeight = 3;
}
public override Drawing GetDrawing()
{
var lw = LegWidth > 0 ? LegWidth : Width / 2.0;

View File

@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
{
public double Width { get; set; }
public override void SetPreviewDefaults()
{
Width = 8;
}
public override Drawing GetDrawing()
{
var center = Width / 2.0;

View File

@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double Length { get; set; }
public double Width { get; set; }
public override void SetPreviewDefaults()
{
Length = 12;
Width = 6;
}
public override Drawing GetDrawing()
{
var entities = new List<Entity>

View File

@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double Width { get; set; }
public double Height { get; set; }
public override void SetPreviewDefaults()
{
Width = 8;
Height = 6;
}
public override Drawing GetDrawing()
{
var entities = new List<Entity>

View File

@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double OuterDiameter { get; set; }
public double InnerDiameter { get; set; }
public override void SetPreviewDefaults()
{
OuterDiameter = 10;
InnerDiameter = 6;
}
public override Drawing GetDrawing()
{
var entities = new List<Entity>

View File

@@ -10,6 +10,13 @@ namespace OpenNest.Shapes
public double Width { get; set; }
public double Radius { get; set; }
public override void SetPreviewDefaults()
{
Length = 12;
Width = 6;
Radius = 1;
}
public override Drawing GetDrawing()
{
var r = Radius;

View File

@@ -26,6 +26,8 @@ namespace OpenNest.Shapes
public abstract Drawing GetDrawing();
public virtual void SetPreviewDefaults() { }
public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition
{
var json = File.ReadAllText(path);

View File

@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
public double StemWidth { get; set; }
public double BarHeight { get; set; }
public override void SetPreviewDefaults()
{
Width = 10;
Height = 8;
StemWidth = 3;
BarHeight = 3;
}
public override Drawing GetDrawing()
{
var sw = StemWidth > 0 ? StemWidth : Width / 3.0;

View File

@@ -9,6 +9,13 @@ namespace OpenNest.Shapes
public double BottomWidth { get; set; }
public double Height { get; set; }
public override void SetPreviewDefaults()
{
TopWidth = 6;
BottomWidth = 10;
Height = 6;
}
public override Drawing GetDrawing()
{
var offset = (BottomWidth - TopWidth) / 2.0;

84
OpenNest/ArchUnits.cs Normal file
View File

@@ -0,0 +1,84 @@
using OpenNest.IO.Bom;
using System;
using System.Drawing;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace OpenNest
{
public static class ArchUnits
{
private static readonly Regex UnitRegex =
new Regex("^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$");
public static double ParseToInches(string input)
{
if (string.IsNullOrWhiteSpace(input))
return 0;
var sb = new StringBuilder(input.Trim().ToLower());
sb.Replace("ft", "'");
sb.Replace("feet", "'");
sb.Replace("foot", "'");
sb.Replace("inches", "\"");
sb.Replace("inch", "\"");
sb.Replace("in", "\"");
input = Fraction.ReplaceFractionsWithDecimals(sb.ToString());
var match = UnitRegex.Match(input);
if (!match.Success)
{
if (!input.Contains("'") && !input.Contains("\""))
{
if (double.TryParse(input.Trim(), out var plainInches))
return System.Math.Round(plainInches, 8);
}
throw new FormatException("Input is not in a valid format.");
}
var feet = match.Groups["Feet"];
var inches = match.Groups["Inches"];
var totalInches = 0.0;
if (feet.Success)
{
var x = double.Parse(feet.Value.Remove(feet.Length - 1));
totalInches += x * 12;
}
if (inches.Success)
{
var x = double.Parse(inches.Value.Remove(inches.Length - 1));
totalInches += x;
}
return System.Math.Round(totalInches, 8);
}
public static double GetLengthInches(TextBox tb)
{
try
{
if (double.TryParse(tb.Text, out var d))
{
tb.ForeColor = SystemColors.WindowText;
return d;
}
var x = ParseToInches(tb.Text);
tb.ForeColor = SystemColors.WindowText;
return x;
}
catch
{
tb.ForeColor = Color.Red;
return double.NaN;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace OpenNest.Controls
{
public class ShapePreviewControl : PlateView
{
private string[] infoLines;
public ShapePreviewControl()
{
DrawOrigin = false;
DrawBounds = false;
AllowPan = false;
AllowSelect = false;
AllowZoom = false;
AllowDrop = false;
BackColor = Color.White;
}
public void SetInfo(params string[] lines)
{
infoLines = lines;
Invalidate();
}
public void ShowDrawing(Drawing drawing)
{
Plate.Parts.Clear();
Plate.Size = new Geometry.Size(0, 0);
if (drawing?.Program != null)
{
AddPartFromDrawing(drawing, Geometry.Vector.Zero);
ZoomToFit();
}
else
{
Invalidate();
}
}
protected override void OnResize(System.EventArgs e)
{
base.OnResize(e);
if (Plate.Parts.Count > 0)
ZoomToFit(false);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
e.Graphics.TranslateTransform(origin.X, origin.Y);
Renderer.DrawPlate(e.Graphics);
Renderer.DrawParts(e.Graphics);
e.Graphics.ResetTransform();
PaintInfo(e.Graphics);
}
private void PaintInfo(Graphics g)
{
if (infoLines == null) return;
var lineHeight = Font.GetHeight(g) + 1;
var y = 4f;
foreach (var line in infoLines)
{
if (string.IsNullOrEmpty(line)) continue;
g.DrawString(line, Font, Brushes.Black, 4, y);
y += lineHeight;
}
}
}
}

View File

@@ -85,6 +85,7 @@
mnuNest = new System.Windows.Forms.ToolStripMenuItem();
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem();
mnuNestShapeLibrary = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem7 = new System.Windows.Forms.ToolStripSeparator();
mnuNestFirstPlate = new System.Windows.Forms.ToolStripMenuItem();
mnuNestLastPlate = new System.Windows.Forms.ToolStripMenuItem();
@@ -559,7 +560,7 @@
//
// mnuNest
//
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
mnuNest.Name = "mnuNest";
mnuNest.Size = new System.Drawing.Size(43, 20);
mnuNest.Text = "&Nest";
@@ -578,7 +579,14 @@
mnuNestImportDrawing.Size = new System.Drawing.Size(205, 22);
mnuNestImportDrawing.Text = "Import Drawing";
mnuNestImportDrawing.Click += Import_Click;
//
//
// mnuNestShapeLibrary
//
mnuNestShapeLibrary.Name = "mnuNestShapeLibrary";
mnuNestShapeLibrary.Size = new System.Drawing.Size(205, 22);
mnuNestShapeLibrary.Text = "Shape Library";
mnuNestShapeLibrary.Click += ShapeLibrary_Click;
//
// toolStripMenuItem7
//
toolStripMenuItem7.Name = "toolStripMenuItem7";
@@ -1213,6 +1221,7 @@
private System.Windows.Forms.ToolStripMenuItem mnuNest;
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing;
private System.Windows.Forms.ToolStripMenuItem mnuNestShapeLibrary;
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem7;
private System.Windows.Forms.ToolStripMenuItem mnuNestFirstPlate;
private System.Windows.Forms.ToolStripMenuItem mnuNestLastPlate;

View File

@@ -829,6 +829,20 @@ namespace OpenNest.Forms
activeForm.Import();
}
private void ShapeLibrary_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
var form = new ShapeLibraryForm();
form.ShowDialog();
var drawings = form.GetDrawings();
if (drawings.Count == 0) return;
drawings.ForEach(d => activeForm.Nest.Drawings.Add(d));
activeForm.UpdateDrawingList();
}
private void EditNest_Click(object sender, EventArgs e)
{
if (activeForm == null) return;

View File

@@ -0,0 +1,338 @@
namespace OpenNest.Forms
{
partial class ShapeLibraryForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
ColorScheme colorScheme1 = new ColorScheme();
CutOffSettings cutOffSettings1 = new CutOffSettings();
Plate plate1 = new Plate();
Collections.ObservableList<CutOff> observableList_11 = new Collections.ObservableList<CutOff>();
Collections.ObservableList<Part> observableList_12 = new Collections.ObservableList<Part>();
splitContainer = new System.Windows.Forms.SplitContainer();
shapeListBox = new System.Windows.Forms.ListBox();
layoutTable = new System.Windows.Forms.TableLayoutPanel();
fieldsTable = new System.Windows.Forms.TableLayoutPanel();
nameLabel = new System.Windows.Forms.Label();
nameTextBox = new System.Windows.Forms.TextBox();
qtyLabel = new System.Windows.Forms.Label();
quantityUpDown = new OpenNest.Controls.NumericUpDown();
configLabel = new System.Windows.Forms.Label();
configComboBox = new System.Windows.Forms.ComboBox();
contentPanel = new System.Windows.Forms.Panel();
previewBox = new OpenNest.Controls.ShapePreviewControl();
parametersPanel = new System.Windows.Forms.Panel();
buttonPanel = new System.Windows.Forms.Panel();
addButton = new System.Windows.Forms.Button();
closeButton = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
splitContainer.Panel1.SuspendLayout();
splitContainer.Panel2.SuspendLayout();
splitContainer.SuspendLayout();
layoutTable.SuspendLayout();
fieldsTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)quantityUpDown).BeginInit();
contentPanel.SuspendLayout();
buttonPanel.SuspendLayout();
SuspendLayout();
//
// splitContainer
//
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
splitContainer.Location = new System.Drawing.Point(0, 0);
splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
splitContainer.Panel1.Controls.Add(shapeListBox);
//
// splitContainer.Panel2
//
splitContainer.Panel2.Controls.Add(layoutTable);
splitContainer.Size = new System.Drawing.Size(750, 520);
splitContainer.SplitterDistance = 150;
splitContainer.TabIndex = 0;
//
// shapeListBox
//
shapeListBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
shapeListBox.Dock = System.Windows.Forms.DockStyle.Fill;
shapeListBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed;
shapeListBox.Font = new System.Drawing.Font("Segoe UI", 10F);
shapeListBox.IntegralHeight = false;
shapeListBox.ItemHeight = 32;
shapeListBox.Location = new System.Drawing.Point(0, 0);
shapeListBox.Name = "shapeListBox";
shapeListBox.Size = new System.Drawing.Size(150, 520);
shapeListBox.TabIndex = 0;
//
// layoutTable
//
layoutTable.ColumnCount = 1;
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.Controls.Add(fieldsTable, 0, 0);
layoutTable.Controls.Add(contentPanel, 0, 1);
layoutTable.Controls.Add(buttonPanel, 0, 2);
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(6, 4, 6, 0);
layoutTable.RowCount = 3;
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 44F));
layoutTable.Size = new System.Drawing.Size(596, 520);
layoutTable.TabIndex = 0;
//
// fieldsTable
//
fieldsTable.AutoSize = true;
fieldsTable.ColumnCount = 2;
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
fieldsTable.Controls.Add(nameLabel, 0, 0);
fieldsTable.Controls.Add(nameTextBox, 1, 0);
fieldsTable.Controls.Add(qtyLabel, 0, 1);
fieldsTable.Controls.Add(quantityUpDown, 1, 1);
fieldsTable.Controls.Add(configLabel, 0, 2);
fieldsTable.Controls.Add(configComboBox, 1, 2);
fieldsTable.Dock = System.Windows.Forms.DockStyle.Fill;
fieldsTable.Location = new System.Drawing.Point(6, 4);
fieldsTable.Margin = new System.Windows.Forms.Padding(0, 0, 0, 4);
fieldsTable.Name = "fieldsTable";
fieldsTable.RowCount = 3;
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
fieldsTable.Size = new System.Drawing.Size(584, 97);
fieldsTable.TabIndex = 0;
//
// nameLabel
//
nameLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
nameLabel.AutoSize = true;
nameLabel.Location = new System.Drawing.Point(4, 8);
nameLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
nameLabel.Name = "nameLabel";
nameLabel.Size = new System.Drawing.Size(46, 17);
nameLabel.TabIndex = 0;
nameLabel.Text = "Name:";
//
// nameTextBox
//
nameTextBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
nameTextBox.Location = new System.Drawing.Point(102, 4);
nameTextBox.Margin = new System.Windows.Forms.Padding(0, 4, 4, 4);
nameTextBox.Name = "nameTextBox";
nameTextBox.Size = new System.Drawing.Size(478, 25);
nameTextBox.TabIndex = 1;
//
// qtyLabel
//
qtyLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
qtyLabel.AutoSize = true;
qtyLabel.Location = new System.Drawing.Point(4, 41);
qtyLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
qtyLabel.Name = "qtyLabel";
qtyLabel.Size = new System.Drawing.Size(59, 17);
qtyLabel.TabIndex = 2;
qtyLabel.Text = "Quantity:";
//
// quantityUpDown
//
quantityUpDown.Location = new System.Drawing.Point(102, 37);
quantityUpDown.Margin = new System.Windows.Forms.Padding(0, 4, 4, 4);
quantityUpDown.Maximum = new decimal(new int[] { 999999, 0, 0, 0 });
quantityUpDown.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
quantityUpDown.Name = "quantityUpDown";
quantityUpDown.Size = new System.Drawing.Size(100, 25);
quantityUpDown.Suffix = "";
quantityUpDown.TabIndex = 2;
quantityUpDown.Value = new decimal(new int[] { 1, 0, 0, 0 });
//
// configLabel
//
configLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
configLabel.AutoSize = true;
configLabel.Location = new System.Drawing.Point(4, 73);
configLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
configLabel.Name = "configLabel";
configLabel.Size = new System.Drawing.Size(90, 17);
configLabel.TabIndex = 3;
configLabel.Text = "Configuration:";
configLabel.Visible = false;
//
// configComboBox
//
configComboBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
configComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
configComboBox.Location = new System.Drawing.Point(102, 70);
configComboBox.Margin = new System.Windows.Forms.Padding(0, 4, 4, 4);
configComboBox.Name = "configComboBox";
configComboBox.Size = new System.Drawing.Size(478, 25);
configComboBox.TabIndex = 3;
configComboBox.Visible = false;
//
// contentPanel
//
contentPanel.Controls.Add(previewBox);
contentPanel.Controls.Add(parametersPanel);
contentPanel.Dock = System.Windows.Forms.DockStyle.Fill;
contentPanel.Location = new System.Drawing.Point(9, 108);
contentPanel.Name = "contentPanel";
contentPanel.Size = new System.Drawing.Size(578, 365);
contentPanel.TabIndex = 1;
//
// previewBox
//
previewBox.ActiveWorkArea = null;
previewBox.AllowPan = false;
previewBox.AllowSelect = false;
previewBox.AllowZoom = false;
previewBox.BackColor = System.Drawing.Color.White;
colorScheme1.BackgroundColor = System.Drawing.Color.DarkGray;
colorScheme1.BoundingBoxColor = System.Drawing.Color.FromArgb(128, 128, 255);
colorScheme1.EdgeSpacingColor = System.Drawing.Color.FromArgb(180, 180, 180);
colorScheme1.LayoutFillColor = System.Drawing.Color.WhiteSmoke;
colorScheme1.LayoutOutlineColor = System.Drawing.Color.Gray;
colorScheme1.OriginColor = System.Drawing.Color.Gray;
colorScheme1.PreviewPartColor = System.Drawing.Color.FromArgb(255, 140, 0);
colorScheme1.RapidColor = System.Drawing.Color.DodgerBlue;
previewBox.ColorScheme = colorScheme1;
cutOffSettings1.CutDirection = CutDirection.AwayFromOrigin;
cutOffSettings1.MinSegmentLength = 0.05D;
cutOffSettings1.Overtravel = 0D;
cutOffSettings1.PartClearance = 0.02D;
previewBox.CutOffSettings = cutOffSettings1;
previewBox.DebugRemnantPriorities = null;
previewBox.DebugRemnants = null;
previewBox.Dock = System.Windows.Forms.DockStyle.Fill;
previewBox.DrawBounds = false;
previewBox.DrawCutDirection = false;
previewBox.DrawOffset = false;
previewBox.DrawOrigin = false;
previewBox.DrawPiercePoints = false;
previewBox.DrawRapid = false;
previewBox.FillParts = true;
previewBox.Location = new System.Drawing.Point(0, 0);
previewBox.Name = "previewBox";
previewBox.OffsetIncrementDistance = 10D;
previewBox.OffsetTolerance = 0.001D;
plate1.CutOffs = observableList_11;
plate1.CuttingParameters = null;
plate1.GrainAngle = 0D;
plate1.Parts = observableList_12;
plate1.PartSpacing = 0D;
plate1.Quadrant = 1;
plate1.Quantity = 0;
previewBox.Plate = plate1;
previewBox.RotateIncrementAngle = 10D;
previewBox.ShowBendLines = false;
previewBox.Size = new System.Drawing.Size(318, 365);
previewBox.Status = "Select";
previewBox.TabIndex = 4;
previewBox.TabStop = false;
//
// parametersPanel
//
parametersPanel.AutoScroll = true;
parametersPanel.Dock = System.Windows.Forms.DockStyle.Right;
parametersPanel.Location = new System.Drawing.Point(318, 0);
parametersPanel.Name = "parametersPanel";
parametersPanel.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
parametersPanel.Size = new System.Drawing.Size(260, 365);
parametersPanel.TabIndex = 5;
//
// buttonPanel
//
buttonPanel.Controls.Add(addButton);
buttonPanel.Controls.Add(closeButton);
buttonPanel.Dock = System.Windows.Forms.DockStyle.Fill;
buttonPanel.Location = new System.Drawing.Point(9, 479);
buttonPanel.Name = "buttonPanel";
buttonPanel.Size = new System.Drawing.Size(578, 38);
buttonPanel.TabIndex = 2;
//
// addButton
//
addButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
addButton.Location = new System.Drawing.Point(379, 5);
addButton.Name = "addButton";
addButton.Size = new System.Drawing.Size(100, 30);
addButton.TabIndex = 0;
addButton.Text = "Add to Nest";
addButton.UseVisualStyleBackColor = true;
//
// closeButton
//
closeButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
closeButton.Location = new System.Drawing.Point(485, 5);
closeButton.Name = "closeButton";
closeButton.Size = new System.Drawing.Size(90, 30);
closeButton.TabIndex = 1;
closeButton.Text = "Close";
closeButton.UseVisualStyleBackColor = true;
//
// ShapeLibraryForm
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
CancelButton = closeButton;
ClientSize = new System.Drawing.Size(750, 520);
Controls.Add(splitContainer);
Font = new System.Drawing.Font("Segoe UI", 9.75F);
MinimizeBox = false;
MinimumSize = new System.Drawing.Size(600, 400);
Name = "ShapeLibraryForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Shape Library";
splitContainer.Panel1.ResumeLayout(false);
splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
splitContainer.ResumeLayout(false);
layoutTable.ResumeLayout(false);
layoutTable.PerformLayout();
fieldsTable.ResumeLayout(false);
fieldsTable.PerformLayout();
((System.ComponentModel.ISupportInitialize)quantityUpDown).EndInit();
contentPanel.ResumeLayout(false);
buttonPanel.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.SplitContainer splitContainer;
private System.Windows.Forms.ListBox shapeListBox;
private System.Windows.Forms.TableLayoutPanel layoutTable;
private System.Windows.Forms.TableLayoutPanel fieldsTable;
private System.Windows.Forms.Label nameLabel;
private System.Windows.Forms.TextBox nameTextBox;
private System.Windows.Forms.Label qtyLabel;
private Controls.NumericUpDown quantityUpDown;
private System.Windows.Forms.Label configLabel;
private System.Windows.Forms.ComboBox configComboBox;
private System.Windows.Forms.Panel contentPanel;
private Controls.ShapePreviewControl previewBox;
private System.Windows.Forms.Panel parametersPanel;
private System.Windows.Forms.Panel buttonPanel;
private System.Windows.Forms.Button addButton;
private System.Windows.Forms.Button closeButton;
}
}

View File

@@ -0,0 +1,322 @@
using OpenNest.Shapes;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace OpenNest.Forms
{
public partial class ShapeLibraryForm : Form
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
private readonly List<Drawing> addedDrawings = new List<Drawing>();
private readonly List<ShapeEntry> shapeEntries = new List<ShapeEntry>();
private readonly List<ParameterBinding> parameterBindings = new List<ParameterBinding>();
private ShapeEntry selectedEntry;
private bool suppressPreview;
public ShapeLibraryForm()
{
InitializeComponent();
DiscoverShapes();
PopulateShapeList();
shapeListBox.DrawItem += ShapeListBox_DrawItem;
shapeListBox.SelectedIndexChanged += ShapeListBox_SelectedIndexChanged;
configComboBox.SelectedIndexChanged += ConfigComboBox_SelectedIndexChanged;
addButton.Click += AddButton_Click;
closeButton.Click += (s, e) => Close();
if (shapeListBox.Items.Count > 0)
shapeListBox.SelectedIndex = 0;
}
public List<Drawing> GetDrawings() => addedDrawings;
private void DiscoverShapes()
{
var baseType = typeof(ShapeDefinition);
var shapeTypes = baseType.Assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t))
.OrderBy(t => t.Name)
.ToList();
var configDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configurations");
foreach (var type in shapeTypes)
{
var entry = new ShapeEntry { ShapeType = type };
entry.DisplayName = FriendlyName(type.Name);
var configPath = Path.Combine(configDir, type.Name + ".json");
if (File.Exists(configPath))
entry.Configurations = LoadConfigurations(type, configPath);
shapeEntries.Add(entry);
}
}
private List<ShapeDefinition> LoadConfigurations(Type shapeType, string path)
{
try
{
var json = File.ReadAllText(path);
var listType = typeof(List<>).MakeGenericType(shapeType);
var list = JsonSerializer.Deserialize(json, listType, JsonOptions);
return ((System.Collections.IEnumerable)list).Cast<ShapeDefinition>().ToList();
}
catch
{
return null;
}
}
private void PopulateShapeList()
{
foreach (var entry in shapeEntries)
shapeListBox.Items.Add(entry);
}
private void ShapeListBox_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0) return;
e.DrawBackground();
var entry = (ShapeEntry)shapeListBox.Items[e.Index];
var textColor = (e.State & DrawItemState.Selected) != 0
? SystemColors.HighlightText
: SystemColors.ControlText;
var text = entry.DisplayName;
if (entry.HasConfigurations)
text += $" ({entry.Configurations.Count})";
using (var brush = new SolidBrush(textColor))
{
var format = new StringFormat { LineAlignment = StringAlignment.Center };
var rect = new RectangleF(8, e.Bounds.Y, e.Bounds.Width - 8, e.Bounds.Height);
e.Graphics.DrawString(text, e.Font, brush, rect, format);
}
e.DrawFocusRectangle();
}
private void ShapeListBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (shapeListBox.SelectedIndex < 0) return;
selectedEntry = (ShapeEntry)shapeListBox.SelectedItem;
suppressPreview = true;
var hasConfigs = selectedEntry.HasConfigurations;
configLabel.Visible = hasConfigs;
configComboBox.Visible = hasConfigs;
if (hasConfigs)
{
configComboBox.Items.Clear();
foreach (var cfg in selectedEntry.Configurations)
configComboBox.Items.Add(cfg.Name);
configComboBox.SelectedIndex = 0;
}
else
{
nameTextBox.Text = selectedEntry.DisplayName;
var defaults = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
defaults.SetPreviewDefaults();
BuildParameterControls(selectedEntry.ShapeType, defaults);
}
suppressPreview = false;
UpdatePreview();
}
private void ConfigComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (configComboBox.SelectedIndex < 0 || selectedEntry == null) return;
var config = selectedEntry.Configurations[configComboBox.SelectedIndex];
nameTextBox.Text = config.Name;
suppressPreview = true;
BuildParameterControls(selectedEntry.ShapeType, config);
suppressPreview = false;
UpdatePreview();
}
private void BuildParameterControls(Type shapeType, ShapeDefinition sourceValues)
{
parametersPanel.SuspendLayout();
parametersPanel.Controls.Clear();
parameterBindings.Clear();
var props = shapeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(p => p.CanRead && p.CanWrite && p.Name != "Name")
.ToArray();
var panelWidth = parametersPanel.ClientSize.Width - parametersPanel.Padding.Horizontal;
var y = 4;
foreach (var prop in props)
{
var label = new Label
{
Text = FriendlyName(prop.Name),
Location = new Point(parametersPanel.Padding.Left, y),
AutoSize = true
};
y += 18;
var tb = new TextBox
{
Location = new Point(parametersPanel.Padding.Left, y),
Width = panelWidth,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
};
if (sourceValues != null)
{
if (prop.PropertyType == typeof(int))
tb.Text = ((int)prop.GetValue(sourceValues)).ToString();
else
tb.Text = ((double)prop.GetValue(sourceValues)).ToString("G");
}
tb.TextChanged += (s, ev) => UpdatePreview();
parameterBindings.Add(new ParameterBinding { Property = prop, Control = tb });
parametersPanel.Controls.Add(label);
parametersPanel.Controls.Add(tb);
y += 30;
}
parametersPanel.ResumeLayout(true);
}
private void UpdatePreview()
{
if (suppressPreview || selectedEntry == null) return;
try
{
var shape = CreateShapeFromInputs();
if (shape == null) return;
var drawing = shape.GetDrawing();
previewBox.ShowDrawing(drawing);
if (drawing?.Program != null)
{
var bb = drawing.Program.BoundingBox();
previewBox.SetInfo(
nameTextBox.Text,
string.Format("{0:F3} x {1:F3}", bb.Size.Length, bb.Size.Width));
}
}
catch
{
previewBox.ShowDrawing(null);
}
}
private ShapeDefinition CreateShapeFromInputs()
{
var shape = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
shape.Name = nameTextBox.Text;
foreach (var binding in parameterBindings)
{
var tb = (TextBox)binding.Control;
if (binding.Property.PropertyType == typeof(int))
{
if (int.TryParse(tb.Text, out var intVal))
{
binding.Property.SetValue(shape, intVal);
tb.ForeColor = SystemColors.WindowText;
}
else
{
tb.ForeColor = Color.Red;
return null;
}
}
else
{
var val = ArchUnits.GetLengthInches(tb);
if (double.IsNaN(val))
return null;
binding.Property.SetValue(shape, val);
}
}
return shape;
}
private void AddButton_Click(object sender, EventArgs e)
{
try
{
var shape = CreateShapeFromInputs();
if (shape == null) return;
var drawing = shape.GetDrawing();
drawing.Color = Drawing.GetNextColor();
drawing.Quantity.Required = (int)quantityUpDown.Value;
addedDrawings.Add(drawing);
DialogResult = DialogResult.OK;
addButton.Text = $"Added ({addedDrawings.Count})";
}
catch (Exception ex)
{
MessageBox.Show(
$"Failed to create shape: {ex.Message}",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
private static string FriendlyName(string name)
{
if (name.EndsWith("Shape"))
name = name.Substring(0, name.Length - 5);
return Regex.Replace(name, @"(?<=[a-z0-9])([A-Z])", " $1");
}
private class ShapeEntry
{
public Type ShapeType { get; set; }
public string DisplayName { get; set; }
public List<ShapeDefinition> Configurations { get; set; }
public bool HasConfigurations => Configurations != null && Configurations.Count > 0;
public override string ToString() => DisplayName;
}
private class ParameterBinding
{
public PropertyInfo Property { get; set; }
public Control Control { get; set; }
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -10,6 +10,11 @@
<ItemGroup>
<Compile Remove="Controls\LayoutViewGL.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Configurations\**\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />