Files
OpenNest/OpenNest/Forms/MainForm.Tools.cs
T
ajandClaude Opus 5.5 b026bd19d2 feat(common-line): place parts on common lines automatically at a chosen level
Slice 3 of common-line cutting. On costly plate (N26-295: 3/16 RA310S at about
$2,500 a sheet) pairing parts one kerf apart fits the part the spacing cannot:
the last plate's 38 parts fit on one 48x120 with Auto Nest only with pairs.

- Drawing Properties gains "Allow common line" (Drawing.AllowCommonLine, off by
  default because customer edge-quality needs vary). The nest's CommonLineUsage
  (Off / When it adds parts / Whenever available) is set in Auto Nest and seeded
  from Tools > Nest Defaults; both persist (nest.json, defaults.json).
- NestPipeline runs the ordinary solve, then, with a level on, trials where
  eligible parts are offered as kerf-gap pair compounds. Placed pairs expand into
  parts plus explicit NestJobCommonLine groups, are validated again, and
  CommonLineRanking keeps a trial only by the level: count first, then sheets;
  "when it adds parts" keeps the plain layout on a tie, "whenever" also takes a
  shorter cut or less material, never at the cost of a part. Engines, FillScore
  and their comparers are unchanged; Off asks the engine exactly what it did.
- NestLayoutCheck exempts members only through a sheet's explicit group list and
  only when the job allows common lines; materializing and committing create
  Plate.CommonLines.
- Server nesting writes common-line jobs as version 3 and replays the recorded
  trial answers, so the desktop binds the same groups.
- Fill Area honors the level through a PlateFillService overload.
- The benchmark gains --common-lines <level> --kerf <k>.

Benchmark (synthetic nests + N26-295, --parallel 1): with no flag and with
--common-lines off every engine matches the base commit; with a level on no
layout becomes invalid and three runs save a sheet at equal nested area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:38:00 -05:00

303 lines
11 KiB
C#

using System;
using System.Windows.Forms;
using OpenNest.Actions;
using OpenNest.Data;
using OpenNest.Geometry;
namespace OpenNest.Forms
{
public partial class MainForm
{
#region Tools Menu Events
private void MeasureArea_Click(object sender, EventArgs e) => WithActive(f => f.PlateView.SetAction(typeof(ActionSelectArea)));
private void BestFitViewer_Click(object sender, EventArgs e)
{
if (activeForm == null)
return;
var plate = activeForm.PlateView.Plate;
var drawings = activeForm.Nest.Drawings;
if (drawings.Count == 0)
{
MessageBox.Show(
"No drawings available.",
"Best-Fit Viewer",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
return;
}
using (var form = new BestFitViewerForm(drawings, plate, activeForm.Nest.Units))
{
if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null)
{
var parts =
form.SelectedParts
?? form.SelectedResult.BuildSourceParts(form.SelectedDrawing);
activeForm.PlateView.SetAction(typeof(ActionClone), parts);
}
}
}
private void ExpandSpacing_Click(object sender, EventArgs e)
{
var editForm = activeForm;
if (editForm == null)
return;
// The search reads the plate on a worker, so a nest or fill that can still
// add parts must finish first.
if (IsExpandSpacingBusy(editForm))
{
ShowExpandSpacingNotice(
"Finish or cancel the current nesting or fill before Expand Spacing."
);
return;
}
// Complex layouts can search for minutes; run behind a Cancel window.
var expanded = editForm.PlateView.ExpandSelected(work =>
OperationProgressForm.Run(
this,
"Expand Spacing",
"Expanding spacing between the selected parts...",
work
)
);
if (!expanded)
ShowExpandSpacingNotice("Select at least two parts on the plate to expand.");
}
// Native tests capture this notice without opening a modal dialog in session zero.
internal System.Action<string> ExpandSpacingNotice { get; set; }
private void ShowExpandSpacingNotice(string message)
{
if (ExpandSpacingNotice != null)
ExpandSpacingNotice(message);
else
MessageBox.Show(
this,
message,
"Expand Spacing",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
}
private void JoinCommonLine_Click(object sender, EventArgs e)
{
var editForm = activeForm;
if (editForm?.PlateView?.Plate == null)
return;
var kerf = MachineSettings.CuttingParametersFor(editForm.PlateView.Plate, editForm.Nest).Kerf;
var result = editForm.PlateView.JoinSelectedCommonLine(kerf);
if (!result.Succeeded)
ShowCommonLineNotice(result.Failure);
}
private void RemoveCommonLine_Click(object sender, EventArgs e)
{
if (activeForm?.PlateView?.Plate == null)
return;
if (!activeForm.PlateView.RemoveSelectedCommonLines())
ShowCommonLineNotice("None of the selected parts share a common line.");
}
private void CutCommonLinesTwice_Click(object sender, EventArgs e)
{
var plateView = activeForm?.PlateView;
if (plateView?.Plate == null)
return;
plateView.SetCutCommonLinesTwice(!plateView.Plate.CutCommonLinesTwice);
}
private void ArrangeMenu_Opening(object sender, EventArgs e)
{
var plate = activeForm?.PlateView?.Plate;
mnuPlateCutCommonLinesTwice.Checked = plate?.CutCommonLinesTwice == true;
mnuPlateCutCommonLinesTwice.Enabled = plate != null;
}
// Native tests capture this notice without opening a modal dialog in session zero.
internal System.Action<string> CommonLineNotice { get; set; }
private void ShowCommonLineNotice(string message)
{
if (CommonLineNotice != null)
CommonLineNotice(message);
else
MessageBox.Show(this, message, "Common Line", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void PatternTile_Click(object sender, EventArgs e)
{
if (activeForm == null)
return;
if (activeForm.Nest.Drawings.Count == 0)
{
MessageBox.Show(
"No drawings available.",
"Pattern Tile",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
return;
}
using (var form = new PatternTileForm(activeForm.Nest, activeForm.PlateView.Plate))
{
if (form.ShowDialog(this) != DialogResult.OK || form.Result == null)
return;
ApplyPatternTile(activeForm, form.Result);
}
}
internal static void ApplyPatternTile(EditNestForm editForm, PatternTileResult result)
{
using var change = editForm.BeginChange("Pattern Tile");
change.MarkChanged();
if (result.Target == PatternTileTarget.CurrentPlate)
{
editForm.PlateView.Plate.Parts.Clear();
foreach (var part in result.Parts)
editForm.PlateView.Plate.Parts.Add(part);
editForm.PlateView.ZoomToFit();
}
else
{
// The empty new-plate sentinel takes the pattern; a plate created beside it
// would be dropped as a second trailing empty plate before the parts arrive.
var plate = editForm.PlateManager.GetOrCreateEmpty();
plate.Size = result.PlateSize;
foreach (var part in result.Parts)
plate.Parts.Add(part);
editForm.PlateManager.LoadAt(editForm.Nest.Plates.IndexOf(plate));
}
editForm.Nest.UpdateDrawingQuantities();
change.Complete();
}
private void SetOffsetIncrement_Click(object sender, EventArgs e)
{
if (activeForm == null)
return;
using var form = new SetValueForm();
form.Text = "Set Offset Increment";
form.Value = activeForm.PlateView.OffsetIncrementDistance;
form.OptionText = "Calculated distance (selection size + part spacing)";
form.OptionChecked = activeForm.PlateView.CalculatedOffsetDistance;
if (form.ShowDialog(this) == DialogResult.OK)
{
activeForm.PlateView.OffsetIncrementDistance = form.Value;
activeForm.PlateView.CalculatedOffsetDistance = form.OptionChecked;
}
}
private void SetRotationIncrement_Click(object sender, EventArgs e)
{
if (activeForm == null)
return;
using var form = new SetValueForm();
form.Text = "Set Rotation Increment";
form.Value = activeForm.PlateView.RotateIncrementAngle;
form.Maximum = 360;
if (form.ShowDialog(this) == DialogResult.OK)
activeForm.PlateView.RotateIncrementAngle = form.Value;
}
private void Options_Click(object sender, EventArgs e)
{
using var form = new OptionsForm();
form.ShowDialog(this);
RefreshLedgerHolds(); // The ledger address or key may have changed.
}
private void NestDefaults_Click(object sender, EventArgs e)
{
var defaults = MachineSettings.LoadSavedNestDefaults(out _);
SharedSettings.WarnIfOffline(this);
using (var form = new NestDefaultsForm(defaults, MachineSettings.LoadMachines(), SharedSettings.NestDefaultsSource()))
{
if (form.ShowDialog(this) == DialogResult.OK)
MachineSettings.SaveNestDefaults(this, form.GetDefaults());
}
}
private void SaveCurrentAsDefaults_Click(object sender, EventArgs e)
{
if (activeForm == null)
return;
// A plate holds no common-line usage, so the saved defaults keep theirs.
MachineSettings.SaveNestDefaults(this, NestDefaults.FromPlate(activeForm.Nest.Units, activeForm.PlateView.Plate,
MachineSettings.LoadSavedNestDefaults(out _)), activeForm.Nest.Machine);
}
private void MachineConfig_Click(object sender, EventArgs e)
{
var provider = MachineSettings.CreateMachineProvider(refresh: true);
SharedSettings.WarnIfOffline(this);
using (var form = new MachineConfigForm(provider))
{
var source = SharedSettings.MachineSource(provider);
if (source.Length > 0)
form.Text += $" ({source})";
form.ShowDialog(this);
}
}
private void InitializeAlignMenu()
{
mnuPlateAlignLeft.Tag = AlignType.Left;
mnuPlateAlignRight.Tag = AlignType.Right;
mnuPlateAlignTop.Tag = AlignType.Top;
mnuPlateAlignBottom.Tag = AlignType.Bottom;
mnuPlateAlignHorizontal.Tag = AlignType.Horizontally;
mnuPlateAlignVertically.Tag = AlignType.Vertically;
mnuPlateEvenlySpaceHorizontal.Tag = AlignType.EvenlySpaceHorizontally;
mnuPlateEvenlySpaceVertical.Tag = AlignType.EvenlySpaceVertically;
}
private void Align_Click(object sender, EventArgs e)
{
if (((ToolStripItem)sender).Tag is AlignType type)
WithActive(f => f.PlateView.AlignSelected(type));
}
#endregion Tools Menu Events
private void CenterParts_Click(object sender, EventArgs e)
{
var view = activeForm?.PlateView;
if (view?.Plate == null || view.Parts.Count == 0)
return;
var offset = view.Plate.OffsetToCenterPartsInWorkArea();
using var change = view.BeginChange("Center Parts");
if (offset.X != 0 || offset.Y != 0)
change.MarkChanged();
foreach (var part in view.Parts)
part.Offset(offset);
view.Invalidate();
change.Complete();
}
}
}