Files
OpenNest/OpenNest/Forms/SplitDrawingForm.cs
AJ Isaacs adb8ed12d7 feat: add SplitDrawingForm UI dialog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:18:31 -04:00

364 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest.Forms;
public partial class SplitDrawingForm : Form
{
private readonly Drawing _drawing;
private readonly List<Entity> _drawingEntities;
private readonly Box _drawingBounds;
private readonly List<SplitLine> _splitLines = new();
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
private bool _placingLine;
// Zoom/pan state
private float _zoom = 1f;
private PointF _pan;
private Point _lastMouse;
private bool _panning;
// Snap threshold in screen pixels
private const double SnapThreshold = 5.0;
public List<Drawing> ResultDrawings { get; private set; }
public SplitDrawingForm(Drawing drawing)
{
InitializeComponent();
// Enable double buffering on the preview panel to reduce flicker
typeof(Panel).InvokeMember(
"DoubleBuffered",
System.Reflection.BindingFlags.SetProperty | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
null, pnlPreview, new object[] { true });
_drawing = drawing;
_drawingEntities = ConvertProgram.ToGeometry(drawing.Program);
_drawingBounds = drawing.Program.BoundingBox();
Text = $"Split Drawing: {drawing.Name}";
UpdateUI();
FitToView();
}
// --- Split Method Selection ---
private void OnMethodChanged(object sender, EventArgs e)
{
grpAutoFit.Visible = radFitToPlate.Checked;
grpByCount.Visible = radByCount.Checked;
if (radFitToPlate.Checked || radByCount.Checked)
RecalculateAutoSplitLines();
}
private void RecalculateAutoSplitLines()
{
_splitLines.Clear();
if (radFitToPlate.Checked)
{
var plateW = (double)nudPlateWidth.Value;
var plateH = (double)nudPlateHeight.Value;
var spacing = (double)nudEdgeSpacing.Value;
var overhang = GetCurrentParameters().FeatureOverhang;
_splitLines.AddRange(AutoSplitCalculator.FitToPlate(_drawingBounds, plateW, plateH, spacing, overhang));
}
else if (radByCount.Checked)
{
var hPieces = (int)nudHorizontalPieces.Value;
var vPieces = (int)nudVerticalPieces.Value;
_splitLines.AddRange(AutoSplitCalculator.SplitByCount(_drawingBounds, hPieces, vPieces));
}
UpdateUI();
pnlPreview.Invalidate();
}
private void OnAutoFitValueChanged(object sender, EventArgs e)
{
if (radFitToPlate.Checked)
RecalculateAutoSplitLines();
}
private void OnByCountValueChanged(object sender, EventArgs e)
{
if (radByCount.Checked)
RecalculateAutoSplitLines();
}
// --- Split Type Selection ---
private void OnTypeChanged(object sender, EventArgs e)
{
grpTabParams.Visible = radTabs.Checked;
grpSpikeParams.Visible = radSpike.Checked;
if (radFitToPlate.Checked)
RecalculateAutoSplitLines(); // overhang changed
pnlPreview.Invalidate();
}
private SplitParameters GetCurrentParameters()
{
var p = new SplitParameters();
if (radTabs.Checked)
{
p.Type = SplitType.WeldGapTabs;
p.TabWidth = (double)nudTabWidth.Value;
p.TabHeight = (double)nudTabHeight.Value;
p.TabCount = (int)nudTabCount.Value;
}
else if (radSpike.Checked)
{
p.Type = SplitType.SpikeGroove;
p.SpikeDepth = (double)nudSpikeDepth.Value;
p.SpikeAngle = (double)nudSpikeAngle.Value;
p.SpikePairCount = (int)nudSpikePairCount.Value;
}
return p;
}
// --- Manual Split Line Placement ---
private void OnPreviewMouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && radManual.Checked && _placingLine)
{
var pt = ScreenToDrawing(e.Location);
var snapped = SnapToMidpoint(pt);
var position = _currentAxis == CutOffAxis.Vertical ? snapped.X : snapped.Y;
_splitLines.Add(new SplitLine(position, _currentAxis));
UpdateUI();
pnlPreview.Invalidate();
}
else if (e.Button == MouseButtons.Middle)
{
_panning = true;
_lastMouse = e.Location;
}
}
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (_panning)
{
_pan.X += e.X - _lastMouse.X;
_pan.Y += e.Y - _lastMouse.Y;
_lastMouse = e.Location;
pnlPreview.Invalidate();
}
var drawPt = ScreenToDrawing(e.Location);
lblCursor.Text = $"Cursor: {drawPt.X:F2}, {drawPt.Y:F2}";
}
private void OnPreviewMouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Middle)
_panning = false;
}
private void OnPreviewMouseWheel(object sender, MouseEventArgs e)
{
var factor = e.Delta > 0 ? 1.1f : 0.9f;
_zoom *= factor;
pnlPreview.Invalidate();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Space)
{
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
return true;
}
if (keyData == Keys.Escape)
{
_placingLine = false;
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
private Vector SnapToMidpoint(Vector pt)
{
var midX = _drawingBounds.Center.X;
var midY = _drawingBounds.Center.Y;
var threshold = SnapThreshold / _zoom;
if (_currentAxis == CutOffAxis.Vertical && System.Math.Abs(pt.X - midX) < threshold)
return new Vector(midX, pt.Y);
if (_currentAxis == CutOffAxis.Horizontal && System.Math.Abs(pt.Y - midY) < threshold)
return new Vector(pt.X, midY);
return pt;
}
// --- Rendering ---
private void OnPreviewPaint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.FromArgb(26, 26, 26));
g.TranslateTransform(_pan.X, _pan.Y);
g.ScaleTransform(_zoom, -_zoom); // flip Y for CNC coordinates
// Draw part outline
using var partPen = new Pen(Color.LightGray, 1f / _zoom);
DrawEntities(g, _drawingEntities, partPen);
// Draw split lines
using var splitPen = new Pen(Color.FromArgb(255, 82, 82), 1f / _zoom);
splitPen.DashStyle = DashStyle.Dash;
foreach (var sl in _splitLines)
{
if (sl.Axis == CutOffAxis.Vertical)
g.DrawLine(splitPen, (float)sl.Position, (float)(_drawingBounds.Bottom - 10), (float)sl.Position, (float)(_drawingBounds.Top + 10));
else
g.DrawLine(splitPen, (float)(_drawingBounds.Left - 10), (float)sl.Position, (float)(_drawingBounds.Right + 10), (float)sl.Position);
}
// Draw piece color overlays
DrawPieceOverlays(g);
}
private void DrawEntities(Graphics g, List<Entity> entities, Pen pen)
{
foreach (var entity in entities)
{
if (entity is Line line)
g.DrawLine(pen, (float)line.StartPoint.X, (float)line.StartPoint.Y, (float)line.EndPoint.X, (float)line.EndPoint.Y);
else if (entity is Arc arc)
{
var rect = new RectangleF(
(float)(arc.Center.X - arc.Radius),
(float)(arc.Center.Y - arc.Radius),
(float)(arc.Radius * 2),
(float)(arc.Radius * 2));
var startDeg = (float)OpenNest.Math.Angle.ToDegrees(arc.StartAngle);
var sweepDeg = (float)OpenNest.Math.Angle.ToDegrees(arc.EndAngle - arc.StartAngle);
if (rect.Width > 0 && rect.Height > 0)
g.DrawArc(pen, rect, startDeg, sweepDeg);
}
}
}
private static readonly Color[] PieceColors =
{
Color.FromArgb(40, 79, 195, 247),
Color.FromArgb(40, 129, 199, 132),
Color.FromArgb(40, 255, 183, 77),
Color.FromArgb(40, 206, 147, 216),
Color.FromArgb(40, 255, 138, 128),
Color.FromArgb(40, 128, 222, 234)
};
private void DrawPieceOverlays(Graphics g)
{
// Simple region overlay based on split lines and bounding box
var regions = BuildPreviewRegions();
for (var i = 0; i < regions.Count; i++)
{
var color = PieceColors[i % PieceColors.Length];
using var brush = new SolidBrush(color);
var r = regions[i];
g.FillRectangle(brush, (float)r.Left, (float)r.Bottom, (float)r.Width, (float)r.Length);
}
}
private List<Box> BuildPreviewRegions()
{
var verticals = _splitLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList();
var horizontals = _splitLines.Where(l => l.Axis == CutOffAxis.Horizontal).OrderBy(l => l.Position).ToList();
var xEdges = new List<double> { _drawingBounds.Left };
xEdges.AddRange(verticals.Select(v => v.Position));
xEdges.Add(_drawingBounds.Right);
var yEdges = new List<double> { _drawingBounds.Bottom };
yEdges.AddRange(horizontals.Select(h => h.Position));
yEdges.Add(_drawingBounds.Top);
var regions = new List<Box>();
for (var yi = 0; yi < yEdges.Count - 1; yi++)
for (var xi = 0; xi < xEdges.Count - 1; xi++)
regions.Add(new Box(xEdges[xi], yEdges[yi], xEdges[xi + 1] - xEdges[xi], yEdges[yi + 1] - yEdges[yi]));
return regions;
}
// --- Coordinate transforms ---
private Vector ScreenToDrawing(Point screen)
{
var x = (screen.X - _pan.X) / _zoom;
var y = -(screen.Y - _pan.Y) / _zoom; // flip Y
return new Vector(x, y);
}
private void FitToView()
{
if (_drawingBounds.Width <= 0 || _drawingBounds.Length <= 0) return;
var pad = 40f;
var scaleX = (pnlPreview.Width - pad * 2) / (float)_drawingBounds.Width;
var scaleY = (pnlPreview.Height - pad * 2) / (float)_drawingBounds.Length;
_zoom = System.Math.Min(scaleX, scaleY);
_pan = new PointF(
pad - (float)_drawingBounds.Left * _zoom,
pnlPreview.Height - pad + (float)_drawingBounds.Bottom * _zoom);
}
// --- OK/Cancel ---
private void OnOK(object sender, EventArgs e)
{
if (_splitLines.Count == 0)
{
MessageBox.Show("No split lines defined.", "Split Drawing", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
ResultDrawings = DrawingSplitter.Split(_drawing, _splitLines, GetCurrentParameters());
DialogResult = DialogResult.OK;
Close();
}
private void OnCancel(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
// --- Toolbar ---
private void OnAddSplitLine(object sender, EventArgs e)
{
radManual.Checked = true;
_placingLine = true;
}
private void OnDeleteSplitLine(object sender, EventArgs e)
{
if (_splitLines.Count > 0)
{
_splitLines.RemoveAt(_splitLines.Count - 1);
UpdateUI();
pnlPreview.Invalidate();
}
}
private void UpdateUI()
{
var pieceCount = _splitLines.Count == 0 ? 1 : BuildPreviewRegions().Count;
lblStatus.Text = $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces";
}
}