Files
OpenNest/OpenNest/Forms/SplitDrawingForm.cs
AJ Isaacs 7a6c407edd feat: add owner-drawn color swatch to FilterPanel
Switch colorsList from CheckedListBox (which silently ignores owner
draw) to a plain ListBox with manual checkbox, color swatch, and hex
label rendering. Clone entities in ProgramEditorControl preview to
avoid mutating originals. Remove contour color application from
CadConverterForm. Fix struct null comparison warning in SplitDrawingForm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:24:28 -04:00

652 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Controls;
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;
private Vector _placingCursor;
// Feature handle drag state
private int _dragLineIndex = -1;
private int _dragFeatureIndex = -1;
private int _hoverLineIndex = -1;
private int _hoverFeatureIndex = -1;
private const float HandleRadius = 5f;
private const double SnapThreshold = 5.0;
public List<Drawing> ResultDrawings { get; private set; }
public SplitDrawingForm(Drawing drawing)
{
InitializeComponent();
_drawing = drawing;
_drawingEntities = ConvertProgram.ToGeometry(drawing.Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
_drawingBounds = drawing.Program.BoundingBox();
foreach (var entity in _drawingEntities)
entity.Layer.IsVisible = true;
pnlPreview.Entities = _drawingEntities;
pnlPreview.DrawOverlays = PaintOverlays;
Text = $"Split Drawing: {drawing.Name}";
UpdateUI();
pnlPreview.ZoomToFit(true);
}
// --- 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;
var axisIndex = cboSplitAxis.SelectedIndex;
if (axisIndex == 1)
{
var usable = plateW - 2 * spacing - overhang;
if (usable > 0)
{
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
for (var i = 1; i <= splits; i++)
_splitLines.Add(new SplitLine(_drawingBounds.X + usable * i, CutOffAxis.Vertical));
}
}
else if (axisIndex == 2)
{
var usable = plateH - 2 * spacing - overhang;
if (usable > 0)
{
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
for (var i = 1; i <= splits; i++)
_splitLines.Add(new SplitLine(_drawingBounds.Y + usable * i, CutOffAxis.Horizontal));
}
}
else
{
_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));
}
InitializeAllFeaturePositions();
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;
InitializeAllFeaturePositions();
if (radFitToPlate.Checked)
RecalculateAutoSplitLines();
pnlPreview.Invalidate();
}
private void OnSpikeParamChanged(object sender, EventArgs e)
{
if (radFitToPlate.Checked)
RecalculateAutoSplitLines();
pnlPreview.Invalidate();
}
private void OnFeatureCountChanged(object sender, EventArgs e)
{
InitializeAllFeaturePositions();
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.GrooveDepth = (double)nudGrooveDepth.Value;
p.SpikeWeldGap = (double)nudSpikeWeldGap.Value;
p.SpikeDepth = p.GrooveDepth + p.SpikeWeldGap;
p.SpikeAngle = (double)nudSpikeAngle.Value;
p.SpikePairCount = (int)nudSpikePairCount.Value;
}
return p;
}
// --- Feature Position Management ---
private int GetFeatureCount()
{
if (radTabs.Checked) return (int)nudTabCount.Value;
if (radSpike.Checked) return (int)nudSpikePairCount.Value;
return 0;
}
private void GetExtent(SplitLine sl, out double start, out double end)
{
if (sl.Axis == CutOffAxis.Vertical)
{
start = _drawingBounds.Bottom;
end = _drawingBounds.Top;
}
else
{
start = _drawingBounds.Left;
end = _drawingBounds.Right;
}
}
private void InitializeFeaturePositions(SplitLine sl)
{
var count = GetFeatureCount();
GetExtent(sl, out var start, out var end);
var extent = end - start;
sl.FeaturePositions.Clear();
if (count <= 0 || extent <= 0) return;
if (radSpike.Checked)
{
var margin = extent * 0.15;
if (count == 1)
{
sl.FeaturePositions.Add(start + extent / 2);
}
else
{
var usable = extent - 2 * margin;
for (var i = 0; i < count; i++)
sl.FeaturePositions.Add(start + margin + usable * i / (count - 1));
}
}
else
{
var spacing = extent / (count + 1);
for (var i = 0; i < count; i++)
sl.FeaturePositions.Add(start + spacing * (i + 1));
}
}
private void InitializeAllFeaturePositions()
{
foreach (var sl in _splitLines)
InitializeFeaturePositions(sl);
}
// --- Mouse Interaction ---
private void OnPreviewMouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
var worldPt = pnlPreview.PointControlToWorld(e.Location);
// Check for feature handle hit
var (lineIdx, featIdx) = HitTestFeatureHandle(worldPt);
if (lineIdx >= 0)
{
_dragLineIndex = lineIdx;
_dragFeatureIndex = featIdx;
return;
}
// Split line placement
if (radManual.Checked && _placingLine)
{
var snapped = SnapToMidpoint(worldPt);
var position = _currentAxis == CutOffAxis.Vertical ? snapped.X : snapped.Y;
var sl = new SplitLine(position, _currentAxis);
InitializeFeaturePositions(sl);
_splitLines.Add(sl);
UpdateUI();
pnlPreview.Invalidate();
}
}
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
var worldPt = pnlPreview.PointControlToWorld(e.Location);
if (_dragLineIndex >= 0)
{
var sl = _splitLines[_dragLineIndex];
GetExtent(sl, out var start, out var end);
var pos = sl.Axis == CutOffAxis.Vertical ? worldPt.Y : worldPt.X;
pos = System.Math.Max(start, System.Math.Min(end, pos));
sl.FeaturePositions[_dragFeatureIndex] = pos;
pnlPreview.Invalidate();
}
else
{
var (lineIdx, featIdx) = HitTestFeatureHandle(worldPt);
if (lineIdx != _hoverLineIndex || featIdx != _hoverFeatureIndex)
{
_hoverLineIndex = lineIdx;
_hoverFeatureIndex = featIdx;
pnlPreview.Cursor = _hoverLineIndex >= 0
? (_splitLines[_hoverLineIndex].Axis == CutOffAxis.Vertical ? Cursors.SizeNS : Cursors.SizeWE)
: Cursors.Cross;
pnlPreview.Invalidate();
}
}
if (_placingLine)
{
_placingCursor = SnapToMidpoint(worldPt);
pnlPreview.Invalidate();
}
lblCursor.Text = $"Cursor: {worldPt.X:F2}, {worldPt.Y:F2}";
}
private void OnPreviewMouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && _dragLineIndex >= 0)
{
_dragLineIndex = -1;
_dragFeatureIndex = -1;
pnlPreview.Invalidate();
}
}
private (int lineIndex, int featureIndex) HitTestFeatureHandle(Vector worldPt)
{
if (radStraight.Checked) return (-1, -1);
var hitRadius = HandleRadius / pnlPreview.ViewScale;
for (var li = 0; li < _splitLines.Count; li++)
{
var sl = _splitLines[li];
for (var fi = 0; fi < sl.FeaturePositions.Count; fi++)
{
var center = GetFeatureHandleWorld(sl, fi);
var dx = worldPt.X - center.X;
var dy = worldPt.Y - center.Y;
if (dx * dx + dy * dy <= hitRadius * hitRadius)
return (li, fi);
}
}
return (-1, -1);
}
private Vector GetFeatureHandleWorld(SplitLine sl, int featureIndex)
{
var pos = sl.FeaturePositions[featureIndex];
return sl.Axis == CutOffAxis.Vertical
? new Vector(sl.Position, pos)
: new Vector(pos, sl.Position);
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Space)
{
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
pnlPreview.Invalidate();
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 / pnlPreview.ViewScale;
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 (drawn on top of entities via SplitPreview) ---
private void PaintOverlays(Graphics g)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
// Piece color overlays
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];
var tl = pnlPreview.PointWorldToGraph(r.Left, r.Top);
var br = pnlPreview.PointWorldToGraph(r.Right, r.Bottom);
g.FillRectangle(brush, System.Math.Min(tl.X, br.X), System.Math.Min(tl.Y, br.Y),
System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y));
}
// Piece number and dimension labels at center of each region
if (regions.Count > 1)
{
using var labelFont = new Font("Segoe UI", 14f, FontStyle.Bold, GraphicsUnit.Pixel);
using var dimFont = new Font("Segoe UI", 11f, FontStyle.Regular, GraphicsUnit.Pixel);
using var labelBrush = new SolidBrush(Color.FromArgb(200, 255, 255, 255));
using var shadowBrush = new SolidBrush(Color.FromArgb(160, 0, 0, 0));
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
for (var i = 0; i < regions.Count; i++)
{
var r = regions[i];
var center = pnlPreview.PointWorldToGraph(r.Center.X, r.Center.Y);
var label = (i + 1).ToString();
var dim = $"{r.Width:F2} x {r.Length:F2}";
// Shadow offset for readability
g.DrawString(label, labelFont, shadowBrush, center.X + 1, center.Y - 7, sf);
g.DrawString(label, labelFont, labelBrush, center.X, center.Y - 8, sf);
g.DrawString(dim, dimFont, shadowBrush, center.X + 1, center.Y + 9, sf);
g.DrawString(dim, dimFont, labelBrush, center.X, center.Y + 8, sf);
}
}
// Split lines — trimmed at feature positions with feature contours
var parameters = GetCurrentParameters();
var feature = GetSplitFeature(parameters.Type);
using var splitPen = new Pen(Color.FromArgb(255, 82, 82));
splitPen.DashStyle = DashStyle.Dash;
using var featurePen = new Pen(Color.FromArgb(200, 255, 82, 82), 1.5f);
foreach (var sl in _splitLines)
{
GetExtent(sl, out var extStart, out var extEnd);
var isVert = sl.Axis == CutOffAxis.Vertical;
var margin = 10.0;
if (sl.FeaturePositions.Count == 0 || radStraight.Checked)
{
// No features — draw one continuous line
var p1 = isVert
? pnlPreview.PointWorldToGraph(sl.Position, extStart - margin)
: pnlPreview.PointWorldToGraph(extStart - margin, sl.Position);
var p2 = isVert
? pnlPreview.PointWorldToGraph(sl.Position, extEnd + margin)
: pnlPreview.PointWorldToGraph(extEnd + margin, sl.Position);
g.DrawLine(splitPen, p1, p2);
}
else
{
// Generate feature geometry and draw contours
var featureResult = feature.GenerateFeatures(sl, extStart, extEnd, parameters);
DrawFeatureEdge(g, featurePen, featureResult.NegativeSideEdge, isVert);
DrawFeatureEdge(g, featurePen, featureResult.PositiveSideEdge, isVert);
// Draw split line in segments between features
var halfExt = GetFeatureHalfExtent(parameters);
var sorted = new List<double>(sl.FeaturePositions);
sorted.Sort();
var cursor = extStart - margin;
foreach (var fc in sorted)
{
var gapStart = fc - halfExt;
if (gapStart > cursor)
{
var p1 = isVert
? pnlPreview.PointWorldToGraph(sl.Position, cursor)
: pnlPreview.PointWorldToGraph(cursor, sl.Position);
var p2 = isVert
? pnlPreview.PointWorldToGraph(sl.Position, gapStart)
: pnlPreview.PointWorldToGraph(gapStart, sl.Position);
g.DrawLine(splitPen, p1, p2);
}
cursor = fc + halfExt;
}
// Final segment after last feature
var end = extEnd + margin;
if (end > cursor)
{
var p1 = isVert
? pnlPreview.PointWorldToGraph(sl.Position, cursor)
: pnlPreview.PointWorldToGraph(cursor, sl.Position);
var p2 = isVert
? pnlPreview.PointWorldToGraph(sl.Position, end)
: pnlPreview.PointWorldToGraph(end, sl.Position);
g.DrawLine(splitPen, p1, p2);
}
}
}
// Placement preview line
if (_placingLine)
{
var isVert = _currentAxis == CutOffAxis.Vertical;
var snapped = _placingCursor;
var pos = isVert ? snapped.X : snapped.Y;
var margin = 10.0;
PointF pp1, pp2;
if (isVert)
{
pp1 = pnlPreview.PointWorldToGraph(pos, _drawingBounds.Bottom - margin);
pp2 = pnlPreview.PointWorldToGraph(pos, _drawingBounds.Top + margin);
}
else
{
pp1 = pnlPreview.PointWorldToGraph(_drawingBounds.Left - margin, pos);
pp2 = pnlPreview.PointWorldToGraph(_drawingBounds.Right + margin, pos);
}
using var previewPen = new Pen(Color.FromArgb(180, 255, 213, 79), 1.5f);
previewPen.DashStyle = DashStyle.DashDot;
g.DrawLine(previewPen, pp1, pp2);
}
// Feature position handles
if (!radStraight.Checked)
{
for (var li = 0; li < _splitLines.Count; li++)
{
var sl = _splitLines[li];
for (var fi = 0; fi < sl.FeaturePositions.Count; fi++)
{
var center = pnlPreview.PointWorldToGraph(GetFeatureHandleWorld(sl, fi));
var isDrag = li == _dragLineIndex && fi == _dragFeatureIndex;
var isHover = li == _hoverLineIndex && fi == _hoverFeatureIndex;
var fillColor = isDrag ? Color.FromArgb(255, 82, 82)
: isHover ? Color.FromArgb(255, 183, 77)
: Color.White;
using var fill = new SolidBrush(fillColor);
using var border = new Pen(Color.FromArgb(80, 80, 80));
g.FillEllipse(fill, center.X - HandleRadius, center.Y - HandleRadius,
HandleRadius * 2, HandleRadius * 2);
g.DrawEllipse(border, center.X - HandleRadius, center.Y - HandleRadius,
HandleRadius * 2, HandleRadius * 2);
}
}
}
}
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 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;
}
// --- 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";
}
// --- Feature rendering helpers ---
private static ISplitFeature GetSplitFeature(SplitType type)
{
return type switch
{
SplitType.WeldGapTabs => new WeldGapTabSplit(),
SplitType.SpikeGroove => new SpikeGrooveSplit(),
_ => new StraightSplit()
};
}
private static double GetFeatureHalfExtent(SplitParameters p)
{
return p.Type switch
{
SplitType.WeldGapTabs => p.TabWidth / 2,
SplitType.SpikeGroove => p.GrooveDepth * System.Math.Tan(OpenNest.Math.Angle.ToRadians(p.SpikeAngle / 2)),
_ => 0
};
}
private void DrawFeatureEdge(Graphics g, Pen pen, List<Geometry.Entity> entities, bool isVertical)
{
foreach (var entity in entities)
{
if (entity is Geometry.Line line)
{
var p1 = pnlPreview.PointWorldToGraph(line.StartPoint.X, line.StartPoint.Y);
var p2 = pnlPreview.PointWorldToGraph(line.EndPoint.X, line.EndPoint.Y);
g.DrawLine(pen, p1, p2);
}
}
}
// --- SplitPreview control ---
private class SplitPreview : EntityView
{
public Action<Graphics> DrawOverlays { get; set; }
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
DrawOverlays?.Invoke(e.Graphics);
}
}
}