feat: fit-to-plate splits use full plate work area with preview line

FitToPlate now places split lines at usable-width intervals so each
piece (except the last) fills the entire plate work area. Also adds a
live yellow preview line that follows the cursor during manual split
line placement, and piece dimension labels in the preview regions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 19:27:15 -04:00
parent 35218a7435
commit 269746b8a4
3 changed files with 51 additions and 28 deletions
+4 -12
View File
@@ -19,19 +19,11 @@ public static class AutoSplitCalculator
if (verticalSplits < 0) verticalSplits = 0; if (verticalSplits < 0) verticalSplits = 0;
if (horizontalSplits < 0) horizontalSplits = 0; if (horizontalSplits < 0) horizontalSplits = 0;
if (verticalSplits > 0) for (var i = 1; i <= verticalSplits; i++)
{ lines.Add(new SplitLine(partBounds.X + usableWidth * i, CutOffAxis.Vertical));
var spacing = partBounds.Width / (verticalSplits + 1);
for (var i = 1; i <= verticalSplits; i++)
lines.Add(new SplitLine(partBounds.X + spacing * i, CutOffAxis.Vertical));
}
if (horizontalSplits > 0) for (var i = 1; i <= horizontalSplits; i++)
{ lines.Add(new SplitLine(partBounds.Y + usableHeight * i, CutOffAxis.Horizontal));
var spacing = partBounds.Length / (horizontalSplits + 1);
for (var i = 1; i <= horizontalSplits; i++)
lines.Add(new SplitLine(partBounds.Y + spacing * i, CutOffAxis.Horizontal));
}
return lines; return lines;
} }
+1 -1
View File
@@ -42,7 +42,7 @@ public class AutoSplitCalculatorTests
Assert.Single(lines); Assert.Single(lines);
Assert.Equal(CutOffAxis.Vertical, lines[0].Axis); Assert.Equal(CutOffAxis.Vertical, lines[0].Axis);
Assert.Equal(50.0, lines[0].Position, 1); Assert.Equal(58.0, lines[0].Position, 1);
} }
[Fact] [Fact]
+46 -15
View File
@@ -18,6 +18,7 @@ public partial class SplitDrawingForm : Form
private readonly List<SplitLine> _splitLines = new(); private readonly List<SplitLine> _splitLines = new();
private CutOffAxis _currentAxis = CutOffAxis.Vertical; private CutOffAxis _currentAxis = CutOffAxis.Vertical;
private bool _placingLine; private bool _placingLine;
private Vector _placingCursor;
// Feature handle drag state // Feature handle drag state
private int _dragLineIndex = -1; private int _dragLineIndex = -1;
@@ -78,12 +79,8 @@ public partial class SplitDrawingForm : Form
if (usable > 0) if (usable > 0)
{ {
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1; var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
if (splits > 0) for (var i = 1; i <= splits; i++)
{ _splitLines.Add(new SplitLine(_drawingBounds.X + usable * i, CutOffAxis.Vertical));
var step = _drawingBounds.Width / (splits + 1);
for (var i = 1; i <= splits; i++)
_splitLines.Add(new SplitLine(_drawingBounds.X + step * i, CutOffAxis.Vertical));
}
} }
} }
else if (axisIndex == 2) else if (axisIndex == 2)
@@ -92,12 +89,8 @@ public partial class SplitDrawingForm : Form
if (usable > 0) if (usable > 0)
{ {
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1; var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
if (splits > 0) for (var i = 1; i <= splits; i++)
{ _splitLines.Add(new SplitLine(_drawingBounds.Y + usable * i, CutOffAxis.Horizontal));
var step = _drawingBounds.Length / (splits + 1);
for (var i = 1; i <= splits; i++)
_splitLines.Add(new SplitLine(_drawingBounds.Y + step * i, CutOffAxis.Horizontal));
}
} }
} }
else else
@@ -293,6 +286,12 @@ public partial class SplitDrawingForm : Form
} }
} }
if (_placingLine)
{
_placingCursor = SnapToMidpoint(worldPt);
pnlPreview.Invalidate();
}
lblCursor.Text = $"Cursor: {worldPt.X:F2}, {worldPt.Y:F2}"; lblCursor.Text = $"Cursor: {worldPt.X:F2}, {worldPt.Y:F2}";
} }
@@ -339,6 +338,7 @@ public partial class SplitDrawingForm : Form
if (keyData == Keys.Space) if (keyData == Keys.Space)
{ {
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical; _currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
pnlPreview.Invalidate();
return true; return true;
} }
if (keyData == Keys.Escape) if (keyData == Keys.Escape)
@@ -381,10 +381,11 @@ public partial class SplitDrawingForm : Form
System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y)); System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y));
} }
// Piece number labels at center of each region // Piece number and dimension labels at center of each region
if (regions.Count > 1) if (regions.Count > 1)
{ {
using var labelFont = new Font("Segoe UI", 14f, FontStyle.Bold, GraphicsUnit.Pixel); 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 labelBrush = new SolidBrush(Color.FromArgb(200, 255, 255, 255));
using var shadowBrush = new SolidBrush(Color.FromArgb(160, 0, 0, 0)); using var shadowBrush = new SolidBrush(Color.FromArgb(160, 0, 0, 0));
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
@@ -394,10 +395,13 @@ public partial class SplitDrawingForm : Form
var r = regions[i]; var r = regions[i];
var center = pnlPreview.PointWorldToGraph(r.Center.X, r.Center.Y); var center = pnlPreview.PointWorldToGraph(r.Center.X, r.Center.Y);
var label = (i + 1).ToString(); var label = (i + 1).ToString();
var dim = $"{r.Width:F2} x {r.Length:F2}";
// Shadow offset for readability // Shadow offset for readability
g.DrawString(label, labelFont, shadowBrush, center.X + 1, center.Y + 1, sf); g.DrawString(label, labelFont, shadowBrush, center.X + 1, center.Y - 7, sf);
g.DrawString(label, labelFont, labelBrush, center.X, center.Y, 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);
} }
} }
@@ -469,6 +473,33 @@ public partial class SplitDrawingForm : Form
} }
} }
// Placement preview line
if (_placingLine && _placingCursor != null)
{
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 // Feature position handles
if (!radStraight.Checked) if (!radStraight.Checked)
{ {