From b1d094104af233ee1c2e61050e4bd8de2cbd8712 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 10 Apr 2026 17:50:01 -0400 Subject: [PATCH] feat(ui): add filtered pipe size dropdown to shape library Renders PipeSize as a DropDownList ComboBox, filters entries to those fitting the current hole geometry, disables the combo when Blind is checked, and appends an invalid-pipe warning to the preview info when TryGetOD fails. Co-Authored-By: Claude Sonnet 4.6 --- OpenNest/Forms/ShapeLibraryForm.cs | 112 ++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/OpenNest/Forms/ShapeLibraryForm.cs b/OpenNest/Forms/ShapeLibraryForm.cs index 730b11e..1a8b943 100644 --- a/OpenNest/Forms/ShapeLibraryForm.cs +++ b/OpenNest/Forms/ShapeLibraryForm.cs @@ -192,6 +192,29 @@ namespace OpenNest.Forms cb.CheckedChanged += (s, ev) => UpdatePreview(); editor = cb; } + else if (prop.PropertyType == typeof(string) && prop.Name == "PipeSize") + { + var combo = new ComboBox + { + Location = new Point(parametersPanel.Padding.Left, y), + Width = panelWidth, + Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, + DropDownStyle = ComboBoxStyle.DropDownList + }; + + // Initial population: every entry; the filter runs on first UpdatePreview. + foreach (var entry in PipeSizes.All) + combo.Items.Add(entry.Label); + + var initial = sourceValues != null ? (string)prop.GetValue(sourceValues) : null; + if (!string.IsNullOrEmpty(initial) && combo.Items.Contains(initial)) + combo.SelectedItem = initial; + else if (combo.Items.Count > 0) + combo.SelectedIndex = 0; + + combo.SelectedIndexChanged += (s, ev) => UpdatePreview(); + editor = combo; + } else { var tb = new TextBox @@ -228,6 +251,8 @@ namespace OpenNest.Forms { if (suppressPreview || selectedEntry == null) return; + UpdatePipeSizeFilter(); + try { var shape = CreateShapeFromInputs(); @@ -239,9 +264,17 @@ namespace OpenNest.Forms 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)); + var info = string.Format("{0:F3} x {1:F3}", bb.Size.Length, bb.Size.Width); + + if (shape is PipeFlangeShape flange + && !flange.Blind + && !string.IsNullOrEmpty(flange.PipeSize) + && !PipeSizes.TryGetOD(flange.PipeSize, out _)) + { + info += " — Invalid pipe size, no bore cut"; + } + + previewBox.SetInfo(nameTextBox.Text, info); } } catch @@ -250,6 +283,72 @@ namespace OpenNest.Forms } } + private void UpdatePipeSizeFilter() + { + // Find the PipeSize combo and the numeric inputs it depends on. + ComboBox pipeCombo = null; + double holePattern = 0, holeDia = 0, clearance = 0; + bool blind = false; + + foreach (var binding in parameterBindings) + { + var name = binding.Property.Name; + if (name == "PipeSize" && binding.Control is ComboBox cb) + pipeCombo = cb; + else if (name == "HolePatternDiameter" && binding.Control is TextBox tb1) + double.TryParse(tb1.Text, out holePattern); + else if (name == "HoleDiameter" && binding.Control is TextBox tb2) + double.TryParse(tb2.Text, out holeDia); + else if (name == "PipeClearance" && binding.Control is TextBox tb3) + double.TryParse(tb3.Text, out clearance); + else if (name == "Blind" && binding.Control is CheckBox chk) + blind = chk.Checked; + } + + if (pipeCombo == null) + return; + + // Disable when blind, but keep visible with the selection preserved. + pipeCombo.Enabled = !blind; + + // Compute filter: pipeOD + clearance < HolePatternDiameter - HoleDiameter. + var maxPipeOD = holePattern - holeDia - clearance; + var fittingLabels = PipeSizes.GetFittingSizes(maxPipeOD).Select(e => e.Label).ToList(); + + // Sequence-equal on existing items — no-op if unchanged (avoids flicker). + var currentLabels = pipeCombo.Items.Cast().ToList(); + if (currentLabels.SequenceEqual(fittingLabels)) + return; + + var previousSelection = pipeCombo.SelectedItem as string; + + pipeCombo.BeginUpdate(); + try + { + pipeCombo.Items.Clear(); + foreach (var label in fittingLabels) + pipeCombo.Items.Add(label); + + if (fittingLabels.Count == 0) + { + // No pipe fits — leave unselected. + } + else if (previousSelection != null && fittingLabels.Contains(previousSelection)) + { + pipeCombo.SelectedItem = previousSelection; + } + else + { + // Select the largest (last, since PipeSizes.All is sorted ascending). + pipeCombo.SelectedIndex = fittingLabels.Count - 1; + } + } + finally + { + pipeCombo.EndUpdate(); + } + } + private ShapeDefinition CreateShapeFromInputs() { var shape = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType); @@ -264,6 +363,13 @@ namespace OpenNest.Forms continue; } + if (binding.Property.PropertyType == typeof(string)) + { + var combo = (ComboBox)binding.Control; + binding.Property.SetValue(shape, combo.SelectedItem?.ToString()); + continue; + } + var tb = (TextBox)binding.Control; if (binding.Property.PropertyType == typeof(int))