From 7a6c407edd090ee1c325befff389ab40a4eb40a7 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 1 Apr 2026 20:24:28 -0400 Subject: [PATCH] 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) --- OpenNest/Controls/FilterPanel.cs | 52 +++++-- OpenNest/Controls/ProgramEditorControl.cs | 41 +++--- OpenNest/Forms/CadConverterForm.Designer.cs | 142 ++++++++++---------- OpenNest/Forms/CadConverterForm.cs | 35 +---- OpenNest/Forms/SplitDrawingForm.cs | 2 +- 5 files changed, 136 insertions(+), 136 deletions(-) diff --git a/OpenNest/Controls/FilterPanel.cs b/OpenNest/Controls/FilterPanel.cs index 9901011..ceddf6d 100644 --- a/OpenNest/Controls/FilterPanel.cs +++ b/OpenNest/Controls/FilterPanel.cs @@ -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(); 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(); @@ -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) { diff --git a/OpenNest/Controls/ProgramEditorControl.cs b/OpenNest/Controls/ProgramEditorControl.cs index 041f865..191597f 100644 --- a/OpenNest/Controls/ProgramEditorControl.cs +++ b/OpenNest/Controls/ProgramEditorControl.cs @@ -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) diff --git a/OpenNest/Forms/CadConverterForm.Designer.cs b/OpenNest/Forms/CadConverterForm.Designer.cs index 1f27f4a..50dda1e 100644 --- a/OpenNest/Forms/CadConverterForm.Designer.cs +++ b/OpenNest/Forms/CadConverterForm.Designer.cs @@ -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); diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index 2ad71a3..1adbcf5 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -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 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) + { + + } } } diff --git a/OpenNest/Forms/SplitDrawingForm.cs b/OpenNest/Forms/SplitDrawingForm.cs index 6512c80..756edfe 100644 --- a/OpenNest/Forms/SplitDrawingForm.cs +++ b/OpenNest/Forms/SplitDrawingForm.cs @@ -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;