Files
OpenNest/OpenNest/Forms/SimplifierViewerForm.cs
AJ Isaacs c5943e22eb fix: correct Width/Length axis mapping and add spiral center-fill
Box constructor and derived properties (Right, Top, Center, Translate, Offset)
had Width and Length swapped — Length is X axis, Width is Y axis. Corrected
across Core geometry, plate bounding box, rectangle packing, fill algorithms,
tests, and UI renderers.

Added FillSpiral with center remnant detection and recursive FillBest on
the gap between the 4 spiral quadrants. RectFill.FillBest now compares
spiral+center vs full best-fit fairly. BestCombination returns a
CombinationResult record instead of out params.

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

172 lines
5.2 KiB
C#

using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Geometry;
namespace OpenNest.Forms;
public partial class SimplifierViewerForm : Form
{
private EntityView entityView;
private GeometrySimplifier simplifier;
private List<Shape> shapes;
private List<ArcCandidate> candidates;
public event System.Action<List<Entity>> Applied;
public SimplifierViewerForm()
{
InitializeComponent();
}
public void LoadShapes(List<Shape> shapes, EntityView view, double tolerance = 0.004)
{
this.shapes = shapes;
this.entityView = view;
numTolerance.Value = (decimal)tolerance;
simplifier = new GeometrySimplifier { Tolerance = tolerance };
RunAnalysis();
Show();
BringToFront();
}
private void RunAnalysis()
{
candidates = new List<ArcCandidate>();
for (var i = 0; i < shapes.Count; i++)
{
var shapeCandidates = simplifier.Analyze(shapes[i]);
foreach (var c in shapeCandidates)
c.ShapeIndex = i;
var axis = GeometrySimplifier.DetectMirrorAxis(shapes[i]);
if (axis.IsValid)
simplifier.Symmetrize(shapeCandidates, axis);
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<Entity>();
for (var i = candidate.StartIndex; i <= candidate.EndIndex; i++)
highlightEntities.Add(shape.Entities[i]);
entityView.SimplifierHighlight = highlightEntities;
entityView.SimplifierPreview = candidate.FittedArc;
// Build tolerance zone by offsetting each original line both directions
var tol = simplifier.Tolerance;
var leftEntities = new List<Entity>();
var rightEntities = new List<Entity>();
foreach (var entity in highlightEntities)
{
var left = entity.OffsetEntity(tol, OffsetSide.Left);
var right = entity.OffsetEntity(tol, OffsetSide.Right);
if (left != null) leftEntities.Add(left);
if (right != null) rightEntities.Add(right);
}
entityView.SimplifierToleranceLeft = leftEntities;
entityView.SimplifierToleranceRight = rightEntities;
// Zoom with padding for the tolerance zone
var padded = new Box(
candidate.BoundingBox.X - tol * 2,
candidate.BoundingBox.Y - tol * 2,
candidate.BoundingBox.Length + tol * 4,
candidate.BoundingBox.Width + tol * 4);
entityView.ZoomToArea(padded);
}
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)
{
if (simplifier == null) return;
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);
}
}