From f711a2e4d6dabee83cc8b708e7fee03b579aed92 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 25 Mar 2026 23:41:37 -0400 Subject: [PATCH] feat: add SimplifierViewerForm tool window Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Forms/SimplifierViewerForm.cs | 223 +++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 OpenNest/Forms/SimplifierViewerForm.cs diff --git a/OpenNest/Forms/SimplifierViewerForm.cs b/OpenNest/Forms/SimplifierViewerForm.cs new file mode 100644 index 0000000..ff91db5 --- /dev/null +++ b/OpenNest/Forms/SimplifierViewerForm.cs @@ -0,0 +1,223 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Geometry; + +namespace OpenNest.Forms; + +public class SimplifierViewerForm : Form +{ + private ListView listView; + private System.Windows.Forms.NumericUpDown numTolerance; + private Label lblCount; + private Button btnApply; + private EntityView entityView; + private GeometrySimplifier simplifier; + private List shapes; + private List candidates; + + public event System.Action> Applied; + + public SimplifierViewerForm() + { + Text = "Geometry Simplifier"; + FormBorderStyle = FormBorderStyle.SizableToolWindow; + ShowInTaskbar = false; + TopMost = true; + StartPosition = FormStartPosition.Manual; + Size = new System.Drawing.Size(420, 450); + Font = new Font("Segoe UI", 9f); + + InitializeControls(); + } + + private void InitializeControls() + { + // Bottom panel + var bottomPanel = new FlowLayoutPanel + { + Dock = DockStyle.Bottom, + Height = 36, + Padding = new Padding(4, 6, 4, 4), + WrapContents = false, + }; + + var lblTolerance = new Label + { + Text = "Tolerance:", + AutoSize = true, + Margin = new Padding(0, 3, 2, 0), + }; + + numTolerance = new System.Windows.Forms.NumericUpDown + { + Minimum = 0.001m, + Maximum = 1.000m, + DecimalPlaces = 3, + Increment = 0.001m, + Value = 0.005m, + Width = 70, + }; + numTolerance.ValueChanged += OnToleranceChanged; + + lblCount = new Label + { + Text = "0 of 0 selected", + AutoSize = true, + Margin = new Padding(8, 3, 4, 0), + }; + + btnApply = new Button + { + Text = "Apply", + FlatStyle = FlatStyle.Flat, + Width = 60, + Margin = new Padding(4, 0, 0, 0), + }; + btnApply.Click += OnApplyClick; + + bottomPanel.Controls.AddRange(new Control[] { lblTolerance, numTolerance, lblCount, btnApply }); + + // ListView + listView = new ListView + { + Dock = DockStyle.Fill, + View = View.Details, + FullRowSelect = true, + CheckBoxes = true, + GridLines = true, + }; + listView.Columns.Add("Lines", 50); + listView.Columns.Add("Radius", 70); + listView.Columns.Add("Deviation", 75); + listView.Columns.Add("Location", 100); + listView.ItemSelectionChanged += OnItemSelected; + listView.ItemChecked += OnItemChecked; + + Controls.Add(listView); + Controls.Add(bottomPanel); + } + + public void LoadShapes(List shapes, EntityView view, double tolerance = 0.005) + { + this.shapes = shapes; + this.entityView = view; + numTolerance.Value = (decimal)tolerance; + simplifier = new GeometrySimplifier { Tolerance = tolerance }; + RunAnalysis(); + Show(); + BringToFront(); + } + + private void RunAnalysis() + { + candidates = new List(); + for (var i = 0; i < shapes.Count; i++) + { + var shapeCandidates = simplifier.Analyze(shapes[i]); + foreach (var c in shapeCandidates) + c.ShapeIndex = i; + candidates.AddRange(shapeCandidates); + } + RefreshList(); + } + + private void RefreshList() + { + listView.BeginUpdate(); + listView.Items.Clear(); + + foreach (var c in candidates) + { + var item = new ListViewItem(c.LineCount.ToString()); + item.Checked = c.IsSelected; + item.SubItems.Add(c.FittedArc.Radius.ToString("F3")); + item.SubItems.Add(c.MaxDeviation.ToString("F4")); + item.SubItems.Add($"{c.BoundingBox.Center.X:F1}, {c.BoundingBox.Center.Y:F1}"); + item.Tag = c; + listView.Items.Add(item); + } + + listView.EndUpdate(); + UpdateCountLabel(); + } + + private void UpdateCountLabel() + { + var selected = candidates.Count(c => c.IsSelected); + lblCount.Text = $"{selected} of {candidates.Count} selected"; + btnApply.Enabled = selected > 0; + } + + private void OnItemSelected(object sender, ListViewItemSelectionChangedEventArgs e) + { + if (!e.IsSelected || e.Item.Tag is not ArcCandidate candidate) + { + entityView?.ClearSimplifierPreview(); + return; + } + + // Highlight the candidate lines in the shape + var shape = shapes[candidate.ShapeIndex]; + var highlightEntities = new List(); + for (var i = candidate.StartIndex; i <= candidate.EndIndex; i++) + highlightEntities.Add(shape.Entities[i]); + + entityView.SimplifierHighlight = highlightEntities; + entityView.SimplifierPreview = candidate.FittedArc; + entityView.ZoomToArea(candidate.BoundingBox); + } + + private void OnItemChecked(object sender, ItemCheckedEventArgs e) + { + if (e.Item.Tag is ArcCandidate candidate) + { + candidate.IsSelected = e.Item.Checked; + UpdateCountLabel(); + } + } + + private void OnToleranceChanged(object sender, System.EventArgs e) + { + simplifier.Tolerance = (double)numTolerance.Value; + entityView?.ClearSimplifierPreview(); + RunAnalysis(); + } + + private void OnApplyClick(object sender, System.EventArgs e) + { + var byShape = candidates + .Where(c => c.IsSelected) + .GroupBy(c => c.ShapeIndex) + .ToDictionary(g => g.Key, g => g.ToList()); + + for (var i = 0; i < shapes.Count; i++) + { + if (byShape.TryGetValue(i, out var selected)) + shapes[i] = simplifier.Apply(shapes[i], selected); + } + + var entities = shapes.SelectMany(s => s.Entities).ToList(); + entityView?.ClearSimplifierPreview(); + Applied?.Invoke(entities); + Close(); + } + + protected override bool ProcessDialogKey(Keys keyData) + { + if (keyData == Keys.Escape) + { + Close(); + return true; + } + return base.ProcessDialogKey(keyData); + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + entityView?.ClearSimplifierPreview(); + base.OnFormClosing(e); + } +}