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)
|
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 a1 = line1.EndPoint.Y - line1.StartPoint.Y;
|
||||||
var b1 = line1.StartPoint.X - line1.EndPoint.X;
|
var b1 = line1.StartPoint.X - line1.EndPoint.X;
|
||||||
@@ -240,7 +248,7 @@ namespace OpenNest.Geometry
|
|||||||
var y = (a1 * c2 - a2 * c1) / d;
|
var y = (a1 * c2 - a2 * c1) / d;
|
||||||
|
|
||||||
pt = new Vector(x, y);
|
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)
|
internal static bool Intersects(Line line, Shape shape, out List<Vector> pts)
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
Vector intersection;
|
Vector intersection;
|
||||||
|
|
||||||
if (Intersect.Intersects(offsetLine, lastOffsetLine, out intersection))
|
if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out intersection))
|
||||||
{
|
{
|
||||||
offsetLine.StartPoint = intersection;
|
offsetLine.StartPoint = intersection;
|
||||||
lastOffsetLine.EndPoint = intersection;
|
lastOffsetLine.EndPoint = intersection;
|
||||||
@@ -558,6 +558,46 @@ namespace OpenNest.Geometry
|
|||||||
throw new NotImplementedException();
|
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>
|
/// <summary>
|
||||||
/// Gets the closest point on the shape to the given point.
|
/// Gets the closest point on the shape to the given point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
// Add chord tolerance to compensate for inscribed polygon chords
|
// Add chord tolerance to compensate for inscribed polygon chords
|
||||||
// being inside the actual offset arcs.
|
// 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)
|
if (offsetEntity == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -71,7 +71,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
{
|
{
|
||||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||||
|
|
||||||
if (offsetEntity == null)
|
if (offsetEntity == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -109,7 +109,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
{
|
{
|
||||||
var offsetEntity = shape.OffsetEntity(spacing + chordTolerance, OffsetSide.Left) as Shape;
|
var offsetEntity = shape.OffsetOutward(spacing + chordTolerance);
|
||||||
|
|
||||||
if (offsetEntity == null)
|
if (offsetEntity == null)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -26,15 +26,19 @@ public class SpikeGrooveSplit : ISplitFeature
|
|||||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||||
var pos = line.Position;
|
var pos = line.Position;
|
||||||
|
|
||||||
// Place pairs evenly: one near each end, with margin
|
// Use custom positions if provided, otherwise place evenly with margin
|
||||||
var margin = extent * 0.15;
|
|
||||||
var pairPositions = new List<double>();
|
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);
|
pairPositions.Add(extentStart + extent / 2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var margin = extent * 0.15;
|
||||||
var usable = extent - 2 * margin;
|
var usable = extent - 2 * margin;
|
||||||
for (var i = 0; i < pairCount; i++)
|
for (var i = 0; i < pairCount; i++)
|
||||||
pairPositions.Add(extentStart + margin + usable * i / (pairCount - 1));
|
pairPositions.Add(extentStart + margin + usable * i / (pairCount - 1));
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest;
|
namespace OpenNest;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -9,6 +11,13 @@ public class SplitLine
|
|||||||
public double Position { get; }
|
public double Position { get; }
|
||||||
public CutOffAxis Axis { 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)
|
public SplitLine(double position, CutOffAxis axis)
|
||||||
{
|
{
|
||||||
Position = position;
|
Position = position;
|
||||||
|
|||||||
@@ -18,8 +18,18 @@ public class WeldGapTabSplit : ISplitFeature
|
|||||||
var tabWidth = parameters.TabWidth;
|
var tabWidth = parameters.TabWidth;
|
||||||
var tabHeight = parameters.TabHeight;
|
var tabHeight = parameters.TabHeight;
|
||||||
|
|
||||||
// Evenly space tabs along the split line
|
// Use custom positions if provided, otherwise evenly space
|
||||||
var spacing = extent / (tabCount + 1);
|
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 negEntities = new List<Entity>();
|
||||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||||
@@ -30,9 +40,9 @@ public class WeldGapTabSplit : ISplitFeature
|
|||||||
|
|
||||||
var cursor = extentStart;
|
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 tabStart = tabCenter - tabWidth / 2;
|
||||||
var tabEnd = tabCenter + tabWidth / 2;
|
var tabEnd = tabCenter + tabWidth / 2;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
if (perimeter != null)
|
if (perimeter != null)
|
||||||
{
|
{
|
||||||
var offsetEntity = perimeter.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
var offsetEntity = perimeter.OffsetOutward(spacing);
|
||||||
|
|
||||||
if (offsetEntity != null)
|
if (offsetEntity != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ namespace OpenNest.Controls
|
|||||||
origin.Y += (Size.Height - lastSize.Height) * 0.5f;
|
origin.Y += (Size.Height - lastSize.Height) * 0.5f;
|
||||||
|
|
||||||
lastSize = Size;
|
lastSize = Size;
|
||||||
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public float LengthWorldToGui(double length)
|
public float LengthWorldToGui(double length)
|
||||||
|
|||||||
+672
-620
File diff suppressed because it is too large
Load Diff
+234
-109
@@ -4,6 +4,7 @@ using System.Drawing;
|
|||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using OpenNest.Controls;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
@@ -18,13 +19,12 @@ public partial class SplitDrawingForm : Form
|
|||||||
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
|
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
|
||||||
private bool _placingLine;
|
private bool _placingLine;
|
||||||
|
|
||||||
// Zoom/pan state
|
// Feature handle drag state
|
||||||
private float _zoom = 1f;
|
private int _dragLineIndex = -1;
|
||||||
private PointF _pan;
|
private int _dragFeatureIndex = -1;
|
||||||
private Point _lastMouse;
|
private int _hoverLineIndex = -1;
|
||||||
private bool _panning;
|
private int _hoverFeatureIndex = -1;
|
||||||
|
private const float HandleRadius = 5f;
|
||||||
// Snap threshold in screen pixels
|
|
||||||
private const double SnapThreshold = 5.0;
|
private const double SnapThreshold = 5.0;
|
||||||
|
|
||||||
public List<Drawing> ResultDrawings { get; private set; }
|
public List<Drawing> ResultDrawings { get; private set; }
|
||||||
@@ -33,20 +33,20 @@ public partial class SplitDrawingForm : Form
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
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;
|
_drawing = drawing;
|
||||||
_drawingEntities = ConvertProgram.ToGeometry(drawing.Program)
|
_drawingEntities = ConvertProgram.ToGeometry(drawing.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
_drawingBounds = drawing.Program.BoundingBox();
|
_drawingBounds = drawing.Program.BoundingBox();
|
||||||
|
|
||||||
|
foreach (var entity in _drawingEntities)
|
||||||
|
entity.Layer.IsVisible = true;
|
||||||
|
|
||||||
|
pnlPreview.Entities = _drawingEntities;
|
||||||
|
pnlPreview.DrawOverlays = PaintOverlays;
|
||||||
|
|
||||||
Text = $"Split Drawing: {drawing.Name}";
|
Text = $"Split Drawing: {drawing.Name}";
|
||||||
UpdateUI();
|
UpdateUI();
|
||||||
FitToView();
|
pnlPreview.ZoomToFit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Split Method Selection ---
|
// --- Split Method Selection ---
|
||||||
@@ -72,7 +72,7 @@ public partial class SplitDrawingForm : Form
|
|||||||
var overhang = GetCurrentParameters().FeatureOverhang;
|
var overhang = GetCurrentParameters().FeatureOverhang;
|
||||||
var axisIndex = cboSplitAxis.SelectedIndex;
|
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;
|
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||||
if (usable > 0)
|
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;
|
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||||
if (usable > 0)
|
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));
|
_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));
|
_splitLines.AddRange(AutoSplitCalculator.SplitByCount(_drawingBounds, hPieces, vPieces));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InitializeAllFeaturePositions();
|
||||||
UpdateUI();
|
UpdateUI();
|
||||||
pnlPreview.Invalidate();
|
pnlPreview.Invalidate();
|
||||||
}
|
}
|
||||||
@@ -134,8 +135,30 @@ public partial class SplitDrawingForm : Form
|
|||||||
{
|
{
|
||||||
grpTabParams.Visible = radTabs.Checked;
|
grpTabParams.Visible = radTabs.Checked;
|
||||||
grpSpikeParams.Visible = radSpike.Checked;
|
grpSpikeParams.Visible = radSpike.Checked;
|
||||||
|
InitializeAllFeaturePositions();
|
||||||
if (radFitToPlate.Checked)
|
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();
|
pnlPreview.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +176,7 @@ public partial class SplitDrawingForm : Form
|
|||||||
{
|
{
|
||||||
p.Type = SplitType.SpikeGroove;
|
p.Type = SplitType.SpikeGroove;
|
||||||
p.SpikeDepth = (double)nudSpikeDepth.Value;
|
p.SpikeDepth = (double)nudSpikeDepth.Value;
|
||||||
p.GrooveDepth = (double)nudGrooveDepth.Value;
|
p.GrooveDepth = p.SpikeDepth + (double)nudGrooveDepth.Value;
|
||||||
p.SpikeWeldGap = (double)nudSpikeWeldGap.Value;
|
p.SpikeWeldGap = (double)nudSpikeWeldGap.Value;
|
||||||
p.SpikeAngle = (double)nudSpikeAngle.Value;
|
p.SpikeAngle = (double)nudSpikeAngle.Value;
|
||||||
p.SpikePairCount = (int)nudSpikePairCount.Value;
|
p.SpikePairCount = (int)nudSpikePairCount.Value;
|
||||||
@@ -161,51 +184,162 @@ public partial class SplitDrawingForm : Form
|
|||||||
return p;
|
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)
|
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);
|
_dragLineIndex = lineIdx;
|
||||||
var snapped = SnapToMidpoint(pt);
|
_dragFeatureIndex = featIdx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split line placement
|
||||||
|
if (radManual.Checked && _placingLine)
|
||||||
|
{
|
||||||
|
var snapped = SnapToMidpoint(worldPt);
|
||||||
var position = _currentAxis == CutOffAxis.Vertical ? snapped.X : snapped.Y;
|
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();
|
UpdateUI();
|
||||||
pnlPreview.Invalidate();
|
pnlPreview.Invalidate();
|
||||||
}
|
}
|
||||||
else if (e.Button == MouseButtons.Middle)
|
|
||||||
{
|
|
||||||
_panning = true;
|
|
||||||
_lastMouse = e.Location;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (_panning)
|
var worldPt = pnlPreview.PointControlToWorld(e.Location);
|
||||||
|
|
||||||
|
if (_dragLineIndex >= 0)
|
||||||
{
|
{
|
||||||
_pan.X += e.X - _lastMouse.X;
|
var sl = _splitLines[_dragLineIndex];
|
||||||
_pan.Y += e.Y - _lastMouse.Y;
|
GetExtent(sl, out var start, out var end);
|
||||||
_lastMouse = e.Location;
|
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();
|
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: {worldPt.X:F2}, {worldPt.Y:F2}";
|
||||||
lblCursor.Text = $"Cursor: {drawPt.X:F2}, {drawPt.Y:F2}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPreviewMouseUp(object sender, MouseEventArgs e)
|
private void OnPreviewMouseUp(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Button == MouseButtons.Middle)
|
if (e.Button == MouseButtons.Left && _dragLineIndex >= 0)
|
||||||
_panning = false;
|
{
|
||||||
|
_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;
|
if (radStraight.Checked) return (-1, -1);
|
||||||
_zoom *= factor;
|
|
||||||
pnlPreview.Invalidate();
|
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)
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||||
@@ -227,7 +361,7 @@ public partial class SplitDrawingForm : Form
|
|||||||
{
|
{
|
||||||
var midX = _drawingBounds.Center.X;
|
var midX = _drawingBounds.Center.X;
|
||||||
var midY = _drawingBounds.Center.Y;
|
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)
|
if (_currentAxis == CutOffAxis.Vertical && System.Math.Abs(pt.X - midX) < threshold)
|
||||||
return new Vector(midX, pt.Y);
|
return new Vector(midX, pt.Y);
|
||||||
@@ -236,53 +370,65 @@ public partial class SplitDrawingForm : Form
|
|||||||
return pt;
|
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.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
g.Clear(Color.FromArgb(26, 26, 26));
|
|
||||||
|
|
||||||
g.TranslateTransform(_pan.X, _pan.Y);
|
// Piece color overlays
|
||||||
g.ScaleTransform(_zoom, -_zoom); // flip Y for CNC coordinates
|
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
|
// Split lines
|
||||||
using var partPen = new Pen(Color.LightGray, 1f / _zoom);
|
using var splitPen = new Pen(Color.FromArgb(255, 82, 82));
|
||||||
DrawEntities(g, _drawingEntities, partPen);
|
|
||||||
|
|
||||||
// Draw split lines
|
|
||||||
using var splitPen = new Pen(Color.FromArgb(255, 82, 82), 1f / _zoom);
|
|
||||||
splitPen.DashStyle = DashStyle.Dash;
|
splitPen.DashStyle = DashStyle.Dash;
|
||||||
foreach (var sl in _splitLines)
|
foreach (var sl in _splitLines)
|
||||||
{
|
{
|
||||||
|
PointF p1, p2;
|
||||||
if (sl.Axis == CutOffAxis.Vertical)
|
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
|
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
|
// Feature position handles
|
||||||
DrawPieceOverlays(g);
|
if (!radStraight.Checked)
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawEntities(Graphics g, List<Entity> entities, Pen pen)
|
|
||||||
{
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
{
|
||||||
if (entity is Line line)
|
for (var li = 0; li < _splitLines.Count; li++)
|
||||||
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(
|
var sl = _splitLines[li];
|
||||||
(float)(arc.Center.X - arc.Radius),
|
for (var fi = 0; fi < sl.FeaturePositions.Count; fi++)
|
||||||
(float)(arc.Center.Y - arc.Radius),
|
{
|
||||||
(float)(arc.Radius * 2),
|
var center = pnlPreview.PointWorldToGraph(GetFeatureHandleWorld(sl, fi));
|
||||||
(float)(arc.Radius * 2));
|
var isDrag = li == _dragLineIndex && fi == _dragFeatureIndex;
|
||||||
var startDeg = (float)OpenNest.Math.Angle.ToDegrees(arc.StartAngle);
|
var isHover = li == _hoverLineIndex && fi == _hoverFeatureIndex;
|
||||||
var sweepDeg = (float)OpenNest.Math.Angle.ToDegrees(arc.EndAngle - arc.StartAngle);
|
var fillColor = isDrag ? Color.FromArgb(255, 82, 82)
|
||||||
if (rect.Width > 0 && rect.Height > 0)
|
: isHover ? Color.FromArgb(255, 183, 77)
|
||||||
g.DrawArc(pen, rect, startDeg, sweepDeg);
|
: 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)
|
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()
|
private List<Box> BuildPreviewRegions()
|
||||||
{
|
{
|
||||||
var verticals = _splitLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList();
|
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;
|
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 ---
|
// --- OK/Cancel ---
|
||||||
|
|
||||||
private void OnOK(object sender, EventArgs e)
|
private void OnOK(object sender, EventArgs e)
|
||||||
@@ -396,4 +508,17 @@ public partial class SplitDrawingForm : Form
|
|||||||
var pieceCount = _splitLines.Count == 0 ? 1 : BuildPreviewRegions().Count;
|
var pieceCount = _splitLines.Count == 0 ? 1 : BuildPreviewRegions().Count;
|
||||||
lblStatus.Text = $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces";
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
foreach (var shape in shapes)
|
||||||
{
|
{
|
||||||
var offsetEntity = shape.OffsetEntity(spacing, OffsetSide.Left) as Shape;
|
var offsetEntity = shape.OffsetOutward(spacing);
|
||||||
|
|
||||||
if (offsetEntity == null)
|
if (offsetEntity == null)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user