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>
This commit is contained in:
2026-04-03 21:22:55 -04:00
parent e50a7c82cf
commit c5943e22eb
55 changed files with 433 additions and 257 deletions

View File

@@ -198,9 +198,9 @@ namespace OpenNest.Actions
Box cutoffBox;
if (cutoff.Axis == CutOffAxis.Vertical)
cutoffBox = new Box(cutoff.Position.X, plateBounds.Y, 0, plateBounds.Length);
cutoffBox = new Box(cutoff.Position.X, plateBounds.Y, 0, plateBounds.Width);
else
cutoffBox = new Box(plateBounds.X, cutoff.Position.Y, plateBounds.Width, 0);
cutoffBox = new Box(plateBounds.X, cutoff.Position.Y, plateBounds.Length, 0);
boxes.Add(cutoffBox.Offset(plate.PartSpacing));
}

View File

@@ -92,8 +92,8 @@ namespace OpenNest.Actions
var location = plateView.PointWorldToGraph(SelectedArea.Location);
var size = new SizeF(
plateView.LengthWorldToGui(SelectedArea.Width),
plateView.LengthWorldToGui(SelectedArea.Length));
plateView.LengthWorldToGui(SelectedArea.Length),
plateView.LengthWorldToGui(SelectedArea.Width));
var rect = new System.Drawing.RectangleF(location.X, location.Y - size.Height, size.Width, size.Height);
@@ -176,9 +176,9 @@ namespace OpenNest.Actions
Box cutoffBox;
if (cutoff.Axis == CutOffAxis.Vertical)
cutoffBox = new Box(cutoff.Position.X, plateBounds.Y, 0, plateBounds.Length);
cutoffBox = new Box(cutoff.Position.X, plateBounds.Y, 0, plateBounds.Width);
else
cutoffBox = new Box(plateBounds.X, cutoff.Position.Y, plateBounds.Width, 0);
cutoffBox = new Box(plateBounds.X, cutoff.Position.Y, plateBounds.Length, 0);
boxes.Add(cutoffBox.Offset(plateView.Plate.PartSpacing));
}

View File

@@ -206,7 +206,7 @@ namespace OpenNest.Controls
public virtual void ZoomToArea(Box box, bool redraw = true)
{
ZoomToArea(box.X, box.Y, box.Width, box.Length, redraw);
ZoomToArea(box.X, box.Y, box.Length, box.Width, redraw);
}
public virtual void ZoomToArea(double x, double y, double width, double height, bool redraw = true)

View File

@@ -190,8 +190,8 @@ namespace OpenNest.Controls
var rect = new RectangleF
{
Location = view.PointWorldToGraph(workArea.Location),
Width = view.LengthWorldToGui(workArea.Width),
Height = view.LengthWorldToGui(workArea.Length)
Width = view.LengthWorldToGui(workArea.Length),
Height = view.LengthWorldToGui(workArea.Width)
};
rect.Y -= rect.Height;
@@ -226,8 +226,8 @@ namespace OpenNest.Controls
{
var box = remnants[i];
var loc = view.PointWorldToGraph(box.Location);
var w = view.LengthWorldToGui(box.Width);
var h = view.LengthWorldToGui(box.Length);
var w = view.LengthWorldToGui(box.Length);
var h = view.LengthWorldToGui(box.Width);
var rect = new RectangleF(loc.X, loc.Y - h, w, h);
var priority = view.DebugRemnantPriorities != null && i < view.DebugRemnantPriorities.Count
@@ -355,7 +355,7 @@ namespace OpenNest.Controls
var location = part.Location;
var pt1 = view.PointWorldToGraph(location);
var pt2 = view.PointWorldToGraph(new Vector(
location.X + box.Width, location.Y + box.Length));
location.X + box.Length, location.Y + box.Width));
using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f);
g.DrawRectangle(warnPen, pt1.X, pt2.Y,
System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y));
@@ -542,8 +542,8 @@ namespace OpenNest.Controls
var rect = new RectangleF
{
Location = view.PointWorldToGraph(box.Location),
Width = view.LengthWorldToGui(box.Width),
Height = view.LengthWorldToGui(box.Length)
Width = view.LengthWorldToGui(box.Length),
Height = view.LengthWorldToGui(box.Width)
};
g.DrawRectangle(view.ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);

View File

@@ -173,13 +173,13 @@ namespace OpenNest.Forms
return;
}
if (LeftSpacing + RightSpacing >= size.Width)
if (LeftSpacing + RightSpacing >= size.Length)
{
applyButton.Enabled = false;
return;
}
if (TopSpacing + BottomSpacing >= size.Length)
if (TopSpacing + BottomSpacing >= size.Width)
{
applyButton.Enabled = false;
return;

View File

@@ -131,13 +131,13 @@ namespace OpenNest.Forms
return;
}
if (LeftSpacing + RightSpacing >= size.Width)
if (LeftSpacing + RightSpacing >= size.Length)
{
applyButton.Enabled = false;
return;
}
if (TopSpacing + BottomSpacing >= size.Length)
if (TopSpacing + BottomSpacing >= size.Width)
{
applyButton.Enabled = false;
return;

View File

@@ -42,6 +42,7 @@
this.saveButton = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button();
this.bottomPanel1 = new OpenNest.Controls.BottomPanel();
this.strategyGrid = new System.Windows.Forms.DataGridView();
this.strategyGroupBox = new System.Windows.Forms.GroupBox();
((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
this.tableLayoutPanel1.SuspendLayout();
@@ -212,8 +213,24 @@
this.bottomPanel1.Size = new System.Drawing.Size(708, 50);
this.bottomPanel1.TabIndex = 1;
//
// strategyGrid
//
this.strategyGrid.AllowUserToAddRows = false;
this.strategyGrid.AllowUserToDeleteRows = false;
this.strategyGrid.AllowUserToResizeRows = false;
this.strategyGrid.BackgroundColor = System.Drawing.SystemColors.Window;
this.strategyGrid.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.strategyGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.strategyGrid.Dock = System.Windows.Forms.DockStyle.Fill;
this.strategyGrid.Location = new System.Drawing.Point(3, 18);
this.strategyGrid.Name = "strategyGrid";
this.strategyGrid.RowHeadersVisible = false;
this.strategyGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.strategyGrid.TabIndex = 0;
//
// strategyGroupBox
//
this.strategyGroupBox.Controls.Add(this.strategyGrid);
this.strategyGroupBox.Location = new System.Drawing.Point(12, 178);
this.strategyGroupBox.Name = "strategyGroupBox";
this.strategyGroupBox.Size = new System.Drawing.Size(684, 180);
@@ -263,6 +280,7 @@
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.DataGridView strategyGrid;
private System.Windows.Forms.GroupBox strategyGroupBox;
}
}

View File

@@ -1,4 +1,4 @@
using OpenNest.Engine.Strategies;
using OpenNest.Engine.Strategies;
using OpenNest.Properties;
using System;
using System.Collections.Generic;
@@ -9,12 +9,10 @@ namespace OpenNest.Forms
{
public partial class OptionsForm : Form
{
private readonly List<CheckBox> _strategyCheckBoxes = new();
public OptionsForm()
{
InitializeComponent();
BuildStrategyCheckBoxes();
BuildStrategyGrid();
}
protected override void OnLoad(EventArgs e)
@@ -23,23 +21,44 @@ namespace OpenNest.Forms
LoadSettings();
}
private void BuildStrategyCheckBoxes()
private void BuildStrategyGrid()
{
var strategies = FillStrategyRegistry.AllStrategies;
var y = 20;
strategyGrid.AutoGenerateColumns = false;
foreach (var strategy in strategies)
strategyGrid.Columns.Add(new DataGridViewCheckBoxColumn
{
var cb = new CheckBox
{
Text = strategy.Name,
Tag = strategy.Name,
AutoSize = true,
Location = new System.Drawing.Point(10, y),
};
strategyGroupBox.Controls.Add(cb);
_strategyCheckBoxes.Add(cb);
y += 24;
Name = "Enabled",
HeaderText = "",
Width = 30,
});
strategyGrid.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "Name",
HeaderText = "Strategy",
ReadOnly = true,
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
});
strategyGrid.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "Phase",
HeaderText = "Phase",
ReadOnly = true,
Width = 100,
});
strategyGrid.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "Order",
HeaderText = "Order",
ReadOnly = true,
Width = 55,
});
foreach (var strategy in FillStrategyRegistry.AllStrategies)
{
strategyGrid.Rows.Add(true, strategy.Name, strategy.Phase, strategy.Order);
}
}
@@ -51,8 +70,8 @@ namespace OpenNest.Forms
numericUpDown2.Value = (decimal)Settings.Default.ImportSplinePrecision;
var disabledNames = ParseDisabledStrategies(Settings.Default.DisabledStrategies);
foreach (var cb in _strategyCheckBoxes)
cb.Checked = !disabledNames.Contains((string)cb.Tag);
foreach (DataGridViewRow row in strategyGrid.Rows)
row.Cells["Enabled"].Value = !disabledNames.Contains((string)row.Cells["Name"].Value);
}
private void SaveSettings()
@@ -62,9 +81,12 @@ namespace OpenNest.Forms
Settings.Default.AutoSizePlateFactor = (double)numericUpDown1.Value;
Settings.Default.ImportSplinePrecision = (int)numericUpDown2.Value;
var disabledNames = _strategyCheckBoxes
.Where(cb => !cb.Checked)
.Select(cb => (string)cb.Tag);
var disabledNames = new List<string>();
foreach (DataGridViewRow row in strategyGrid.Rows)
{
if (row.Cells["Enabled"].Value is false)
disabledNames.Add((string)row.Cells["Name"].Value);
}
Settings.Default.DisabledStrategies = string.Join(",", disabledNames);
Settings.Default.Save();

View File

@@ -112,8 +112,8 @@ public partial class SimplifierViewerForm : Form
var padded = new Box(
candidate.BoundingBox.X - tol * 2,
candidate.BoundingBox.Y - tol * 2,
candidate.BoundingBox.Width + tol * 4,
candidate.BoundingBox.Length + tol * 4);
candidate.BoundingBox.Length + tol * 4,
candidate.BoundingBox.Width + tol * 4);
entityView.ZoomToArea(padded);
}

View File

@@ -78,7 +78,7 @@ public partial class SplitDrawingForm : Form
var usable = plateW - 2 * spacing - overhang;
if (usable > 0)
{
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
for (var i = 1; i <= splits; i++)
_splitLines.Add(new SplitLine(_drawingBounds.X + usable * i, CutOffAxis.Vertical));
}
@@ -88,7 +88,7 @@ public partial class SplitDrawingForm : Form
var usable = plateH - 2 * spacing - overhang;
if (usable > 0)
{
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
for (var i = 1; i <= splits; i++)
_splitLines.Add(new SplitLine(_drawingBounds.Y + usable * i, CutOffAxis.Horizontal));
}

View File

@@ -128,7 +128,7 @@ namespace OpenNest
if (shapes.Count == 0)
{
var bbox = BasePart.BaseDrawing.Program.BoundingBox();
return new Vector(bbox.Location.X + bbox.Width / 2, bbox.Location.Y + bbox.Length / 2);
return new Vector(bbox.Location.X + bbox.Length / 2, bbox.Location.Y + bbox.Width / 2);
}
var profile = new ShapeProfile(nonRapid);