feat: overhaul SplitDrawingForm — EntityView, draggable feature handles, UI fixes
- Replace raw Panel with EntityView (via SplitPreview subclass) for proper zoom-to-point, middle-button pan, and double-buffered rendering - Add draggable handles for tab/spike positions along split lines; positions flow through to WeldGapTabSplit and SpikeGrooveSplit via SplitLine.FeaturePositions - Fix OK/Cancel buttons hidden off-screen by putting them in a bottom-docked panel - Fix DrawControl not invalidating on resize - Swap plate Width/Length label order, default edge spacing to 0.5 - Rename tab labels: Tab Width→Tab Length, Tab Height→Weld Gap, default count 2 - Spike depth now calculated (read-only), groove depth means positioning depth beyond spike tip (default 0.125), converted to total depth internally - Set entity layers visible so EntityView renders them Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,14 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
|
||||
internal static bool Intersects(Line line1, Line line2, out Vector pt)
|
||||
{
|
||||
if (!IntersectsUnbounded(line1, line2, out pt))
|
||||
return false;
|
||||
|
||||
return line1.BoundingBox.Contains(pt) && line2.BoundingBox.Contains(pt);
|
||||
}
|
||||
|
||||
internal static bool IntersectsUnbounded(Line line1, Line line2, out Vector pt)
|
||||
{
|
||||
var a1 = line1.EndPoint.Y - line1.StartPoint.Y;
|
||||
var b1 = line1.StartPoint.X - line1.EndPoint.X;
|
||||
@@ -240,7 +248,7 @@ namespace OpenNest.Geometry
|
||||
var y = (a1 * c2 - a2 * c1) / d;
|
||||
|
||||
pt = new Vector(x, y);
|
||||
return line1.BoundingBox.Contains(pt) && line2.BoundingBox.Contains(pt);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool Intersects(Line line, Shape shape, out List<Vector> pts)
|
||||
|
||||
@@ -534,7 +534,7 @@ namespace OpenNest.Geometry
|
||||
{
|
||||
Vector intersection;
|
||||
|
||||
if (Intersect.Intersects(offsetLine, lastOffsetLine, out intersection))
|
||||
if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out intersection))
|
||||
{
|
||||
offsetLine.StartPoint = intersection;
|
||||
lastOffsetLine.EndPoint = intersection;
|
||||
@@ -558,6 +558,46 @@ namespace OpenNest.Geometry
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offsets the shape outward by the given distance, detecting winding direction
|
||||
/// to choose the correct offset side. Falls back to the opposite side if the
|
||||
/// bounding box shrinks (indicating the offset went inward).
|
||||
/// </summary>
|
||||
public Shape OffsetOutward(double distance)
|
||||
{
|
||||
var poly = ToPolygon();
|
||||
var side = poly.Vertices.Count >= 3 && poly.RotationDirection() == RotationType.CW
|
||||
? OffsetSide.Left
|
||||
: OffsetSide.Right;
|
||||
|
||||
var result = OffsetEntity(distance, side) as Shape;
|
||||
|
||||
if (result == null)
|
||||
return null;
|
||||
|
||||
UpdateBounds();
|
||||
var originalBB = BoundingBox;
|
||||
result.UpdateBounds();
|
||||
var offsetBB = result.BoundingBox;
|
||||
|
||||
if (offsetBB.Width < originalBB.Width || offsetBB.Length < originalBB.Length)
|
||||
{
|
||||
Trace.TraceWarning(
|
||||
"Shape.OffsetOutward: offset shrank bounding box " +
|
||||
$"(original={originalBB.Width:F3}x{originalBB.Length:F3}, " +
|
||||
$"offset={offsetBB.Width:F3}x{offsetBB.Length:F3}). " +
|
||||
"Retrying with opposite side.");
|
||||
|
||||
var opposite = side == OffsetSide.Left ? OffsetSide.Right : OffsetSide.Left;
|
||||
var retry = OffsetEntity(distance, opposite) as Shape;
|
||||
|
||||
if (retry != null)
|
||||
result = retry;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the closest point on the shape to the given point.
|
||||
/// </summary>
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace OpenNest
|
||||
{
|
||||
// Add chord tolerance to compensate for inscribed polygon chords
|
||||
// being inside the actual offset arcs.
|
||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
@@ -71,7 +71,7 @@ namespace OpenNest
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
@@ -109,7 +109,7 @@ namespace OpenNest
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
@@ -26,15 +26,19 @@ public class SpikeGrooveSplit : ISplitFeature
|
||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||
var pos = line.Position;
|
||||
|
||||
// Place pairs evenly: one near each end, with margin
|
||||
var margin = extent * 0.15;
|
||||
// Use custom positions if provided, otherwise place evenly with margin
|
||||
var pairPositions = new List<double>();
|
||||
if (pairCount == 1)
|
||||
if (line.FeaturePositions.Count > 0)
|
||||
{
|
||||
pairPositions.AddRange(line.FeaturePositions);
|
||||
}
|
||||
else if (pairCount == 1)
|
||||
{
|
||||
pairPositions.Add(extentStart + extent / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var margin = extent * 0.15;
|
||||
var usable = extent - 2 * margin;
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
pairPositions.Add(extentStart + margin + usable * i / (pairCount - 1));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,6 +11,13 @@ public class SplitLine
|
||||
public double Position { get; }
|
||||
public CutOffAxis Axis { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional custom center positions for features (tabs/spikes) along the split line.
|
||||
/// Values are absolute coordinates on the perpendicular axis.
|
||||
/// When empty, feature generators use their default even spacing.
|
||||
/// </summary>
|
||||
public List<double> FeaturePositions { get; set; } = new();
|
||||
|
||||
public SplitLine(double position, CutOffAxis axis)
|
||||
{
|
||||
Position = position;
|
||||
|
||||
@@ -18,8 +18,18 @@ public class WeldGapTabSplit : ISplitFeature
|
||||
var tabWidth = parameters.TabWidth;
|
||||
var tabHeight = parameters.TabHeight;
|
||||
|
||||
// Evenly space tabs along the split line
|
||||
var spacing = extent / (tabCount + 1);
|
||||
// Use custom positions if provided, otherwise evenly space
|
||||
var tabCenters = new List<double>();
|
||||
if (line.FeaturePositions.Count > 0)
|
||||
{
|
||||
tabCenters.AddRange(line.FeaturePositions);
|
||||
}
|
||||
else
|
||||
{
|
||||
var spacing = extent / (tabCount + 1);
|
||||
for (var i = 0; i < tabCount; i++)
|
||||
tabCenters.Add(extentStart + spacing * (i + 1));
|
||||
}
|
||||
|
||||
var negEntities = new List<Entity>();
|
||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||
@@ -30,9 +40,9 @@ public class WeldGapTabSplit : ISplitFeature
|
||||
|
||||
var cursor = extentStart;
|
||||
|
||||
for (var i = 0; i < tabCount; i++)
|
||||
for (var i = 0; i < tabCenters.Count; i++)
|
||||
{
|
||||
var tabCenter = extentStart + spacing * (i + 1);
|
||||
var tabCenter = tabCenters[i];
|
||||
var tabStart = tabCenter - tabWidth / 2;
|
||||
var tabEnd = tabCenter + tabWidth / 2;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
if (perimeter != null)
|
||||
{
|
||||
var offsetEntity = perimeter.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = perimeter.OffsetOutward(spacing);
|
||||
|
||||
if (offsetEntity != null)
|
||||
{
|
||||
|
||||
@@ -167,6 +167,7 @@ namespace OpenNest.Controls
|
||||
origin.Y += (Size.Height - lastSize.Height) * 0.5f;
|
||||
|
||||
lastSize = Size;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
public float LengthWorldToGui(double length)
|
||||
|
||||
1292
OpenNest/Forms/SplitDrawingForm.Designer.cs
generated
1292
OpenNest/Forms/SplitDrawingForm.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
@@ -18,13 +19,12 @@ public partial class SplitDrawingForm : Form
|
||||
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
|
||||
// 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; }
|
||||
@@ -33,20 +33,20 @@ public partial class SplitDrawingForm : Form
|
||||
{
|
||||
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)
|
||||
.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();
|
||||
FitToView();
|
||||
pnlPreview.ZoomToFit(true);
|
||||
}
|
||||
|
||||
// --- Split Method Selection ---
|
||||
@@ -72,7 +72,7 @@ public partial class SplitDrawingForm : Form
|
||||
var overhang = GetCurrentParameters().FeatureOverhang;
|
||||
var axisIndex = cboSplitAxis.SelectedIndex;
|
||||
|
||||
if (axisIndex == 1) // Vertical Only — split the part width using the plate size
|
||||
if (axisIndex == 1)
|
||||
{
|
||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||
if (usable > 0)
|
||||
@@ -86,7 +86,7 @@ public partial class SplitDrawingForm : Form
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (axisIndex == 2) // Horizontal Only — split the part height using the plate size
|
||||
else if (axisIndex == 2)
|
||||
{
|
||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||
if (usable > 0)
|
||||
@@ -100,7 +100,7 @@ public partial class SplitDrawingForm : Form
|
||||
}
|
||||
}
|
||||
}
|
||||
else // Auto — both axes
|
||||
else
|
||||
{
|
||||
_splitLines.AddRange(AutoSplitCalculator.FitToPlate(_drawingBounds, plateW, plateH, spacing, overhang));
|
||||
}
|
||||
@@ -112,6 +112,7 @@ public partial class SplitDrawingForm : Form
|
||||
_splitLines.AddRange(AutoSplitCalculator.SplitByCount(_drawingBounds, hPieces, vPieces));
|
||||
}
|
||||
|
||||
InitializeAllFeaturePositions();
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
@@ -134,8 +135,30 @@ public partial class SplitDrawingForm : Form
|
||||
{
|
||||
grpTabParams.Visible = radTabs.Checked;
|
||||
grpSpikeParams.Visible = radSpike.Checked;
|
||||
InitializeAllFeaturePositions();
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines(); // overhang changed
|
||||
RecalculateAutoSplitLines();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private void UpdateSpikeDepth()
|
||||
{
|
||||
var grooveDepth = (double)nudGrooveDepth.Value;
|
||||
var weldGap = (double)nudSpikeWeldGap.Value;
|
||||
nudSpikeDepth.Value = (decimal)(grooveDepth + weldGap);
|
||||
}
|
||||
|
||||
private void OnSpikeParamChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateSpikeDepth();
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private void OnFeatureCountChanged(object sender, EventArgs e)
|
||||
{
|
||||
InitializeAllFeaturePositions();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
@@ -153,7 +176,7 @@ public partial class SplitDrawingForm : Form
|
||||
{
|
||||
p.Type = SplitType.SpikeGroove;
|
||||
p.SpikeDepth = (double)nudSpikeDepth.Value;
|
||||
p.GrooveDepth = (double)nudGrooveDepth.Value;
|
||||
p.GrooveDepth = p.SpikeDepth + (double)nudGrooveDepth.Value;
|
||||
p.SpikeWeldGap = (double)nudSpikeWeldGap.Value;
|
||||
p.SpikeAngle = (double)nudSpikeAngle.Value;
|
||||
p.SpikePairCount = (int)nudSpikePairCount.Value;
|
||||
@@ -161,51 +184,162 @@ public partial class SplitDrawingForm : Form
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Manual Split Line Placement ---
|
||||
// --- 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 && radManual.Checked && _placingLine)
|
||||
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)
|
||||
{
|
||||
var pt = ScreenToDrawing(e.Location);
|
||||
var snapped = SnapToMidpoint(pt);
|
||||
_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;
|
||||
_splitLines.Add(new SplitLine(position, _currentAxis));
|
||||
var sl = new SplitLine(position, _currentAxis);
|
||||
InitializeFeaturePositions(sl);
|
||||
_splitLines.Add(sl);
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
else if (e.Button == MouseButtons.Middle)
|
||||
{
|
||||
_panning = true;
|
||||
_lastMouse = e.Location;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (_panning)
|
||||
var worldPt = pnlPreview.PointControlToWorld(e.Location);
|
||||
|
||||
if (_dragLineIndex >= 0)
|
||||
{
|
||||
_pan.X += e.X - _lastMouse.X;
|
||||
_pan.Y += e.Y - _lastMouse.Y;
|
||||
_lastMouse = e.Location;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
var drawPt = ScreenToDrawing(e.Location);
|
||||
lblCursor.Text = $"Cursor: {drawPt.X:F2}, {drawPt.Y:F2}";
|
||||
lblCursor.Text = $"Cursor: {worldPt.X:F2}, {worldPt.Y:F2}";
|
||||
}
|
||||
|
||||
private void OnPreviewMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Middle)
|
||||
_panning = false;
|
||||
if (e.Button == MouseButtons.Left && _dragLineIndex >= 0)
|
||||
{
|
||||
_dragLineIndex = -1;
|
||||
_dragFeatureIndex = -1;
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewMouseWheel(object sender, MouseEventArgs e)
|
||||
private (int lineIndex, int featureIndex) HitTestFeatureHandle(Vector worldPt)
|
||||
{
|
||||
var factor = e.Delta > 0 ? 1.1f : 0.9f;
|
||||
_zoom *= factor;
|
||||
pnlPreview.Invalidate();
|
||||
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)
|
||||
@@ -227,7 +361,7 @@ public partial class SplitDrawingForm : Form
|
||||
{
|
||||
var midX = _drawingBounds.Center.X;
|
||||
var midY = _drawingBounds.Center.Y;
|
||||
var threshold = SnapThreshold / _zoom;
|
||||
var threshold = SnapThreshold / pnlPreview.ViewScale;
|
||||
|
||||
if (_currentAxis == CutOffAxis.Vertical && System.Math.Abs(pt.X - midX) < threshold)
|
||||
return new Vector(midX, pt.Y);
|
||||
@@ -236,53 +370,65 @@ public partial class SplitDrawingForm : Form
|
||||
return pt;
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
// --- Rendering (drawn on top of entities via SplitPreview) ---
|
||||
|
||||
private void OnPreviewPaint(object sender, PaintEventArgs e)
|
||||
private void PaintOverlays(Graphics g)
|
||||
{
|
||||
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
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Split lines
|
||||
using var splitPen = new Pen(Color.FromArgb(255, 82, 82));
|
||||
splitPen.DashStyle = DashStyle.Dash;
|
||||
foreach (var sl in _splitLines)
|
||||
{
|
||||
PointF p1, p2;
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
g.DrawLine(splitPen, (float)sl.Position, (float)(_drawingBounds.Bottom - 10), (float)sl.Position, (float)(_drawingBounds.Top + 10));
|
||||
{
|
||||
p1 = pnlPreview.PointWorldToGraph(sl.Position, _drawingBounds.Bottom - 10);
|
||||
p2 = pnlPreview.PointWorldToGraph(sl.Position, _drawingBounds.Top + 10);
|
||||
}
|
||||
else
|
||||
g.DrawLine(splitPen, (float)(_drawingBounds.Left - 10), (float)sl.Position, (float)(_drawingBounds.Right + 10), (float)sl.Position);
|
||||
{
|
||||
p1 = pnlPreview.PointWorldToGraph(_drawingBounds.Left - 10, sl.Position);
|
||||
p2 = pnlPreview.PointWorldToGraph(_drawingBounds.Right + 10, sl.Position);
|
||||
}
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
|
||||
// Draw piece color overlays
|
||||
DrawPieceOverlays(g);
|
||||
}
|
||||
|
||||
private void DrawEntities(Graphics g, List<Entity> entities, Pen pen)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
// Feature position handles
|
||||
if (!radStraight.Checked)
|
||||
{
|
||||
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)
|
||||
for (var li = 0; li < _splitLines.Count; li++)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,19 +443,6 @@ public partial class SplitDrawingForm : Form
|
||||
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();
|
||||
@@ -331,27 +464,6 @@ public partial class SplitDrawingForm : Form
|
||||
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)
|
||||
@@ -396,4 +508,17 @@ public partial class SplitDrawingForm : Form
|
||||
var pieceCount = _splitLines.Count == 0 ? 1 : BuildPreviewRegions().Count;
|
||||
lblStatus.Text = $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces";
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
OpenNest/Forms/SplitDrawingForm.resx
Normal file
126
OpenNest/Forms/SplitDrawingForm.resx
Normal file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>116, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -182,7 +182,7 @@ namespace OpenNest
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
||||
var offsetEntity = shape.OffsetOutward(spacing);
|
||||
|
||||
if (offsetEntity == null)
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user