Files
OpenNest/OpenNest/Controls/FilterPanel.cs
T
aj 2c0457d503 feat(ui): add bend line editing to CAD converter
Add Edit link and double-click handler to the bend lines list so
existing bends can be modified without removing and re-adding them.
BendLineDialog gains a LoadBend method to populate fields from an
existing Bend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:36:26 -04:00

336 lines
12 KiB
C#

using OpenNest.Bending;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace OpenNest.Controls
{
public class FilterPanel : Panel
{
private readonly CollapsiblePanel layersPanel;
private readonly CollapsiblePanel colorsPanel;
private readonly CollapsiblePanel lineTypesPanel;
private readonly CollapsiblePanel bendLinesPanel;
private readonly CheckedListBox layersList;
private readonly ListBox colorsList;
private readonly CheckedListBox lineTypesList;
private readonly ListBox bendLinesList;
private readonly LinkLabel bendAddLink;
private List<Entity> currentEntities;
private List<Bend> currentBends;
public event EventHandler FilterChanged;
public event EventHandler<int> BendLineSelected;
public event EventHandler<int> BendLineRemoved;
public event EventHandler<int> BendLineEdited;
public event EventHandler AddBendLineClicked;
public FilterPanel()
{
AutoScroll = true;
BackColor = Color.White;
// Bend Lines
bendLinesPanel = new CollapsiblePanel
{
HeaderText = "Bend Lines (0)",
Dock = DockStyle.Top,
ExpandedHeight = 120,
IsExpanded = false
};
bendLinesList = new ListBox
{
Dock = DockStyle.Fill,
BorderStyle = BorderStyle.None,
Font = new Font("Segoe UI", 9f)
};
bendLinesList.SelectedIndexChanged += (s, e) =>
BendLineSelected?.Invoke(this, bendLinesList.SelectedIndex);
var bendEditLink = new LinkLabel
{
Text = "Edit",
AutoSize = true,
Font = new Font("Segoe UI", 8f)
};
bendEditLink.LinkClicked += (s, e) =>
{
if (bendLinesList.SelectedIndex >= 0)
BendLineEdited?.Invoke(this, bendLinesList.SelectedIndex);
};
var bendDeleteLink = new LinkLabel
{
Text = "Remove",
AutoSize = true,
Font = new Font("Segoe UI", 8f)
};
bendDeleteLink.LinkClicked += (s, e) =>
{
if (bendLinesList.SelectedIndex >= 0)
BendLineRemoved?.Invoke(this, bendLinesList.SelectedIndex);
};
bendLinesList.DoubleClick += (s, e) =>
{
if (bendLinesList.SelectedIndex >= 0)
BendLineEdited?.Invoke(this, bendLinesList.SelectedIndex);
};
bendAddLink = new LinkLabel
{
Text = "Add Bend Line",
AutoSize = true,
Font = new Font("Segoe UI", 8f)
};
bendAddLink.LinkClicked += (s, e) =>
AddBendLineClicked?.Invoke(this, EventArgs.Empty);
var bendLinksPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
Height = 20,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false
};
bendLinksPanel.Controls.Add(bendAddLink);
bendLinksPanel.Controls.Add(bendEditLink);
bendLinksPanel.Controls.Add(bendDeleteLink);
bendLinesPanel.ContentPanel.Controls.Add(bendLinesList);
bendLinesPanel.ContentPanel.Controls.Add(bendLinksPanel);
// Line Types
lineTypesPanel = new CollapsiblePanel
{
HeaderText = "Line Types (0)",
Dock = DockStyle.Top,
ExpandedHeight = 100,
IsExpanded = true
};
lineTypesList = CreateCheckedList();
lineTypesPanel.ContentPanel.Controls.Add(lineTypesList);
// Colors
colorsPanel = new CollapsiblePanel
{
HeaderText = "Colors (0)",
Dock = DockStyle.Top,
ExpandedHeight = 100,
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.DrawItem += ColorsList_DrawItem;
colorsList.MouseClick += ColorsList_MouseClick;
colorsPanel.ContentPanel.Controls.Add(colorsList);
// Layers (always expanded)
layersPanel = new CollapsiblePanel
{
HeaderText = "Layers",
Dock = DockStyle.Top,
ExpandedHeight = 160,
IsExpanded = true
};
var checkAllPanel = new Panel { Dock = DockStyle.Top, Height = 22 };
var checkAll = new LinkLabel { Text = "All", AutoSize = true, Location = new Point(4, 2), Font = new Font("Segoe UI", 8f) };
var uncheckAll = new LinkLabel { Text = "None", AutoSize = true, Location = new Point(30, 2), Font = new Font("Segoe UI", 8f) };
checkAll.LinkClicked += (s, e) => SetAllChecked(layersList, true);
uncheckAll.LinkClicked += (s, e) => SetAllChecked(layersList, false);
checkAllPanel.Controls.AddRange(new Control[] { checkAll, uncheckAll });
layersList = CreateCheckedList();
layersPanel.ContentPanel.Controls.Add(layersList);
layersPanel.ContentPanel.Controls.Add(checkAllPanel);
// Add panels in reverse order (Dock.Top stacks top-down)
Controls.Add(bendLinesPanel);
Controls.Add(lineTypesPanel);
Controls.Add(colorsPanel);
Controls.Add(layersPanel);
}
private CheckedListBox CreateCheckedList()
{
var list = new CheckedListBox
{
Dock = DockStyle.Fill,
BorderStyle = BorderStyle.None,
CheckOnClick = true,
Font = new Font("Segoe UI", 9f)
};
list.ItemCheck += (s, e) =>
{
if (IsHandleCreated)
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty)));
};
return list;
}
public void LoadItem(List<Entity> entities, List<Bend> bends)
{
currentEntities = entities;
currentBends = bends;
// Layers
layersList.Items.Clear();
var layers = entities
.Where(e => e.Layer != null)
.Select(e => e.Layer)
.GroupBy(l => l.Name)
.Select(g => g.First());
foreach (var layer in layers)
layersList.Items.Add(layer.Name, layer.IsVisible);
layersPanel.HeaderText = $"Layers ({layersList.Items.Count})";
// Colors
colorsList.Items.Clear();
var colors = entities
.Select(e => e.Color.ToArgb())
.Distinct()
.Select(argb => new ColorItem(Color.FromArgb(argb)));
foreach (var color in colors)
colorsList.Items.Add(color);
colorsPanel.HeaderText = $"Colors ({colorsList.Items.Count})";
// Line Types
lineTypesList.Items.Clear();
var lineTypes = entities
.GroupBy(e => e.LineTypeName ?? "Continuous")
.Select(g => new { Name = g.Key, Visible = g.Any(e => e.IsVisible) });
foreach (var lt in lineTypes)
lineTypesList.Items.Add(lt.Name, lt.Visible);
lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})";
// Bend Lines
bendLinesList.Items.Clear();
if (bends != null)
{
foreach (var bend in bends)
bendLinesList.Items.Add(bend.ToString());
}
var bendCount = bends?.Count ?? 0;
bendLinesPanel.HeaderText = $"Bend Lines ({bendCount})";
bendLinesPanel.IsExpanded = bendCount > 0;
}
public void ApplyFilters(List<Entity> entities)
{
var hiddenLayers = new HashSet<string>();
for (var i = 0; i < layersList.Items.Count; i++)
{
if (!layersList.GetItemChecked(i))
hiddenLayers.Add(layersList.Items[i].ToString());
}
var hiddenColors = new HashSet<int>();
for (var i = 0; i < colorsList.Items.Count; i++)
{
var item = (ColorItem)colorsList.Items[i];
if (!item.IsChecked)
hiddenColors.Add(item.Argb);
}
var hiddenLineTypes = new HashSet<string>();
for (var i = 0; i < lineTypesList.Items.Count; i++)
{
if (!lineTypesList.GetItemChecked(i))
hiddenLineTypes.Add(lineTypesList.Items[i].ToString());
}
foreach (var entity in entities)
{
var layerVisible = entity.Layer?.Name == null || !hiddenLayers.Contains(entity.Layer.Name);
var colorVisible = !hiddenColors.Contains(entity.Color.ToArgb());
var ltVisible = !hiddenLineTypes.Contains(entity.LineTypeName ?? "Continuous");
entity.IsVisible = layerVisible && colorVisible && ltVisible;
if (entity.Layer != null)
entity.Layer.IsVisible = !hiddenLayers.Contains(entity.Layer.Name);
}
}
private void SetAllChecked(CheckedListBox list, bool isChecked)
{
for (var i = 0; i < list.Items.Count; i++)
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.Graphics.FillRectangle(Brushes.White, e.Bounds);
var colorItem = (ColorItem)colorsList.Items[e.Index];
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);
TextRenderer.DrawText(e.Graphics, colorItem.ToString(), e.Font,
new Point(swatchRect.Right + 4, e.Bounds.Top + 1), SystemColors.WindowText);
}
public void SetPickMode(bool active)
{
bendAddLink.Text = active ? "Cancel (Esc)" : "Add Bend Line";
bendAddLink.LinkColor = active ? Color.OrangeRed : Color.Empty;
}
}
public class ColorItem
{
public int Argb { get; }
public Color Color { get; }
public bool IsChecked { get; set; } = true;
public ColorItem(Color color)
{
Color = color;
Argb = color.ToArgb();
}
public override string ToString() => $"#{Color.R:X2}{Color.G:X2}{Color.B:X2}";
public override bool Equals(object obj) => obj is ColorItem other && Argb == other.Argb;
public override int GetHashCode() => Argb;
}
}