feat: add owner-drawn color swatch to FilterPanel

Switch colorsList from CheckedListBox (which silently ignores owner
draw) to a plain ListBox with manual checkbox, color swatch, and hex
label rendering. Clone entities in ProgramEditorControl preview to
avoid mutating originals. Remove contour color application from
CadConverterForm. Fix struct null comparison warning in SplitDrawingForm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:24:28 -04:00
parent 9f76659d5d
commit 7a6c407edd
5 changed files with 136 additions and 136 deletions

View File

@@ -16,7 +16,7 @@ namespace OpenNest.Controls
private readonly CollapsiblePanel bendLinesPanel;
private readonly CheckedListBox layersList;
private readonly CheckedListBox colorsList;
private readonly ListBox colorsList;
private readonly CheckedListBox lineTypesList;
private readonly ListBox bendLinesList;
private readonly LinkLabel bendAddLink;
@@ -91,7 +91,7 @@ namespace OpenNest.Controls
HeaderText = "Line Types (0)",
Dock = DockStyle.Top,
ExpandedHeight = 100,
IsExpanded = false
IsExpanded = true
};
lineTypesList = CreateCheckedList();
lineTypesPanel.ContentPanel.Controls.Add(lineTypesList);
@@ -102,12 +102,19 @@ namespace OpenNest.Controls
HeaderText = "Colors (0)",
Dock = DockStyle.Top,
ExpandedHeight = 100,
IsExpanded = false
IsExpanded = true
};
colorsList = new ListBox
{
Dock = DockStyle.Fill,
BorderStyle = BorderStyle.None,
Font = new Font("Segoe UI", 9f),
DrawMode = DrawMode.OwnerDrawFixed,
ItemHeight = 20,
SelectionMode = SelectionMode.None
};
colorsList = CreateCheckedList();
colorsList.DrawMode = DrawMode.OwnerDrawFixed;
colorsList.ItemHeight = 20;
colorsList.DrawItem += ColorsList_DrawItem;
colorsList.MouseClick += ColorsList_MouseClick;
colorsPanel.ContentPanel.Controls.Add(colorsList);
// Layers (always expanded)
@@ -174,7 +181,7 @@ namespace OpenNest.Controls
.Distinct()
.Select(argb => new ColorItem(Color.FromArgb(argb)));
foreach (var color in colors)
colorsList.Items.Add(color, true); // checked = visible
colorsList.Items.Add(color);
colorsPanel.HeaderText = $"Colors ({colorsList.Items.Count})";
@@ -213,8 +220,9 @@ namespace OpenNest.Controls
var hiddenColors = new HashSet<int>();
for (var i = 0; i < colorsList.Items.Count; i++)
{
if (!colorsList.GetItemChecked(i))
hiddenColors.Add(((ColorItem)colorsList.Items[i]).Argb);
var item = (ColorItem)colorsList.Items[i];
if (!item.IsChecked)
hiddenColors.Add(item.Argb);
}
var hiddenLineTypes = new HashSet<string>();
@@ -242,20 +250,39 @@ namespace OpenNest.Controls
list.SetItemChecked(i, isChecked);
}
private void ColorsList_MouseClick(object sender, MouseEventArgs e)
{
var index = colorsList.IndexFromPoint(e.Location);
if (index < 0) return;
var item = (ColorItem)colorsList.Items[index];
item.IsChecked = !item.IsChecked;
colorsList.Invalidate(colorsList.GetItemRectangle(index));
FilterChanged?.Invoke(this, EventArgs.Empty);
}
private void ColorsList_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0) return;
e.DrawBackground();
e.Graphics.FillRectangle(Brushes.White, e.Bounds);
var colorItem = (ColorItem)colorsList.Items[e.Index];
var swatchRect = new Rectangle(e.Bounds.Left + 20, e.Bounds.Top + 2, 16, e.Bounds.Height - 4);
var checkSize = CheckBoxRenderer.GetGlyphSize(e.Graphics,
System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal);
var checkY = e.Bounds.Top + (e.Bounds.Height - checkSize.Height) / 2;
var checkState = colorItem.IsChecked
? System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal
: System.Windows.Forms.VisualStyles.CheckBoxState.UncheckedNormal;
CheckBoxRenderer.DrawCheckBox(e.Graphics, new Point(e.Bounds.Left + 2, checkY), checkState);
var swatchX = e.Bounds.Left + checkSize.Width + 6;
var swatchRect = new Rectangle(swatchX, e.Bounds.Top + 2, 16, e.Bounds.Height - 4);
using (var brush = new SolidBrush(colorItem.Color))
e.Graphics.FillRectangle(brush, swatchRect);
e.Graphics.DrawRectangle(Pens.Gray, swatchRect);
e.DrawFocusRectangle();
TextRenderer.DrawText(e.Graphics, colorItem.ToString(), e.Font,
new Point(swatchRect.Right + 4, e.Bounds.Top + 1), SystemColors.WindowText);
}
public void SetPickMode(bool active)
@@ -269,6 +296,7 @@ namespace OpenNest.Controls
{
public int Argb { get; }
public Color Color { get; }
public bool IsChecked { get; set; } = true;
public ColorItem(Color color)
{

View File

@@ -50,14 +50,6 @@ namespace OpenNest.Controls
contours = ContourInfo.Classify(shapes);
// Assign contour-type colors once so the CAD view also picks them up
foreach (var contour in contours)
{
var color = GetContourColor(contour.Type, false);
foreach (var entity in contour.Shape.Entities)
entity.Color = color;
}
Program = BuildProgram(contours);
isDirty = false;
isLoaded = true;
@@ -144,33 +136,38 @@ namespace OpenNest.Controls
preview.ClearPenCache();
preview.Entities.Clear();
// Restore base colors first (undo any selection highlight)
foreach (var contour in contours)
{
var baseColor = GetContourColor(contour.Type, false);
foreach (var entity in contour.Shape.Entities)
entity.Color = baseColor;
}
for (var i = 0; i < contours.Count; i++)
{
var contour = contours[i];
var selected = contourList.SelectedIndices.Contains(i);
var color = GetContourColor(contour.Type, selected);
if (selected)
foreach (var entity in contour.Shape.Entities)
{
var selColor = GetContourColor(contour.Type, true);
foreach (var entity in contour.Shape.Entities)
entity.Color = selColor;
var clone = CloneEntity(entity, color);
if (clone != null)
preview.Entities.Add(clone);
}
preview.Entities.AddRange(contour.Shape.Entities);
}
preview.ZoomToFit();
preview.Invalidate();
}
private static Entity CloneEntity(Entity entity, Color color)
{
Entity clone = entity switch
{
Line line => new Line(line.StartPoint, line.EndPoint) { Layer = line.Layer, IsVisible = line.IsVisible },
Arc arc => new Arc(arc.Center, arc.Radius, arc.StartAngle, arc.EndAngle, arc.IsReversed) { Layer = arc.Layer, IsVisible = arc.IsVisible },
Circle circle => new Circle(circle.Center, circle.Radius) { Layer = circle.Layer, IsVisible = circle.IsVisible },
_ => null,
};
if (clone != null)
clone.Color = color;
return clone;
}
private static Color GetContourColor(ContourClassification type, bool selected)
{
if (selected)

View File

@@ -17,14 +17,12 @@ namespace OpenNest.Forms
{
mainSplit = new System.Windows.Forms.SplitContainer();
fileList = new OpenNest.Controls.FileListControl();
viewTabs = new System.Windows.Forms.TabControl();
tabCadView = new System.Windows.Forms.TabPage();
cadViewSplit = new System.Windows.Forms.SplitContainer();
filterPanel = new OpenNest.Controls.FilterPanel();
entityView1 = new OpenNest.Controls.EntityView();
detailBar = new System.Windows.Forms.FlowLayoutPanel();
viewTabs = new System.Windows.Forms.TabControl();
tabCadView = new System.Windows.Forms.TabPage();
tabProgram = new System.Windows.Forms.TabPage();
programEditor = new OpenNest.Controls.ProgramEditorControl();
lblQty = new System.Windows.Forms.Label();
numQuantity = new System.Windows.Forms.NumericUpDown();
lblCust = new System.Windows.Forms.Label();
@@ -38,6 +36,8 @@ namespace OpenNest.Forms
chkLabels = new System.Windows.Forms.CheckBox();
lblDetect = new System.Windows.Forms.Label();
cboBendDetector = new System.Windows.Forms.ComboBox();
tabProgram = new System.Windows.Forms.TabPage();
programEditor = new OpenNest.Controls.ProgramEditorControl();
bottomPanel1 = new OpenNest.Controls.BottomPanel();
cancelButton = new System.Windows.Forms.Button();
acceptButton = new System.Windows.Forms.Button();
@@ -45,40 +45,40 @@ namespace OpenNest.Forms
mainSplit.Panel1.SuspendLayout();
mainSplit.Panel2.SuspendLayout();
mainSplit.SuspendLayout();
viewTabs.SuspendLayout();
tabCadView.SuspendLayout();
((System.ComponentModel.ISupportInitialize)cadViewSplit).BeginInit();
cadViewSplit.Panel1.SuspendLayout();
cadViewSplit.Panel2.SuspendLayout();
cadViewSplit.SuspendLayout();
detailBar.SuspendLayout();
viewTabs.SuspendLayout();
tabCadView.SuspendLayout();
tabProgram.SuspendLayout();
((System.ComponentModel.ISupportInitialize)numQuantity).BeginInit();
tabProgram.SuspendLayout();
bottomPanel1.SuspendLayout();
SuspendLayout();
//
//
// mainSplit
//
//
mainSplit.Dock = System.Windows.Forms.DockStyle.Fill;
mainSplit.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
mainSplit.Location = new System.Drawing.Point(0, 0);
mainSplit.Name = "mainSplit";
//
//
// mainSplit.Panel1
//
//
mainSplit.Panel1.Controls.Add(fileList);
mainSplit.Panel1MinSize = 200;
//
//
// mainSplit.Panel2
//
//
mainSplit.Panel2.Controls.Add(viewTabs);
mainSplit.Size = new System.Drawing.Size(1024, 670);
mainSplit.SplitterDistance = 260;
mainSplit.SplitterWidth = 5;
mainSplit.TabIndex = 2;
//
//
// fileList
//
//
fileList.AllowDrop = true;
fileList.BackColor = System.Drawing.Color.White;
fileList.Dock = System.Windows.Forms.DockStyle.Fill;
@@ -87,30 +87,51 @@ namespace OpenNest.Forms
fileList.Name = "fileList";
fileList.Size = new System.Drawing.Size(260, 670);
fileList.TabIndex = 0;
//
//
// viewTabs
//
viewTabs.Controls.Add(tabCadView);
viewTabs.Controls.Add(tabProgram);
viewTabs.Dock = System.Windows.Forms.DockStyle.Fill;
viewTabs.Location = new System.Drawing.Point(0, 0);
viewTabs.Name = "viewTabs";
viewTabs.SelectedIndex = 0;
viewTabs.Size = new System.Drawing.Size(759, 670);
viewTabs.TabIndex = 0;
//
// tabCadView
//
tabCadView.Controls.Add(cadViewSplit);
tabCadView.Location = new System.Drawing.Point(4, 24);
tabCadView.Name = "tabCadView";
tabCadView.Size = new System.Drawing.Size(751, 642);
tabCadView.TabIndex = 0;
tabCadView.Text = "CAD View";
tabCadView.UseVisualStyleBackColor = true;
//
// cadViewSplit
//
//
cadViewSplit.Dock = System.Windows.Forms.DockStyle.Fill;
cadViewSplit.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
cadViewSplit.Location = new System.Drawing.Point(0, 0);
cadViewSplit.Name = "cadViewSplit";
//
// cadViewSplit.Panel1 — filter panel
//
//
// cadViewSplit.Panel1
//
cadViewSplit.Panel1.Controls.Add(filterPanel);
cadViewSplit.Panel1MinSize = 150;
//
// cadViewSplit.Panel2 — entity view + detail bar
//
//
// cadViewSplit.Panel2
//
cadViewSplit.Panel2.Controls.Add(entityView1);
cadViewSplit.Panel2.Controls.Add(detailBar);
cadViewSplit.Size = new System.Drawing.Size(751, 642);
cadViewSplit.SplitterDistance = 200;
cadViewSplit.SplitterWidth = 5;
cadViewSplit.TabIndex = 0;
//
//
// filterPanel
//
//
filterPanel.AutoScroll = true;
filterPanel.BackColor = System.Drawing.Color.White;
filterPanel.Dock = System.Windows.Forms.DockStyle.Fill;
@@ -118,6 +139,7 @@ namespace OpenNest.Forms
filterPanel.Name = "filterPanel";
filterPanel.Size = new System.Drawing.Size(200, 642);
filterPanel.TabIndex = 0;
filterPanel.Paint += filterPanel_Paint;
//
// entityView1
//
@@ -128,6 +150,7 @@ namespace OpenNest.Forms
entityView1.Location = new System.Drawing.Point(0, 0);
entityView1.Name = "entityView1";
entityView1.OriginalEntities = null;
entityView1.PaintOverlay = null;
entityView1.ShowEntityLabels = false;
entityView1.SimplifierHighlight = null;
entityView1.SimplifierPreview = null;
@@ -153,7 +176,7 @@ namespace OpenNest.Forms
detailBar.Controls.Add(lblDetect);
detailBar.Controls.Add(cboBendDetector);
detailBar.Dock = System.Windows.Forms.DockStyle.Bottom;
detailBar.Location = new System.Drawing.Point(0, 634);
detailBar.Location = new System.Drawing.Point(0, 606);
detailBar.Name = "detailBar";
detailBar.Padding = new System.Windows.Forms.Padding(4, 6, 4, 4);
detailBar.Size = new System.Drawing.Size(546, 36);
@@ -308,6 +331,24 @@ namespace OpenNest.Forms
cboBendDetector.Size = new System.Drawing.Size(90, 23);
cboBendDetector.TabIndex = 8;
//
// tabProgram
//
tabProgram.Controls.Add(programEditor);
tabProgram.Location = new System.Drawing.Point(4, 24);
tabProgram.Name = "tabProgram";
tabProgram.Size = new System.Drawing.Size(751, 642);
tabProgram.TabIndex = 1;
tabProgram.Text = "Program";
tabProgram.UseVisualStyleBackColor = true;
//
// programEditor
//
programEditor.Dock = System.Windows.Forms.DockStyle.Fill;
programEditor.Location = new System.Drawing.Point(0, 0);
programEditor.Name = "programEditor";
programEditor.Size = new System.Drawing.Size(751, 642);
programEditor.TabIndex = 0;
//
// bottomPanel1
//
bottomPanel1.Controls.Add(cancelButton);
@@ -341,50 +382,9 @@ namespace OpenNest.Forms
acceptButton.Size = new System.Drawing.Size(90, 28);
acceptButton.TabIndex = 1;
acceptButton.Text = "Accept";
//
// viewTabs
//
viewTabs.Controls.Add(tabCadView);
viewTabs.Controls.Add(tabProgram);
viewTabs.Dock = System.Windows.Forms.DockStyle.Fill;
viewTabs.Location = new System.Drawing.Point(0, 0);
viewTabs.Name = "viewTabs";
viewTabs.SelectedIndex = 0;
viewTabs.Size = new System.Drawing.Size(759, 670);
viewTabs.TabIndex = 0;
//
// tabCadView
//
tabCadView.Controls.Add(cadViewSplit);
tabCadView.Location = new System.Drawing.Point(4, 24);
tabCadView.Name = "tabCadView";
tabCadView.Padding = new System.Windows.Forms.Padding(0);
tabCadView.Size = new System.Drawing.Size(751, 642);
tabCadView.TabIndex = 0;
tabCadView.Text = "CAD View";
tabCadView.UseVisualStyleBackColor = true;
//
// tabProgram
//
tabProgram.Controls.Add(programEditor);
tabProgram.Location = new System.Drawing.Point(4, 24);
tabProgram.Name = "tabProgram";
tabProgram.Padding = new System.Windows.Forms.Padding(0);
tabProgram.Size = new System.Drawing.Size(751, 642);
tabProgram.TabIndex = 1;
tabProgram.Text = "Program";
tabProgram.UseVisualStyleBackColor = true;
//
// programEditor
//
programEditor.Dock = System.Windows.Forms.DockStyle.Fill;
programEditor.Location = new System.Drawing.Point(0, 0);
programEditor.Name = "programEditor";
programEditor.Size = new System.Drawing.Size(751, 642);
programEditor.TabIndex = 0;
//
//
// CadConverterForm
//
//
AllowDrop = true;
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
ClientSize = new System.Drawing.Size(1024, 720);
@@ -402,6 +402,8 @@ namespace OpenNest.Forms
mainSplit.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)mainSplit).EndInit();
mainSplit.ResumeLayout(false);
viewTabs.ResumeLayout(false);
tabCadView.ResumeLayout(false);
cadViewSplit.Panel1.ResumeLayout(false);
cadViewSplit.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)cadViewSplit).EndInit();
@@ -409,8 +411,6 @@ namespace OpenNest.Forms
detailBar.ResumeLayout(false);
detailBar.PerformLayout();
((System.ComponentModel.ISupportInitialize)numQuantity).EndInit();
viewTabs.ResumeLayout(false);
tabCadView.ResumeLayout(false);
tabProgram.ResumeLayout(false);
bottomPanel1.ResumeLayout(false);
ResumeLayout(false);

View File

@@ -161,8 +161,6 @@ namespace OpenNest.Forms
item.Entities.ForEach(e => e.Layer.IsVisible = true);
ReHidePromotedEntities(item.Bends);
ApplyContourColors(item.Entities);
filterPanel.LoadItem(item.Entities, item.Bends);
numQuantity.Value = item.Quantity;
@@ -178,30 +176,6 @@ namespace OpenNest.Forms
CheckSimplifiable(item);
}
private static void ApplyContourColors(List<Entity> entities)
{
var visible = entities.Where(e => e.IsVisible && e.Layer != null && e.Layer.IsVisible).ToList();
if (visible.Count == 0) return;
var shapes = ShapeBuilder.GetShapes(visible);
if (shapes.Count == 0) return;
var contours = ContourInfo.Classify(shapes);
foreach (var contour in contours)
{
var color = contour.Type switch
{
ContourClassification.Perimeter => System.Drawing.Color.FromArgb(80, 180, 120),
ContourClassification.Hole => System.Drawing.Color.FromArgb(100, 140, 255),
ContourClassification.Etch => System.Drawing.Color.FromArgb(255, 170, 50),
ContourClassification.Open => System.Drawing.Color.FromArgb(200, 200, 100),
_ => System.Drawing.Color.Gray,
};
foreach (var entity in contour.Shape.Entities)
entity.Color = color;
}
}
private void CheckSimplifiable(FileListItem item)
{
ResetSimplifyButton();
@@ -293,10 +267,6 @@ namespace OpenNest.Forms
var normalized = ShapeProfile.NormalizeEntities(entities);
programEditor.LoadEntities(normalized);
staleProgram = false;
// Refresh CAD view to show contour-type colors
entityView1.ClearPenCache();
entityView1.Invalidate();
}
private void OnBendLineSelected(object sender, int index)
@@ -728,5 +698,10 @@ namespace OpenNest.Forms
}
#endregion
private void filterPanel_Paint(object sender, PaintEventArgs e)
{
}
}
}

View File

@@ -474,7 +474,7 @@ public partial class SplitDrawingForm : Form
}
// Placement preview line
if (_placingLine && _placingCursor != null)
if (_placingLine)
{
var isVert = _currentAxis == CutOffAxis.Vertical;
var snapped = _placingCursor;