feat: add split drawing feature for oversized parts
This commit is contained in:
@@ -31,6 +31,7 @@ Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
||||
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
||||
- **CutOffs** (`namespace OpenNest`): `CutOff` (axis-aligned cut line with position, axis, optional start/end limits), `CutOffAxis` enum (`Horizontal`, `Vertical`), `CutOffSettings` (clearance, overtravel, min segment length, direction), `CutDirection` enum (`TowardOrigin`, `AwayFromOrigin`). Cut-offs generate CNC `Program` objects with trimmed line segments that avoid parts.
|
||||
- **Splitting** (`Splitting/`, `namespace OpenNest`): `DrawingSplitter` splits a Drawing into multiple pieces along split lines. `ISplitFeature` strategy pattern with implementations: `StraightSplit` (clean edge), `WeldGapTabSplit` (rectangular tab spacers on one side), `SpikeGrooveSplit` (interlocking spike/V-groove pairs). `AutoSplitCalculator` computes split lines for fit-to-plate and split-by-count modes. Supporting types: `SplitLine`, `SplitParameters`, `SplitFeatureResult`.
|
||||
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
||||
|
||||
### OpenNest.Engine (class library, depends on Core)
|
||||
@@ -78,7 +79,7 @@ MCP server for Claude Code integration. Exposes nesting operations as MCP tools
|
||||
### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
|
||||
The UI application with MDI interface.
|
||||
|
||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), `SplitDrawingForm` (split oversized drawings into smaller pieces, launched from CadConverterForm), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan, supports temporary preview parts), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`, `ActionCutOff`.
|
||||
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
||||
|
||||
59
OpenNest.Core/Splitting/AutoSplitCalculator.cs
Normal file
59
OpenNest.Core/Splitting/AutoSplitCalculator.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public static class AutoSplitCalculator
|
||||
{
|
||||
public static List<SplitLine> FitToPlate(Box partBounds, double plateWidth, double plateHeight,
|
||||
double edgeSpacing, double featureOverhang)
|
||||
{
|
||||
var usableWidth = plateWidth - 2 * edgeSpacing - featureOverhang;
|
||||
var usableHeight = plateHeight - 2 * edgeSpacing - featureOverhang;
|
||||
|
||||
var lines = new List<SplitLine>();
|
||||
|
||||
var verticalSplits = usableWidth > 0 ? (int)System.Math.Ceiling(partBounds.Width / usableWidth) - 1 : 0;
|
||||
var horizontalSplits = usableHeight > 0 ? (int)System.Math.Ceiling(partBounds.Length / usableHeight) - 1 : 0;
|
||||
|
||||
if (verticalSplits < 0) verticalSplits = 0;
|
||||
if (horizontalSplits < 0) horizontalSplits = 0;
|
||||
|
||||
if (verticalSplits > 0)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public static List<SplitLine> SplitByCount(Box partBounds, int horizontalPieces, int verticalPieces)
|
||||
{
|
||||
var lines = new List<SplitLine>();
|
||||
|
||||
if (verticalPieces > 1)
|
||||
{
|
||||
var spacing = partBounds.Width / verticalPieces;
|
||||
for (var i = 1; i < verticalPieces; i++)
|
||||
lines.Add(new SplitLine(partBounds.X + spacing * i, CutOffAxis.Vertical));
|
||||
}
|
||||
|
||||
if (horizontalPieces > 1)
|
||||
{
|
||||
var spacing = partBounds.Length / horizontalPieces;
|
||||
for (var i = 1; i < horizontalPieces; i++)
|
||||
lines.Add(new SplitLine(partBounds.Y + spacing * i, CutOffAxis.Horizontal));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
375
OpenNest.Core/Splitting/DrawingSplitter.cs
Normal file
375
OpenNest.Core/Splitting/DrawingSplitter.cs
Normal file
@@ -0,0 +1,375 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Splits a Drawing into multiple pieces along split lines with optional feature geometry.
|
||||
/// </summary>
|
||||
public static class DrawingSplitter
|
||||
{
|
||||
public static List<Drawing> Split(Drawing drawing, List<SplitLine> splitLines, SplitParameters parameters)
|
||||
{
|
||||
if (splitLines.Count == 0)
|
||||
return new List<Drawing> { drawing };
|
||||
|
||||
// 1. Convert program to geometry -> ShapeProfile separates perimeter from cutouts
|
||||
// Filter out rapid-layer entities so ShapeBuilder doesn't chain cutouts to the perimeter
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
var profile = new ShapeProfile(entities);
|
||||
|
||||
// Decompose circles to arcs so all entities support SplitAt()
|
||||
DecomposeCircles(profile);
|
||||
|
||||
var perimeter = profile.Perimeter;
|
||||
var bounds = perimeter.BoundingBox;
|
||||
|
||||
// 2. Sort split lines by position, discard any outside the part
|
||||
var sortedLines = splitLines
|
||||
.Where(l => IsLineInsideBounds(l, bounds))
|
||||
.OrderBy(l => l.Position)
|
||||
.ToList();
|
||||
|
||||
if (sortedLines.Count == 0)
|
||||
return new List<Drawing> { drawing };
|
||||
|
||||
// 3. Build clip regions (grid cells between split lines)
|
||||
var regions = BuildClipRegions(sortedLines, bounds);
|
||||
|
||||
// 4. Get the split feature strategy
|
||||
var feature = GetFeature(parameters.Type);
|
||||
|
||||
// 5. For each region, clip the perimeter and build a new drawing
|
||||
var results = new List<Drawing>();
|
||||
var pieceIndex = 1;
|
||||
|
||||
foreach (var region in regions)
|
||||
{
|
||||
var pieceEntities = ClipPerimeterToRegion(perimeter, region, sortedLines, feature, parameters);
|
||||
|
||||
if (pieceEntities.Count == 0)
|
||||
continue;
|
||||
|
||||
// Assign cutouts fully inside this region
|
||||
var cutoutEntities = new List<Entity>();
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
{
|
||||
if (IsCutoutInRegion(cutout, region))
|
||||
cutoutEntities.AddRange(cutout.Entities);
|
||||
else if (DoesCutoutCrossSplitLine(cutout, sortedLines))
|
||||
{
|
||||
// Cutout crosses a split line -- clip it to this region too
|
||||
var clippedCutout = ClipCutoutToRegion(cutout, region);
|
||||
if (clippedCutout.Count > 0)
|
||||
cutoutEntities.AddRange(clippedCutout);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize origin: translate so bounding box starts at (0,0)
|
||||
var allEntities = new List<Entity>();
|
||||
allEntities.AddRange(pieceEntities);
|
||||
allEntities.AddRange(cutoutEntities);
|
||||
|
||||
var pieceBounds = allEntities.Select(e => e.BoundingBox).ToList().GetBoundingBox();
|
||||
var offsetX = -pieceBounds.X;
|
||||
var offsetY = -pieceBounds.Y;
|
||||
|
||||
foreach (var e in allEntities)
|
||||
e.Offset(offsetX, offsetY);
|
||||
|
||||
// Build program (ConvertGeometry.ToProgram internally identifies perimeter vs cutouts)
|
||||
var pgm = ConvertGeometry.ToProgram(allEntities);
|
||||
|
||||
// Create drawing with copied properties
|
||||
var piece = new Drawing($"{drawing.Name}-{pieceIndex}", pgm);
|
||||
piece.Color = drawing.Color;
|
||||
piece.Priority = drawing.Priority;
|
||||
piece.Material = drawing.Material;
|
||||
piece.Constraints = drawing.Constraints;
|
||||
piece.Customer = drawing.Customer;
|
||||
piece.Source = drawing.Source;
|
||||
|
||||
piece.Quantity.Required = drawing.Quantity.Required;
|
||||
|
||||
results.Add(piece);
|
||||
pieceIndex++;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void DecomposeCircles(ShapeProfile profile)
|
||||
{
|
||||
DecomposeCirclesInShape(profile.Perimeter);
|
||||
foreach (var cutout in profile.Cutouts)
|
||||
DecomposeCirclesInShape(cutout);
|
||||
}
|
||||
|
||||
private static void DecomposeCirclesInShape(Shape shape)
|
||||
{
|
||||
for (var i = shape.Entities.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (shape.Entities[i] is Circle circle)
|
||||
{
|
||||
var arc1 = new Arc(circle.Center, circle.Radius, 0, System.Math.PI);
|
||||
var arc2 = new Arc(circle.Center, circle.Radius, System.Math.PI, System.Math.PI * 2);
|
||||
shape.Entities.RemoveAt(i);
|
||||
shape.Entities.Insert(i, arc2);
|
||||
shape.Entities.Insert(i, arc1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLineInsideBounds(SplitLine line, Box bounds)
|
||||
{
|
||||
return line.Axis == CutOffAxis.Vertical
|
||||
? line.Position > bounds.Left + OpenNest.Math.Tolerance.Epsilon
|
||||
&& line.Position < bounds.Right - OpenNest.Math.Tolerance.Epsilon
|
||||
: line.Position > bounds.Bottom + OpenNest.Math.Tolerance.Epsilon
|
||||
&& line.Position < bounds.Top - OpenNest.Math.Tolerance.Epsilon;
|
||||
}
|
||||
|
||||
private static List<Box> BuildClipRegions(List<SplitLine> sortedLines, Box bounds)
|
||||
{
|
||||
var verticals = sortedLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList();
|
||||
var horizontals = sortedLines.Where(l => l.Axis == CutOffAxis.Horizontal).OrderBy(l => l.Position).ToList();
|
||||
|
||||
var xEdges = new List<double> { bounds.Left };
|
||||
xEdges.AddRange(verticals.Select(v => v.Position));
|
||||
xEdges.Add(bounds.Right);
|
||||
|
||||
var yEdges = new List<double> { bounds.Bottom };
|
||||
yEdges.AddRange(horizontals.Select(h => h.Position));
|
||||
yEdges.Add(bounds.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clip perimeter to a region using Clipper2, then recover original arcs and stitch in feature edges.
|
||||
/// </summary>
|
||||
private static List<Entity> ClipPerimeterToRegion(Shape perimeter, Box region,
|
||||
List<SplitLine> splitLines, ISplitFeature feature, SplitParameters parameters)
|
||||
{
|
||||
var perimPoly = perimeter.ToPolygonWithTolerance(0.01);
|
||||
|
||||
var regionPoly = new Polygon();
|
||||
regionPoly.Vertices.Add(new Vector(region.Left, region.Bottom));
|
||||
regionPoly.Vertices.Add(new Vector(region.Right, region.Bottom));
|
||||
regionPoly.Vertices.Add(new Vector(region.Right, region.Top));
|
||||
regionPoly.Vertices.Add(new Vector(region.Left, region.Top));
|
||||
regionPoly.Close();
|
||||
|
||||
// Reuse existing Clipper2 helpers from NoFitPolygon
|
||||
var subj = new Clipper2Lib.PathsD { NoFitPolygon.ToClipperPath(perimPoly) };
|
||||
var clip = new Clipper2Lib.PathsD { NoFitPolygon.ToClipperPath(regionPoly) };
|
||||
var result = Clipper2Lib.Clipper.Intersect(subj, clip, Clipper2Lib.FillRule.NonZero);
|
||||
|
||||
if (result.Count == 0)
|
||||
return new List<Entity>();
|
||||
|
||||
var clippedPoly = NoFitPolygon.FromClipperPath(result[0]);
|
||||
var clippedEntities = new List<Entity>();
|
||||
|
||||
var verts = clippedPoly.Vertices;
|
||||
for (var i = 0; i < verts.Count - 1; i++)
|
||||
{
|
||||
var start = verts[i];
|
||||
var end = verts[i + 1];
|
||||
|
||||
// Check if this edge lies on a split line -- replace with feature geometry
|
||||
var splitLine = FindSplitLineForEdge(start, end, splitLines);
|
||||
if (splitLine != null)
|
||||
{
|
||||
var extentStart = splitLine.Axis == CutOffAxis.Vertical
|
||||
? System.Math.Min(start.Y, end.Y)
|
||||
: System.Math.Min(start.X, end.X);
|
||||
var extentEnd = splitLine.Axis == CutOffAxis.Vertical
|
||||
? System.Math.Max(start.Y, end.Y)
|
||||
: System.Math.Max(start.X, end.X);
|
||||
|
||||
var featureResult = feature.GenerateFeatures(splitLine, extentStart, extentEnd, parameters);
|
||||
|
||||
var regionCenter = splitLine.Axis == CutOffAxis.Vertical
|
||||
? (region.Left + region.Right) / 2
|
||||
: (region.Bottom + region.Top) / 2;
|
||||
var isNegativeSide = regionCenter < splitLine.Position;
|
||||
var featureEdge = isNegativeSide ? featureResult.NegativeSideEdge : featureResult.PositiveSideEdge;
|
||||
|
||||
// Ensure feature edge direction matches the polygon winding
|
||||
if (featureEdge.Count > 0)
|
||||
{
|
||||
var featureStart = GetStartPoint(featureEdge[0]);
|
||||
var featureEnd = GetEndPoint(featureEdge[^1]);
|
||||
var edgeGoesForward = splitLine.Axis == CutOffAxis.Vertical
|
||||
? start.Y < end.Y : start.X < end.X;
|
||||
var featureGoesForward = splitLine.Axis == CutOffAxis.Vertical
|
||||
? featureStart.Y < featureEnd.Y : featureStart.X < featureEnd.X;
|
||||
|
||||
if (edgeGoesForward != featureGoesForward)
|
||||
{
|
||||
featureEdge = new List<Entity>(featureEdge);
|
||||
featureEdge.Reverse();
|
||||
foreach (var e in featureEdge)
|
||||
e.Reverse();
|
||||
}
|
||||
}
|
||||
|
||||
clippedEntities.AddRange(featureEdge);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to recover original arc for this chord edge
|
||||
var originalArc = FindMatchingArc(start, end, perimeter);
|
||||
if (originalArc != null)
|
||||
clippedEntities.Add(originalArc);
|
||||
else
|
||||
clippedEntities.Add(new Line(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure CW winding for perimeter (positive area = CCW in Polygon, so CW = negative)
|
||||
var shape = new Shape();
|
||||
shape.Entities.AddRange(clippedEntities);
|
||||
var poly = shape.ToPolygon();
|
||||
if (poly != null && poly.RotationDirection() != RotationType.CW)
|
||||
shape.Reverse();
|
||||
|
||||
return shape.Entities;
|
||||
}
|
||||
|
||||
private static SplitLine FindSplitLineForEdge(Vector start, Vector end, List<SplitLine> splitLines)
|
||||
{
|
||||
foreach (var sl in splitLines)
|
||||
{
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
{
|
||||
if (System.Math.Abs(start.X - sl.Position) < 0.1 && System.Math.Abs(end.X - sl.Position) < 0.1)
|
||||
return sl;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (System.Math.Abs(start.Y - sl.Position) < 0.1 && System.Math.Abs(end.Y - sl.Position) < 0.1)
|
||||
return sl;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search original perimeter for an arc whose circle matches this polygon chord edge.
|
||||
/// Returns a new arc segment between the chord endpoints if found.
|
||||
/// </summary>
|
||||
private static Arc FindMatchingArc(Vector start, Vector end, Shape perimeter)
|
||||
{
|
||||
foreach (var entity in perimeter.Entities)
|
||||
{
|
||||
if (entity is Arc arc)
|
||||
{
|
||||
var distStart = start.DistanceTo(arc.Center) - arc.Radius;
|
||||
var distEnd = end.DistanceTo(arc.Center) - arc.Radius;
|
||||
|
||||
if (System.Math.Abs(distStart) < 0.1 && System.Math.Abs(distEnd) < 0.1)
|
||||
{
|
||||
var startAngle = OpenNest.Math.Angle.NormalizeRad(arc.Center.AngleTo(start));
|
||||
var endAngle = OpenNest.Math.Angle.NormalizeRad(arc.Center.AngleTo(end));
|
||||
return new Arc(arc.Center, arc.Radius, startAngle, endAngle, arc.IsReversed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsCutoutInRegion(Shape cutout, Box region)
|
||||
{
|
||||
if (cutout.Entities.Count == 0) return false;
|
||||
var pt = GetStartPoint(cutout.Entities[0]);
|
||||
return region.Contains(pt);
|
||||
}
|
||||
|
||||
private static bool DoesCutoutCrossSplitLine(Shape cutout, List<SplitLine> splitLines)
|
||||
{
|
||||
var bb = cutout.BoundingBox;
|
||||
foreach (var sl in splitLines)
|
||||
{
|
||||
if (sl.Axis == CutOffAxis.Vertical && bb.Left < sl.Position && bb.Right > sl.Position)
|
||||
return true;
|
||||
if (sl.Axis == CutOffAxis.Horizontal && bb.Bottom < sl.Position && bb.Top > sl.Position)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Entity> ClipCutoutToRegion(Shape cutout, Box region)
|
||||
{
|
||||
var cutoutPoly = cutout.ToPolygonWithTolerance(0.01);
|
||||
var regionPoly = new Polygon();
|
||||
regionPoly.Vertices.Add(new Vector(region.Left, region.Bottom));
|
||||
regionPoly.Vertices.Add(new Vector(region.Right, region.Bottom));
|
||||
regionPoly.Vertices.Add(new Vector(region.Right, region.Top));
|
||||
regionPoly.Vertices.Add(new Vector(region.Left, region.Top));
|
||||
regionPoly.Close();
|
||||
|
||||
var subj = new Clipper2Lib.PathsD { NoFitPolygon.ToClipperPath(cutoutPoly) };
|
||||
var clip = new Clipper2Lib.PathsD { NoFitPolygon.ToClipperPath(regionPoly) };
|
||||
var result = Clipper2Lib.Clipper.Intersect(subj, clip, Clipper2Lib.FillRule.NonZero);
|
||||
|
||||
if (result.Count == 0)
|
||||
return new List<Entity>();
|
||||
|
||||
var clippedPoly = NoFitPolygon.FromClipperPath(result[0]);
|
||||
var lineEntities = new List<Entity>();
|
||||
var verts = clippedPoly.Vertices;
|
||||
for (var i = 0; i < verts.Count - 1; i++)
|
||||
lineEntities.Add(new Line(verts[i], verts[i + 1]));
|
||||
|
||||
// Ensure CCW winding for cutouts
|
||||
var shape = new Shape();
|
||||
shape.Entities.AddRange(lineEntities);
|
||||
var poly = shape.ToPolygon();
|
||||
if (poly != null && poly.RotationDirection() != RotationType.CCW)
|
||||
shape.Reverse();
|
||||
|
||||
return shape.Entities;
|
||||
}
|
||||
|
||||
private static Vector GetStartPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.StartPoint,
|
||||
Arc a => a.StartPoint(), // Arc.StartPoint() is a method
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector GetEndPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.EndPoint,
|
||||
Arc a => a.EndPoint(), // Arc.EndPoint() is a method
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static ISplitFeature GetFeature(SplitType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
SplitType.Straight => new StraightSplit(),
|
||||
SplitType.WeldGapTabs => new WeldGapTabSplit(),
|
||||
SplitType.SpikeGroove => new SpikeGrooveSplit(),
|
||||
_ => new StraightSplit()
|
||||
};
|
||||
}
|
||||
}
|
||||
22
OpenNest.Core/Splitting/ISplitFeature.cs
Normal file
22
OpenNest.Core/Splitting/ISplitFeature.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public class SplitFeatureResult
|
||||
{
|
||||
public List<Entity> NegativeSideEdge { get; }
|
||||
public List<Entity> PositiveSideEdge { get; }
|
||||
|
||||
public SplitFeatureResult(List<Entity> negativeSideEdge, List<Entity> positiveSideEdge)
|
||||
{
|
||||
NegativeSideEdge = negativeSideEdge;
|
||||
PositiveSideEdge = positiveSideEdge;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ISplitFeature
|
||||
{
|
||||
string Name { get; }
|
||||
SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters);
|
||||
}
|
||||
105
OpenNest.Core/Splitting/SpikeGrooveSplit.cs
Normal file
105
OpenNest.Core/Splitting/SpikeGrooveSplit.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Generates interlocking spike/V-groove pairs along the split edge.
|
||||
/// Spikes protrude from the positive side into the negative side.
|
||||
/// V-grooves on the negative side receive the spikes for self-alignment during welding.
|
||||
/// </summary>
|
||||
public class SpikeGrooveSplit : ISplitFeature
|
||||
{
|
||||
public string Name => "Spike / V-Groove";
|
||||
|
||||
public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters)
|
||||
{
|
||||
var extent = extentEnd - extentStart;
|
||||
var pairCount = parameters.SpikePairCount;
|
||||
var depth = parameters.SpikeDepth;
|
||||
var angleRad = OpenNest.Math.Angle.ToRadians(parameters.SpikeAngle / 2);
|
||||
var halfWidth = depth * System.Math.Tan(angleRad);
|
||||
|
||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||
var pos = line.Position;
|
||||
|
||||
// Place pairs evenly: one near each end, with margin
|
||||
var margin = extent * 0.15;
|
||||
var pairPositions = new List<double>();
|
||||
if (pairCount == 1)
|
||||
{
|
||||
pairPositions.Add(extentStart + extent / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var usable = extent - 2 * margin;
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
pairPositions.Add(extentStart + margin + usable * i / (pairCount - 1));
|
||||
}
|
||||
|
||||
var negEntities = BuildGrooveSide(pairPositions, halfWidth, depth, extentStart, extentEnd, pos, isVertical);
|
||||
var posEntities = BuildSpikeSide(pairPositions, halfWidth, depth, extentStart, extentEnd, pos, isVertical);
|
||||
|
||||
return new SplitFeatureResult(negEntities, posEntities);
|
||||
}
|
||||
|
||||
private static List<Entity> BuildGrooveSide(List<double> pairPositions, double halfWidth, double depth,
|
||||
double extentStart, double extentEnd, double pos, bool isVertical)
|
||||
{
|
||||
var entities = new List<Entity>();
|
||||
var cursor = extentStart;
|
||||
|
||||
foreach (var center in pairPositions)
|
||||
{
|
||||
var grooveStart = center - halfWidth;
|
||||
var grooveEnd = center + halfWidth;
|
||||
|
||||
if (grooveStart > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, grooveStart, isVertical));
|
||||
|
||||
entities.Add(MakeLine(pos, grooveStart, pos - depth, center, isVertical));
|
||||
entities.Add(MakeLine(pos - depth, center, pos, grooveEnd, isVertical));
|
||||
|
||||
cursor = grooveEnd;
|
||||
}
|
||||
|
||||
if (extentEnd > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, extentEnd, isVertical));
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static List<Entity> BuildSpikeSide(List<double> pairPositions, double halfWidth, double depth,
|
||||
double extentStart, double extentEnd, double pos, bool isVertical)
|
||||
{
|
||||
var entities = new List<Entity>();
|
||||
var cursor = extentEnd;
|
||||
|
||||
for (var i = pairPositions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var center = pairPositions[i];
|
||||
var spikeEnd = center + halfWidth;
|
||||
var spikeStart = center - halfWidth;
|
||||
|
||||
if (cursor > spikeEnd + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, spikeEnd, isVertical));
|
||||
|
||||
entities.Add(MakeLine(pos, spikeEnd, pos - depth, center, isVertical));
|
||||
entities.Add(MakeLine(pos - depth, center, pos, spikeStart, isVertical));
|
||||
|
||||
cursor = spikeStart;
|
||||
}
|
||||
|
||||
if (cursor > extentStart + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, extentStart, isVertical));
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static Line MakeLine(double splitAxis1, double along1, double splitAxis2, double along2, bool isVertical)
|
||||
{
|
||||
return isVertical
|
||||
? new Line(new Vector(splitAxis1, along1), new Vector(splitAxis2, along2))
|
||||
: new Line(new Vector(along1, splitAxis1), new Vector(along2, splitAxis2));
|
||||
}
|
||||
}
|
||||
17
OpenNest.Core/Splitting/SplitLine.cs
Normal file
17
OpenNest.Core/Splitting/SplitLine.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a split line at a position along an axis.
|
||||
/// For Vertical, Position is the X coordinate. For Horizontal, Position is the Y coordinate.
|
||||
/// </summary>
|
||||
public class SplitLine
|
||||
{
|
||||
public double Position { get; }
|
||||
public CutOffAxis Axis { get; }
|
||||
|
||||
public SplitLine(double position, CutOffAxis axis)
|
||||
{
|
||||
Position = position;
|
||||
Axis = axis;
|
||||
}
|
||||
}
|
||||
33
OpenNest.Core/Splitting/SplitParameters.cs
Normal file
33
OpenNest.Core/Splitting/SplitParameters.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace OpenNest;
|
||||
|
||||
public enum SplitType
|
||||
{
|
||||
Straight,
|
||||
WeldGapTabs,
|
||||
SpikeGroove
|
||||
}
|
||||
|
||||
public class SplitParameters
|
||||
{
|
||||
public SplitType Type { get; set; } = SplitType.Straight;
|
||||
|
||||
// Tab parameters
|
||||
public double TabWidth { get; set; } = 1.0;
|
||||
public double TabHeight { get; set; } = 0.125;
|
||||
public int TabCount { get; set; } = 3;
|
||||
|
||||
// Spike/Groove parameters
|
||||
public double SpikeDepth { get; set; } = 0.5;
|
||||
public double SpikeAngle { get; set; } = 60.0; // degrees
|
||||
public int SpikePairCount { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Max protrusion from the split edge (for auto-fit plate size calculation).
|
||||
/// </summary>
|
||||
public double FeatureOverhang => Type switch
|
||||
{
|
||||
SplitType.WeldGapTabs => TabHeight,
|
||||
SplitType.SpikeGroove => SpikeDepth,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
22
OpenNest.Core/Splitting/StraightSplit.cs
Normal file
22
OpenNest.Core/Splitting/StraightSplit.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
public class StraightSplit : ISplitFeature
|
||||
{
|
||||
public string Name => "Straight";
|
||||
|
||||
public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters)
|
||||
{
|
||||
var (negEdge, posEdge) = line.Axis == CutOffAxis.Vertical
|
||||
? (new Line(new Vector(line.Position, extentStart), new Vector(line.Position, extentEnd)),
|
||||
new Line(new Vector(line.Position, extentEnd), new Vector(line.Position, extentStart)))
|
||||
: (new Line(new Vector(extentStart, line.Position), new Vector(extentEnd, line.Position)),
|
||||
new Line(new Vector(extentEnd, line.Position), new Vector(extentStart, line.Position)));
|
||||
|
||||
return new SplitFeatureResult(
|
||||
new List<Entity> { negEdge },
|
||||
new List<Entity> { posEdge });
|
||||
}
|
||||
}
|
||||
82
OpenNest.Core/Splitting/WeldGapTabSplit.cs
Normal file
82
OpenNest.Core/Splitting/WeldGapTabSplit.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>
|
||||
/// Generates rectangular tabs on one side of the split edge (negative side).
|
||||
/// The positive side remains a straight line. Tabs act as weld-gap spacers.
|
||||
/// </summary>
|
||||
public class WeldGapTabSplit : ISplitFeature
|
||||
{
|
||||
public string Name => "Weld-Gap Tabs";
|
||||
|
||||
public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters)
|
||||
{
|
||||
var extent = extentEnd - extentStart;
|
||||
var tabCount = parameters.TabCount;
|
||||
var tabWidth = parameters.TabWidth;
|
||||
var tabHeight = parameters.TabHeight;
|
||||
|
||||
// Evenly space tabs along the split line
|
||||
var spacing = extent / (tabCount + 1);
|
||||
|
||||
var negEntities = new List<Entity>();
|
||||
var isVertical = line.Axis == CutOffAxis.Vertical;
|
||||
var pos = line.Position;
|
||||
|
||||
// Tabs protrude toward the negative side (lower coordinate on the split axis)
|
||||
var tabDir = -1.0;
|
||||
|
||||
var cursor = extentStart;
|
||||
|
||||
for (var i = 0; i < tabCount; i++)
|
||||
{
|
||||
var tabCenter = extentStart + spacing * (i + 1);
|
||||
var tabStart = tabCenter - tabWidth / 2;
|
||||
var tabEnd = tabCenter + tabWidth / 2;
|
||||
|
||||
if (isVertical)
|
||||
{
|
||||
if (tabStart > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(pos, cursor), new Vector(pos, tabStart)));
|
||||
|
||||
negEntities.Add(new Line(new Vector(pos, tabStart), new Vector(pos + tabDir * tabHeight, tabStart)));
|
||||
negEntities.Add(new Line(new Vector(pos + tabDir * tabHeight, tabStart), new Vector(pos + tabDir * tabHeight, tabEnd)));
|
||||
negEntities.Add(new Line(new Vector(pos + tabDir * tabHeight, tabEnd), new Vector(pos, tabEnd)));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tabStart > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(cursor, pos), new Vector(tabStart, pos)));
|
||||
|
||||
negEntities.Add(new Line(new Vector(tabStart, pos), new Vector(tabStart, pos + tabDir * tabHeight)));
|
||||
negEntities.Add(new Line(new Vector(tabStart, pos + tabDir * tabHeight), new Vector(tabEnd, pos + tabDir * tabHeight)));
|
||||
negEntities.Add(new Line(new Vector(tabEnd, pos + tabDir * tabHeight), new Vector(tabEnd, pos)));
|
||||
}
|
||||
|
||||
cursor = tabEnd;
|
||||
}
|
||||
|
||||
// Final segment from last tab to extent end
|
||||
if (isVertical)
|
||||
{
|
||||
if (extentEnd > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(pos, cursor), new Vector(pos, extentEnd)));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (extentEnd > cursor + OpenNest.Math.Tolerance.Epsilon)
|
||||
negEntities.Add(new Line(new Vector(cursor, pos), new Vector(extentEnd, pos)));
|
||||
}
|
||||
|
||||
// Positive side: plain straight line (reversed direction)
|
||||
var posEntities = new List<Entity>();
|
||||
if (isVertical)
|
||||
posEntities.Add(new Line(new Vector(pos, extentEnd), new Vector(pos, extentStart)));
|
||||
else
|
||||
posEntities.Add(new Line(new Vector(extentEnd, pos), new Vector(extentStart, pos)));
|
||||
|
||||
return new SplitFeatureResult(negEntities, posEntities);
|
||||
}
|
||||
}
|
||||
163
OpenNest.Tests/Splitting/DrawingSplitterTests.cs
Normal file
163
OpenNest.Tests/Splitting/DrawingSplitterTests.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class DrawingSplitterTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper: creates a Drawing from a rectangular perimeter.
|
||||
/// </summary>
|
||||
private static Drawing MakeRectangleDrawing(string name, double width, double height)
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(0, 0), new Vector(width, 0)),
|
||||
new Line(new Vector(width, 0), new Vector(width, height)),
|
||||
new Line(new Vector(width, height), new Vector(0, height)),
|
||||
new Line(new Vector(0, height), new Vector(0, 0))
|
||||
};
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
return new Drawing(name, pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_Rectangle_Vertical_ProducesTwoPieces()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("RECT", 100, 50);
|
||||
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.Equal("RECT-1", results[0].Name);
|
||||
Assert.Equal("RECT-2", results[1].Name);
|
||||
|
||||
// Each piece should have area close to half the original
|
||||
var totalArea = results.Sum(d => d.Area);
|
||||
Assert.Equal(drawing.Area, totalArea, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_Rectangle_Horizontal_ProducesTwoPieces()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("RECT", 100, 60);
|
||||
var splitLines = new List<SplitLine> { new SplitLine(30.0, CutOffAxis.Horizontal) };
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.Equal("RECT-1", results[0].Name);
|
||||
Assert.Equal("RECT-2", results[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_ThreePieces_NamesSequentially()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("PART", 150, 50);
|
||||
var splitLines = new List<SplitLine>
|
||||
{
|
||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||
new SplitLine(100.0, CutOffAxis.Vertical)
|
||||
};
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||
|
||||
Assert.Equal(3, results.Count);
|
||||
Assert.Equal("PART-1", results[0].Name);
|
||||
Assert.Equal("PART-2", results[1].Name);
|
||||
Assert.Equal("PART-3", results[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_CopiesDrawingProperties()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("PART", 100, 50);
|
||||
drawing.Color = System.Drawing.Color.Red;
|
||||
drawing.Priority = 5;
|
||||
|
||||
var results = DrawingSplitter.Split(drawing,
|
||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters());
|
||||
|
||||
Assert.All(results, d =>
|
||||
{
|
||||
Assert.Equal(System.Drawing.Color.Red, d.Color);
|
||||
Assert.Equal(5, d.Priority);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_PiecesNormalizedToOrigin()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("PART", 100, 50);
|
||||
var results = DrawingSplitter.Split(drawing,
|
||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters());
|
||||
|
||||
// Each piece's program bounding box should start near (0,0)
|
||||
foreach (var d in results)
|
||||
{
|
||||
var bb = d.Program.BoundingBox();
|
||||
Assert.True(bb.X < 1.0, $"Piece {d.Name} not normalized: X={bb.X}");
|
||||
Assert.True(bb.Y < 1.0, $"Piece {d.Name} not normalized: Y={bb.Y}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_WithCutout_AssignsCutoutToCorrectPiece()
|
||||
{
|
||||
// Rectangle 100x50 with a small square cutout at (20,20)-(30,30)
|
||||
var perimeterEntities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(0, 0), new Vector(100, 0)),
|
||||
new Line(new Vector(100, 0), new Vector(100, 50)),
|
||||
new Line(new Vector(100, 50), new Vector(0, 50)),
|
||||
new Line(new Vector(0, 50), new Vector(0, 0))
|
||||
};
|
||||
var cutoutEntities = new List<Entity>
|
||||
{
|
||||
new Line(new Vector(20, 20), new Vector(30, 20)),
|
||||
new Line(new Vector(30, 20), new Vector(30, 30)),
|
||||
new Line(new Vector(30, 30), new Vector(20, 30)),
|
||||
new Line(new Vector(20, 30), new Vector(20, 20))
|
||||
};
|
||||
var allEntities = new List<Entity>();
|
||||
allEntities.AddRange(perimeterEntities);
|
||||
allEntities.AddRange(cutoutEntities);
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(allEntities);
|
||||
var drawing = new Drawing("HOLE", pgm);
|
||||
|
||||
// Split at X=50 — cutout is in the left half
|
||||
var results = DrawingSplitter.Split(drawing,
|
||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters());
|
||||
|
||||
Assert.Equal(2, results.Count);
|
||||
// Left piece should have smaller area (has the cutout)
|
||||
Assert.True(results[0].Area < results[1].Area,
|
||||
"Left piece should have less area due to cutout");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_GridSplit_ProducesFourPieces()
|
||||
{
|
||||
var drawing = MakeRectangleDrawing("GRID", 100, 100);
|
||||
var splitLines = new List<SplitLine>
|
||||
{
|
||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||
new SplitLine(50.0, CutOffAxis.Horizontal)
|
||||
};
|
||||
var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters());
|
||||
|
||||
Assert.Equal(4, results.Count);
|
||||
Assert.Equal("GRID-1", results[0].Name);
|
||||
Assert.Equal("GRID-2", results[1].Name);
|
||||
Assert.Equal("GRID-3", results[2].Name);
|
||||
Assert.Equal("GRID-4", results[3].Name);
|
||||
}
|
||||
}
|
||||
136
OpenNest.Tests/Splitting/SplitFeatureTests.cs
Normal file
136
OpenNest.Tests/Splitting/SplitFeatureTests.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class SplitFeatureTests
|
||||
{
|
||||
[Fact]
|
||||
public void WeldGapTabSplit_Vertical_TabsOnNegativeSide()
|
||||
{
|
||||
var feature = new WeldGapTabSplit();
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.WeldGapTabs,
|
||||
TabWidth = 2.0,
|
||||
TabHeight = 0.25,
|
||||
TabCount = 2
|
||||
};
|
||||
|
||||
var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters);
|
||||
|
||||
// Positive side (right): single straight line (no tabs)
|
||||
Assert.Single(result.PositiveSideEdge);
|
||||
Assert.IsType<Line>(result.PositiveSideEdge[0]);
|
||||
|
||||
// Negative side (left): has tab protrusions — more than 1 entity
|
||||
Assert.True(result.NegativeSideEdge.Count > 1);
|
||||
|
||||
// All entities should be lines
|
||||
Assert.All(result.NegativeSideEdge, e => Assert.IsType<Line>(e));
|
||||
|
||||
// First entity starts at extent start, last ends at extent end
|
||||
var first = (Line)result.NegativeSideEdge[0];
|
||||
var last = (Line)result.NegativeSideEdge[^1];
|
||||
Assert.Equal(0.0, first.StartPoint.Y, 6);
|
||||
Assert.Equal(100.0, last.EndPoint.Y, 6);
|
||||
|
||||
// Tabs protrude in the negative-X direction (left of split line)
|
||||
var tabEntities = result.NegativeSideEdge.Cast<Line>().ToList();
|
||||
var minX = tabEntities.Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X));
|
||||
Assert.Equal(50.0 - 0.25, minX, 6); // tabHeight = 0.25
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WeldGapTabSplit_Name()
|
||||
{
|
||||
Assert.Equal("Weld-Gap Tabs", new WeldGapTabSplit().Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StraightSplit_Vertical_ProducesSingleLineEachSide()
|
||||
{
|
||||
var feature = new StraightSplit();
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var result = feature.GenerateFeatures(line, 10.0, 90.0, parameters);
|
||||
|
||||
Assert.Single(result.NegativeSideEdge);
|
||||
var negLine = Assert.IsType<Line>(result.NegativeSideEdge[0]);
|
||||
Assert.Equal(50.0, negLine.StartPoint.X, 6);
|
||||
Assert.Equal(10.0, negLine.StartPoint.Y, 6);
|
||||
Assert.Equal(50.0, negLine.EndPoint.X, 6);
|
||||
Assert.Equal(90.0, negLine.EndPoint.Y, 6);
|
||||
|
||||
Assert.Single(result.PositiveSideEdge);
|
||||
var posLine = Assert.IsType<Line>(result.PositiveSideEdge[0]);
|
||||
Assert.Equal(50.0, posLine.StartPoint.X, 6);
|
||||
Assert.Equal(90.0, posLine.StartPoint.Y, 6);
|
||||
Assert.Equal(50.0, posLine.EndPoint.X, 6);
|
||||
Assert.Equal(10.0, posLine.EndPoint.Y, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StraightSplit_Horizontal_ProducesSingleLineEachSide()
|
||||
{
|
||||
var feature = new StraightSplit();
|
||||
var line = new SplitLine(40.0, CutOffAxis.Horizontal);
|
||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
var result = feature.GenerateFeatures(line, 5.0, 95.0, parameters);
|
||||
|
||||
var negLine = Assert.IsType<Line>(result.NegativeSideEdge[0]);
|
||||
Assert.Equal(5.0, negLine.StartPoint.X, 6);
|
||||
Assert.Equal(40.0, negLine.StartPoint.Y, 6);
|
||||
Assert.Equal(95.0, negLine.EndPoint.X, 6);
|
||||
Assert.Equal(40.0, negLine.EndPoint.Y, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StraightSplit_Name()
|
||||
{
|
||||
Assert.Equal("Straight", new StraightSplit().Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpikeGrooveSplit_Vertical_TwoPairs_SpikesOnPositiveSide()
|
||||
{
|
||||
var feature = new SpikeGrooveSplit();
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.SpikeGroove,
|
||||
SpikeDepth = 1.0,
|
||||
SpikeAngle = 60.0,
|
||||
SpikePairCount = 2
|
||||
};
|
||||
|
||||
var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters);
|
||||
|
||||
// Both sides should have multiple entities (straight segments + spike/groove geometry)
|
||||
Assert.True(result.NegativeSideEdge.Count > 1, "Negative side should have groove geometry");
|
||||
Assert.True(result.PositiveSideEdge.Count > 1, "Positive side should have spike geometry");
|
||||
|
||||
// All entities should be lines
|
||||
Assert.All(result.NegativeSideEdge, e => Assert.IsType<Line>(e));
|
||||
Assert.All(result.PositiveSideEdge, e => Assert.IsType<Line>(e));
|
||||
|
||||
// Spikes protrude in negative-X direction (into the negative side's territory)
|
||||
var posLines = result.PositiveSideEdge.Cast<Line>().ToList();
|
||||
var minX = posLines.Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X));
|
||||
Assert.True(minX < 50.0, "Spikes should protrude past the split line");
|
||||
|
||||
// Grooves indent in the positive-X direction (into positive side's territory)
|
||||
var negLines = result.NegativeSideEdge.Cast<Line>().ToList();
|
||||
var maxX = negLines.Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X));
|
||||
Assert.True(maxX <= 50.0, "Grooves should not protrude past the split line");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpikeGrooveSplit_Name()
|
||||
{
|
||||
Assert.Equal("Spike / V-Groove", new SpikeGrooveSplit().Name);
|
||||
}
|
||||
}
|
||||
87
OpenNest.Tests/Splitting/SplitLineTests.cs
Normal file
87
OpenNest.Tests/Splitting/SplitLineTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class SplitLineTests
|
||||
{
|
||||
[Fact]
|
||||
public void SplitLine_Vertical_StoresPositionAsX()
|
||||
{
|
||||
var line = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
Assert.Equal(50.0, line.Position);
|
||||
Assert.Equal(CutOffAxis.Vertical, line.Axis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitLine_Horizontal_StoresPositionAsY()
|
||||
{
|
||||
var line = new SplitLine(30.0, CutOffAxis.Horizontal);
|
||||
Assert.Equal(30.0, line.Position);
|
||||
Assert.Equal(CutOffAxis.Horizontal, line.Axis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitParameters_Defaults()
|
||||
{
|
||||
var p = new SplitParameters();
|
||||
Assert.Equal(SplitType.Straight, p.Type);
|
||||
Assert.Equal(3, p.TabCount);
|
||||
Assert.Equal(1.0, p.TabWidth);
|
||||
Assert.Equal(0.125, p.TabHeight);
|
||||
Assert.Equal(2, p.SpikePairCount);
|
||||
}
|
||||
}
|
||||
|
||||
public class AutoSplitCalculatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void FitToPlate_SingleAxis_CalculatesCorrectSplits()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 100, 50);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 1.0, 0);
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Equal(CutOffAxis.Vertical, lines[0].Axis);
|
||||
Assert.Equal(50.0, lines[0].Position, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitToPlate_BothAxes_GeneratesGrid()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 200, 200);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 0, 0);
|
||||
|
||||
var verticals = lines.Where(l => l.Axis == CutOffAxis.Vertical).ToList();
|
||||
var horizontals = lines.Where(l => l.Axis == CutOffAxis.Horizontal).ToList();
|
||||
Assert.Equal(3, verticals.Count);
|
||||
Assert.Equal(3, horizontals.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitToPlate_AlreadyFits_ReturnsEmpty()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 50, 50);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 1.0, 0);
|
||||
Assert.Empty(lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitByCount_SingleAxis_EvenlySpaced()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 100, 50);
|
||||
var lines = AutoSplitCalculator.SplitByCount(partBounds, horizontalPieces: 1, verticalPieces: 3);
|
||||
|
||||
Assert.Equal(2, lines.Count);
|
||||
Assert.All(lines, l => Assert.Equal(CutOffAxis.Vertical, l.Axis));
|
||||
Assert.Equal(33.333, lines[0].Position, 2);
|
||||
Assert.Equal(66.667, lines[1].Position, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FitToPlate_AccountsForFeatureOverhang()
|
||||
{
|
||||
var partBounds = new Box(0, 0, 100, 50);
|
||||
var lines = AutoSplitCalculator.FitToPlate(partBounds, 60, 60, 1.0, 0.5);
|
||||
Assert.Single(lines);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,17 @@ namespace OpenNest.Forms
|
||||
Items = new BindingList<CadConverterItem>();
|
||||
dataGridView1.DataSource = Items;
|
||||
dataGridView1.DataError += dataGridView1_DataError;
|
||||
|
||||
var splitColumn = new DataGridViewButtonColumn
|
||||
{
|
||||
Name = "Split",
|
||||
HeaderText = "",
|
||||
Text = "Split",
|
||||
UseColumnTextForButtonValue = true,
|
||||
Width = 50
|
||||
};
|
||||
dataGridView1.Columns.Add(splitColumn);
|
||||
dataGridView1.CellContentClick += OnCellContentClick;
|
||||
}
|
||||
|
||||
private BindingList<CadConverterItem> Items { get; set; }
|
||||
@@ -96,6 +107,14 @@ namespace OpenNest.Forms
|
||||
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (item.SplitDrawings != null && item.SplitDrawings.Count > 0)
|
||||
{
|
||||
foreach (var splitDrawing in item.SplitDrawings)
|
||||
splitDrawing.Color = GetNextColor();
|
||||
drawings.AddRange(item.SplitDrawings);
|
||||
continue;
|
||||
}
|
||||
|
||||
var entities = item.Entities.Where(e => e.Layer.IsVisible && e.IsVisible).ToList();
|
||||
|
||||
if (entities.Count == 0)
|
||||
@@ -187,6 +206,43 @@ namespace OpenNest.Forms
|
||||
MessageBox.Show(e.Exception.Message);
|
||||
}
|
||||
|
||||
private void OnCellContentClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex < 0) return;
|
||||
if (dataGridView1.Columns[e.ColumnIndex].Name != "Split") return;
|
||||
|
||||
var item = Items[e.RowIndex];
|
||||
var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
|
||||
if (entities.Count == 0) return;
|
||||
|
||||
// Build a temporary drawing from the item's entities (same logic as GetDrawings)
|
||||
var shape = new ShapeProfile(entities);
|
||||
SetRotation(shape.Perimeter, RotationType.CW);
|
||||
foreach (var cutout in shape.Cutouts)
|
||||
SetRotation(cutout, RotationType.CCW);
|
||||
|
||||
var drawEntities = new List<Entity>();
|
||||
drawEntities.AddRange(shape.Perimeter.Entities);
|
||||
shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities));
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(drawEntities);
|
||||
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
||||
{
|
||||
var rapid = (RapidMove)pgm[0];
|
||||
pgm.Offset(-rapid.EndPoint);
|
||||
pgm.Codes.RemoveAt(0);
|
||||
}
|
||||
|
||||
var drawing = new Drawing(item.Name, pgm);
|
||||
|
||||
using var form = new SplitDrawingForm(drawing);
|
||||
if (form.ShowDialog(this) == DialogResult.OK && form.ResultDrawings?.Count > 1)
|
||||
{
|
||||
item.SplitDrawings = form.ResultDrawings;
|
||||
dataGridView1.InvalidateRow(e.RowIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void dataGridView1_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
|
||||
{
|
||||
dataGridView1.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
|
||||
@@ -308,6 +364,9 @@ namespace OpenNest.Forms
|
||||
|
||||
[Browsable(false)]
|
||||
public List<Entity> Entities { get; set; }
|
||||
|
||||
[Browsable(false)]
|
||||
public List<Drawing> SplitDrawings { get; set; }
|
||||
}
|
||||
|
||||
class ColorItem
|
||||
|
||||
641
OpenNest/Forms/SplitDrawingForm.Designer.cs
generated
Normal file
641
OpenNest/Forms/SplitDrawingForm.Designer.cs
generated
Normal file
@@ -0,0 +1,641 @@
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
partial class SplitDrawingForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.pnlSettings = new System.Windows.Forms.Panel();
|
||||
this.pnlPreview = new System.Windows.Forms.Panel();
|
||||
this.toolStrip = new System.Windows.Forms.ToolStrip();
|
||||
this.btnAddLine = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnDeleteLine = new System.Windows.Forms.ToolStripButton();
|
||||
this.statusStrip = new System.Windows.Forms.StatusStrip();
|
||||
this.lblStatus = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.lblCursor = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
|
||||
// Split Method group
|
||||
this.grpMethod = new System.Windows.Forms.GroupBox();
|
||||
this.radManual = new System.Windows.Forms.RadioButton();
|
||||
this.radFitToPlate = new System.Windows.Forms.RadioButton();
|
||||
this.radByCount = new System.Windows.Forms.RadioButton();
|
||||
|
||||
// Auto-fit group
|
||||
this.grpAutoFit = new System.Windows.Forms.GroupBox();
|
||||
this.lblPlateWidth = new System.Windows.Forms.Label();
|
||||
this.nudPlateWidth = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblPlateHeight = new System.Windows.Forms.Label();
|
||||
this.nudPlateHeight = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblEdgeSpacing = new System.Windows.Forms.Label();
|
||||
this.nudEdgeSpacing = new System.Windows.Forms.NumericUpDown();
|
||||
|
||||
// By Count group
|
||||
this.grpByCount = new System.Windows.Forms.GroupBox();
|
||||
this.lblHorizontalPieces = new System.Windows.Forms.Label();
|
||||
this.nudHorizontalPieces = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblVerticalPieces = new System.Windows.Forms.Label();
|
||||
this.nudVerticalPieces = new System.Windows.Forms.NumericUpDown();
|
||||
|
||||
// Split Type group
|
||||
this.grpType = new System.Windows.Forms.GroupBox();
|
||||
this.radStraight = new System.Windows.Forms.RadioButton();
|
||||
this.radTabs = new System.Windows.Forms.RadioButton();
|
||||
this.radSpike = new System.Windows.Forms.RadioButton();
|
||||
|
||||
// Tab Parameters group
|
||||
this.grpTabParams = new System.Windows.Forms.GroupBox();
|
||||
this.lblTabWidth = new System.Windows.Forms.Label();
|
||||
this.nudTabWidth = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblTabHeight = new System.Windows.Forms.Label();
|
||||
this.nudTabHeight = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblTabCount = new System.Windows.Forms.Label();
|
||||
this.nudTabCount = new System.Windows.Forms.NumericUpDown();
|
||||
|
||||
// Spike Parameters group
|
||||
this.grpSpikeParams = new System.Windows.Forms.GroupBox();
|
||||
this.lblSpikeDepth = new System.Windows.Forms.Label();
|
||||
this.nudSpikeDepth = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblSpikeAngle = new System.Windows.Forms.Label();
|
||||
this.nudSpikeAngle = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblSpikePairCount = new System.Windows.Forms.Label();
|
||||
this.nudSpikePairCount = new System.Windows.Forms.NumericUpDown();
|
||||
|
||||
// OK/Cancel buttons
|
||||
this.btnOK = new System.Windows.Forms.Button();
|
||||
this.btnCancel = new System.Windows.Forms.Button();
|
||||
|
||||
// Begin init
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudPlateWidth)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudPlateHeight)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudEdgeSpacing)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudHorizontalPieces)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudVerticalPieces)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudTabWidth)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudTabHeight)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudTabCount)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudSpikeDepth)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudSpikeAngle)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudSpikePairCount)).BeginInit();
|
||||
this.pnlSettings.SuspendLayout();
|
||||
this.grpMethod.SuspendLayout();
|
||||
this.grpAutoFit.SuspendLayout();
|
||||
this.grpByCount.SuspendLayout();
|
||||
this.grpType.SuspendLayout();
|
||||
this.grpTabParams.SuspendLayout();
|
||||
this.grpSpikeParams.SuspendLayout();
|
||||
this.toolStrip.SuspendLayout();
|
||||
this.statusStrip.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
|
||||
// ---- ToolStrip ----
|
||||
this.toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.btnAddLine,
|
||||
this.btnDeleteLine
|
||||
});
|
||||
this.toolStrip.Location = new System.Drawing.Point(0, 0);
|
||||
this.toolStrip.Name = "toolStrip";
|
||||
this.toolStrip.Size = new System.Drawing.Size(580, 25);
|
||||
this.toolStrip.TabIndex = 0;
|
||||
|
||||
// btnAddLine
|
||||
this.btnAddLine.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.btnAddLine.Name = "btnAddLine";
|
||||
this.btnAddLine.Size = new System.Drawing.Size(86, 22);
|
||||
this.btnAddLine.Text = "Add Split Line";
|
||||
this.btnAddLine.Click += new System.EventHandler(this.OnAddSplitLine);
|
||||
|
||||
// btnDeleteLine
|
||||
this.btnDeleteLine.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.btnDeleteLine.Name = "btnDeleteLine";
|
||||
this.btnDeleteLine.Size = new System.Drawing.Size(68, 22);
|
||||
this.btnDeleteLine.Text = "Delete Line";
|
||||
this.btnDeleteLine.Click += new System.EventHandler(this.OnDeleteSplitLine);
|
||||
|
||||
// ---- StatusStrip ----
|
||||
this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.lblStatus,
|
||||
this.lblCursor
|
||||
});
|
||||
this.statusStrip.Location = new System.Drawing.Point(0, 528);
|
||||
this.statusStrip.Name = "statusStrip";
|
||||
this.statusStrip.Size = new System.Drawing.Size(800, 22);
|
||||
this.statusStrip.TabIndex = 1;
|
||||
|
||||
// lblStatus
|
||||
this.lblStatus.Name = "lblStatus";
|
||||
this.lblStatus.Size = new System.Drawing.Size(100, 17);
|
||||
this.lblStatus.Spring = true;
|
||||
this.lblStatus.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
this.lblStatus.Text = "";
|
||||
|
||||
// lblCursor
|
||||
this.lblCursor.Name = "lblCursor";
|
||||
this.lblCursor.Size = new System.Drawing.Size(150, 17);
|
||||
this.lblCursor.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
this.lblCursor.Text = "Cursor: 0.00, 0.00";
|
||||
|
||||
// ---- Settings Panel (right side) ----
|
||||
this.pnlSettings.AutoScroll = true;
|
||||
this.pnlSettings.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.pnlSettings.Location = new System.Drawing.Point(580, 25);
|
||||
this.pnlSettings.Name = "pnlSettings";
|
||||
this.pnlSettings.Padding = new System.Windows.Forms.Padding(6);
|
||||
this.pnlSettings.Size = new System.Drawing.Size(220, 503);
|
||||
this.pnlSettings.TabIndex = 2;
|
||||
this.pnlSettings.Controls.Add(this.btnCancel);
|
||||
this.pnlSettings.Controls.Add(this.btnOK);
|
||||
this.pnlSettings.Controls.Add(this.grpSpikeParams);
|
||||
this.pnlSettings.Controls.Add(this.grpTabParams);
|
||||
this.pnlSettings.Controls.Add(this.grpType);
|
||||
this.pnlSettings.Controls.Add(this.grpByCount);
|
||||
this.pnlSettings.Controls.Add(this.grpAutoFit);
|
||||
this.pnlSettings.Controls.Add(this.grpMethod);
|
||||
|
||||
// ---- Split Method Group ----
|
||||
this.grpMethod.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.grpMethod.Location = new System.Drawing.Point(6, 6);
|
||||
this.grpMethod.Name = "grpMethod";
|
||||
this.grpMethod.Size = new System.Drawing.Size(208, 95);
|
||||
this.grpMethod.TabIndex = 0;
|
||||
this.grpMethod.TabStop = false;
|
||||
this.grpMethod.Text = "Split Method";
|
||||
this.grpMethod.Controls.Add(this.radByCount);
|
||||
this.grpMethod.Controls.Add(this.radFitToPlate);
|
||||
this.grpMethod.Controls.Add(this.radManual);
|
||||
|
||||
// radManual
|
||||
this.radManual.AutoSize = true;
|
||||
this.radManual.Checked = true;
|
||||
this.radManual.Location = new System.Drawing.Point(10, 20);
|
||||
this.radManual.Name = "radManual";
|
||||
this.radManual.Size = new System.Drawing.Size(65, 19);
|
||||
this.radManual.TabIndex = 0;
|
||||
this.radManual.TabStop = true;
|
||||
this.radManual.Text = "Manual";
|
||||
this.radManual.CheckedChanged += new System.EventHandler(this.OnMethodChanged);
|
||||
|
||||
// radFitToPlate
|
||||
this.radFitToPlate.AutoSize = true;
|
||||
this.radFitToPlate.Location = new System.Drawing.Point(10, 43);
|
||||
this.radFitToPlate.Name = "radFitToPlate";
|
||||
this.radFitToPlate.Size = new System.Drawing.Size(85, 19);
|
||||
this.radFitToPlate.TabIndex = 1;
|
||||
this.radFitToPlate.Text = "Fit to Plate";
|
||||
this.radFitToPlate.CheckedChanged += new System.EventHandler(this.OnMethodChanged);
|
||||
|
||||
// radByCount
|
||||
this.radByCount.AutoSize = true;
|
||||
this.radByCount.Location = new System.Drawing.Point(10, 66);
|
||||
this.radByCount.Name = "radByCount";
|
||||
this.radByCount.Size = new System.Drawing.Size(102, 19);
|
||||
this.radByCount.TabIndex = 2;
|
||||
this.radByCount.Text = "Split by Count";
|
||||
this.radByCount.CheckedChanged += new System.EventHandler(this.OnMethodChanged);
|
||||
|
||||
// ---- Auto-Fit Group ----
|
||||
this.grpAutoFit.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.grpAutoFit.Location = new System.Drawing.Point(6, 101);
|
||||
this.grpAutoFit.Name = "grpAutoFit";
|
||||
this.grpAutoFit.Size = new System.Drawing.Size(208, 105);
|
||||
this.grpAutoFit.TabIndex = 1;
|
||||
this.grpAutoFit.TabStop = false;
|
||||
this.grpAutoFit.Text = "Auto-Fit Options";
|
||||
this.grpAutoFit.Visible = false;
|
||||
this.grpAutoFit.Controls.Add(this.nudEdgeSpacing);
|
||||
this.grpAutoFit.Controls.Add(this.lblEdgeSpacing);
|
||||
this.grpAutoFit.Controls.Add(this.nudPlateHeight);
|
||||
this.grpAutoFit.Controls.Add(this.lblPlateHeight);
|
||||
this.grpAutoFit.Controls.Add(this.nudPlateWidth);
|
||||
this.grpAutoFit.Controls.Add(this.lblPlateWidth);
|
||||
|
||||
// lblPlateWidth
|
||||
this.lblPlateWidth.AutoSize = true;
|
||||
this.lblPlateWidth.Location = new System.Drawing.Point(10, 22);
|
||||
this.lblPlateWidth.Name = "lblPlateWidth";
|
||||
this.lblPlateWidth.Size = new System.Drawing.Size(70, 15);
|
||||
this.lblPlateWidth.Text = "Plate Width:";
|
||||
|
||||
// nudPlateWidth
|
||||
this.nudPlateWidth.DecimalPlaces = 2;
|
||||
this.nudPlateWidth.Location = new System.Drawing.Point(110, 20);
|
||||
this.nudPlateWidth.Maximum = new decimal(new int[] { 100000, 0, 0, 0 });
|
||||
this.nudPlateWidth.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.nudPlateWidth.Name = "nudPlateWidth";
|
||||
this.nudPlateWidth.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudPlateWidth.TabIndex = 0;
|
||||
this.nudPlateWidth.Value = new decimal(new int[] { 120, 0, 0, 0 });
|
||||
this.nudPlateWidth.ValueChanged += new System.EventHandler(this.OnAutoFitValueChanged);
|
||||
|
||||
// lblPlateHeight
|
||||
this.lblPlateHeight.AutoSize = true;
|
||||
this.lblPlateHeight.Location = new System.Drawing.Point(10, 49);
|
||||
this.lblPlateHeight.Name = "lblPlateHeight";
|
||||
this.lblPlateHeight.Size = new System.Drawing.Size(74, 15);
|
||||
this.lblPlateHeight.Text = "Plate Height:";
|
||||
|
||||
// nudPlateHeight
|
||||
this.nudPlateHeight.DecimalPlaces = 2;
|
||||
this.nudPlateHeight.Location = new System.Drawing.Point(110, 47);
|
||||
this.nudPlateHeight.Maximum = new decimal(new int[] { 100000, 0, 0, 0 });
|
||||
this.nudPlateHeight.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.nudPlateHeight.Name = "nudPlateHeight";
|
||||
this.nudPlateHeight.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudPlateHeight.TabIndex = 1;
|
||||
this.nudPlateHeight.Value = new decimal(new int[] { 60, 0, 0, 0 });
|
||||
this.nudPlateHeight.ValueChanged += new System.EventHandler(this.OnAutoFitValueChanged);
|
||||
|
||||
// lblEdgeSpacing
|
||||
this.lblEdgeSpacing.AutoSize = true;
|
||||
this.lblEdgeSpacing.Location = new System.Drawing.Point(10, 76);
|
||||
this.lblEdgeSpacing.Name = "lblEdgeSpacing";
|
||||
this.lblEdgeSpacing.Size = new System.Drawing.Size(82, 15);
|
||||
this.lblEdgeSpacing.Text = "Edge Spacing:";
|
||||
|
||||
// nudEdgeSpacing
|
||||
this.nudEdgeSpacing.DecimalPlaces = 2;
|
||||
this.nudEdgeSpacing.Location = new System.Drawing.Point(110, 74);
|
||||
this.nudEdgeSpacing.Maximum = new decimal(new int[] { 100, 0, 0, 0 });
|
||||
this.nudEdgeSpacing.Name = "nudEdgeSpacing";
|
||||
this.nudEdgeSpacing.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudEdgeSpacing.TabIndex = 2;
|
||||
this.nudEdgeSpacing.Value = new decimal(new int[] { 5, 0, 0, 131072 });
|
||||
this.nudEdgeSpacing.ValueChanged += new System.EventHandler(this.OnAutoFitValueChanged);
|
||||
|
||||
// ---- By Count Group ----
|
||||
this.grpByCount.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.grpByCount.Location = new System.Drawing.Point(6, 206);
|
||||
this.grpByCount.Name = "grpByCount";
|
||||
this.grpByCount.Size = new System.Drawing.Size(208, 78);
|
||||
this.grpByCount.TabIndex = 2;
|
||||
this.grpByCount.TabStop = false;
|
||||
this.grpByCount.Text = "Split by Count";
|
||||
this.grpByCount.Visible = false;
|
||||
this.grpByCount.Controls.Add(this.nudVerticalPieces);
|
||||
this.grpByCount.Controls.Add(this.lblVerticalPieces);
|
||||
this.grpByCount.Controls.Add(this.nudHorizontalPieces);
|
||||
this.grpByCount.Controls.Add(this.lblHorizontalPieces);
|
||||
|
||||
// lblHorizontalPieces
|
||||
this.lblHorizontalPieces.AutoSize = true;
|
||||
this.lblHorizontalPieces.Location = new System.Drawing.Point(10, 22);
|
||||
this.lblHorizontalPieces.Name = "lblHorizontalPieces";
|
||||
this.lblHorizontalPieces.Size = new System.Drawing.Size(72, 15);
|
||||
this.lblHorizontalPieces.Text = "H. Pieces:";
|
||||
|
||||
// nudHorizontalPieces
|
||||
this.nudHorizontalPieces.Location = new System.Drawing.Point(110, 20);
|
||||
this.nudHorizontalPieces.Maximum = new decimal(new int[] { 20, 0, 0, 0 });
|
||||
this.nudHorizontalPieces.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.nudHorizontalPieces.Name = "nudHorizontalPieces";
|
||||
this.nudHorizontalPieces.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudHorizontalPieces.TabIndex = 0;
|
||||
this.nudHorizontalPieces.Value = new decimal(new int[] { 2, 0, 0, 0 });
|
||||
this.nudHorizontalPieces.ValueChanged += new System.EventHandler(this.OnByCountValueChanged);
|
||||
|
||||
// lblVerticalPieces
|
||||
this.lblVerticalPieces.AutoSize = true;
|
||||
this.lblVerticalPieces.Location = new System.Drawing.Point(10, 49);
|
||||
this.lblVerticalPieces.Name = "lblVerticalPieces";
|
||||
this.lblVerticalPieces.Size = new System.Drawing.Size(63, 15);
|
||||
this.lblVerticalPieces.Text = "V. Pieces:";
|
||||
|
||||
// nudVerticalPieces
|
||||
this.nudVerticalPieces.Location = new System.Drawing.Point(110, 47);
|
||||
this.nudVerticalPieces.Maximum = new decimal(new int[] { 20, 0, 0, 0 });
|
||||
this.nudVerticalPieces.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.nudVerticalPieces.Name = "nudVerticalPieces";
|
||||
this.nudVerticalPieces.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudVerticalPieces.TabIndex = 1;
|
||||
this.nudVerticalPieces.Value = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.nudVerticalPieces.ValueChanged += new System.EventHandler(this.OnByCountValueChanged);
|
||||
|
||||
// ---- Split Type Group ----
|
||||
this.grpType.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.grpType.Location = new System.Drawing.Point(6, 284);
|
||||
this.grpType.Name = "grpType";
|
||||
this.grpType.Size = new System.Drawing.Size(208, 95);
|
||||
this.grpType.TabIndex = 3;
|
||||
this.grpType.TabStop = false;
|
||||
this.grpType.Text = "Split Type";
|
||||
this.grpType.Controls.Add(this.radSpike);
|
||||
this.grpType.Controls.Add(this.radTabs);
|
||||
this.grpType.Controls.Add(this.radStraight);
|
||||
|
||||
// radStraight
|
||||
this.radStraight.AutoSize = true;
|
||||
this.radStraight.Checked = true;
|
||||
this.radStraight.Location = new System.Drawing.Point(10, 20);
|
||||
this.radStraight.Name = "radStraight";
|
||||
this.radStraight.Size = new System.Drawing.Size(68, 19);
|
||||
this.radStraight.TabIndex = 0;
|
||||
this.radStraight.TabStop = true;
|
||||
this.radStraight.Text = "Straight";
|
||||
this.radStraight.CheckedChanged += new System.EventHandler(this.OnTypeChanged);
|
||||
|
||||
// radTabs
|
||||
this.radTabs.AutoSize = true;
|
||||
this.radTabs.Location = new System.Drawing.Point(10, 43);
|
||||
this.radTabs.Name = "radTabs";
|
||||
this.radTabs.Size = new System.Drawing.Size(107, 19);
|
||||
this.radTabs.TabIndex = 1;
|
||||
this.radTabs.Text = "Weld-Gap Tabs";
|
||||
this.radTabs.CheckedChanged += new System.EventHandler(this.OnTypeChanged);
|
||||
|
||||
// radSpike
|
||||
this.radSpike.AutoSize = true;
|
||||
this.radSpike.Location = new System.Drawing.Point(10, 66);
|
||||
this.radSpike.Name = "radSpike";
|
||||
this.radSpike.Size = new System.Drawing.Size(99, 19);
|
||||
this.radSpike.TabIndex = 2;
|
||||
this.radSpike.Text = "Spike-Groove";
|
||||
this.radSpike.CheckedChanged += new System.EventHandler(this.OnTypeChanged);
|
||||
|
||||
// ---- Tab Parameters Group ----
|
||||
this.grpTabParams.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.grpTabParams.Location = new System.Drawing.Point(6, 379);
|
||||
this.grpTabParams.Name = "grpTabParams";
|
||||
this.grpTabParams.Size = new System.Drawing.Size(208, 105);
|
||||
this.grpTabParams.TabIndex = 4;
|
||||
this.grpTabParams.TabStop = false;
|
||||
this.grpTabParams.Text = "Tab Parameters";
|
||||
this.grpTabParams.Visible = false;
|
||||
this.grpTabParams.Controls.Add(this.nudTabCount);
|
||||
this.grpTabParams.Controls.Add(this.lblTabCount);
|
||||
this.grpTabParams.Controls.Add(this.nudTabHeight);
|
||||
this.grpTabParams.Controls.Add(this.lblTabHeight);
|
||||
this.grpTabParams.Controls.Add(this.nudTabWidth);
|
||||
this.grpTabParams.Controls.Add(this.lblTabWidth);
|
||||
|
||||
// lblTabWidth
|
||||
this.lblTabWidth.AutoSize = true;
|
||||
this.lblTabWidth.Location = new System.Drawing.Point(10, 22);
|
||||
this.lblTabWidth.Name = "lblTabWidth";
|
||||
this.lblTabWidth.Size = new System.Drawing.Size(65, 15);
|
||||
this.lblTabWidth.Text = "Tab Width:";
|
||||
|
||||
// nudTabWidth
|
||||
this.nudTabWidth.DecimalPlaces = 2;
|
||||
this.nudTabWidth.Location = new System.Drawing.Point(110, 20);
|
||||
this.nudTabWidth.Maximum = new decimal(new int[] { 1000, 0, 0, 0 });
|
||||
this.nudTabWidth.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
this.nudTabWidth.Name = "nudTabWidth";
|
||||
this.nudTabWidth.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudTabWidth.TabIndex = 0;
|
||||
this.nudTabWidth.Value = new decimal(new int[] { 5, 0, 0, 65536 });
|
||||
|
||||
// lblTabHeight
|
||||
this.lblTabHeight.AutoSize = true;
|
||||
this.lblTabHeight.Location = new System.Drawing.Point(10, 49);
|
||||
this.lblTabHeight.Name = "lblTabHeight";
|
||||
this.lblTabHeight.Size = new System.Drawing.Size(69, 15);
|
||||
this.lblTabHeight.Text = "Tab Height:";
|
||||
|
||||
// nudTabHeight
|
||||
this.nudTabHeight.DecimalPlaces = 2;
|
||||
this.nudTabHeight.Location = new System.Drawing.Point(110, 47);
|
||||
this.nudTabHeight.Maximum = new decimal(new int[] { 100, 0, 0, 0 });
|
||||
this.nudTabHeight.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
this.nudTabHeight.Name = "nudTabHeight";
|
||||
this.nudTabHeight.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudTabHeight.TabIndex = 1;
|
||||
this.nudTabHeight.Value = new decimal(new int[] { 1, 0, 0, 65536 });
|
||||
|
||||
// lblTabCount
|
||||
this.lblTabCount.AutoSize = true;
|
||||
this.lblTabCount.Location = new System.Drawing.Point(10, 76);
|
||||
this.lblTabCount.Name = "lblTabCount";
|
||||
this.lblTabCount.Size = new System.Drawing.Size(64, 15);
|
||||
this.lblTabCount.Text = "Tab Count:";
|
||||
|
||||
// nudTabCount
|
||||
this.nudTabCount.Location = new System.Drawing.Point(110, 74);
|
||||
this.nudTabCount.Maximum = new decimal(new int[] { 50, 0, 0, 0 });
|
||||
this.nudTabCount.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.nudTabCount.Name = "nudTabCount";
|
||||
this.nudTabCount.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudTabCount.TabIndex = 2;
|
||||
this.nudTabCount.Value = new decimal(new int[] { 3, 0, 0, 0 });
|
||||
|
||||
// ---- Spike Parameters Group ----
|
||||
this.grpSpikeParams.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.grpSpikeParams.Location = new System.Drawing.Point(6, 484);
|
||||
this.grpSpikeParams.Name = "grpSpikeParams";
|
||||
this.grpSpikeParams.Size = new System.Drawing.Size(208, 105);
|
||||
this.grpSpikeParams.TabIndex = 5;
|
||||
this.grpSpikeParams.TabStop = false;
|
||||
this.grpSpikeParams.Text = "Spike Parameters";
|
||||
this.grpSpikeParams.Visible = false;
|
||||
this.grpSpikeParams.Controls.Add(this.nudSpikePairCount);
|
||||
this.grpSpikeParams.Controls.Add(this.lblSpikePairCount);
|
||||
this.grpSpikeParams.Controls.Add(this.nudSpikeAngle);
|
||||
this.grpSpikeParams.Controls.Add(this.lblSpikeAngle);
|
||||
this.grpSpikeParams.Controls.Add(this.nudSpikeDepth);
|
||||
this.grpSpikeParams.Controls.Add(this.lblSpikeDepth);
|
||||
|
||||
// lblSpikeDepth
|
||||
this.lblSpikeDepth.AutoSize = true;
|
||||
this.lblSpikeDepth.Location = new System.Drawing.Point(10, 22);
|
||||
this.lblSpikeDepth.Name = "lblSpikeDepth";
|
||||
this.lblSpikeDepth.Size = new System.Drawing.Size(74, 15);
|
||||
this.lblSpikeDepth.Text = "Spike Depth:";
|
||||
|
||||
// nudSpikeDepth
|
||||
this.nudSpikeDepth.DecimalPlaces = 2;
|
||||
this.nudSpikeDepth.Location = new System.Drawing.Point(110, 20);
|
||||
this.nudSpikeDepth.Maximum = new decimal(new int[] { 100, 0, 0, 0 });
|
||||
this.nudSpikeDepth.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
this.nudSpikeDepth.Name = "nudSpikeDepth";
|
||||
this.nudSpikeDepth.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudSpikeDepth.TabIndex = 0;
|
||||
this.nudSpikeDepth.Value = new decimal(new int[] { 5, 0, 0, 65536 });
|
||||
|
||||
// lblSpikeAngle
|
||||
this.lblSpikeAngle.AutoSize = true;
|
||||
this.lblSpikeAngle.Location = new System.Drawing.Point(10, 49);
|
||||
this.lblSpikeAngle.Name = "lblSpikeAngle";
|
||||
this.lblSpikeAngle.Size = new System.Drawing.Size(74, 15);
|
||||
this.lblSpikeAngle.Text = "Spike Angle:";
|
||||
|
||||
// nudSpikeAngle
|
||||
this.nudSpikeAngle.DecimalPlaces = 1;
|
||||
this.nudSpikeAngle.Location = new System.Drawing.Point(110, 47);
|
||||
this.nudSpikeAngle.Maximum = new decimal(new int[] { 89, 0, 0, 0 });
|
||||
this.nudSpikeAngle.Minimum = new decimal(new int[] { 10, 0, 0, 0 });
|
||||
this.nudSpikeAngle.Name = "nudSpikeAngle";
|
||||
this.nudSpikeAngle.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudSpikeAngle.TabIndex = 1;
|
||||
this.nudSpikeAngle.Value = new decimal(new int[] { 45, 0, 0, 0 });
|
||||
|
||||
// lblSpikePairCount
|
||||
this.lblSpikePairCount.AutoSize = true;
|
||||
this.lblSpikePairCount.Location = new System.Drawing.Point(10, 76);
|
||||
this.lblSpikePairCount.Name = "lblSpikePairCount";
|
||||
this.lblSpikePairCount.Size = new System.Drawing.Size(65, 15);
|
||||
this.lblSpikePairCount.Text = "Pair Count:";
|
||||
|
||||
// nudSpikePairCount
|
||||
this.nudSpikePairCount.Location = new System.Drawing.Point(110, 74);
|
||||
this.nudSpikePairCount.Maximum = new decimal(new int[] { 50, 0, 0, 0 });
|
||||
this.nudSpikePairCount.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.nudSpikePairCount.Name = "nudSpikePairCount";
|
||||
this.nudSpikePairCount.Size = new System.Drawing.Size(88, 23);
|
||||
this.nudSpikePairCount.TabIndex = 2;
|
||||
this.nudSpikePairCount.Value = new decimal(new int[] { 3, 0, 0, 0 });
|
||||
|
||||
// ---- OK / Cancel Buttons ----
|
||||
this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnOK.Location = new System.Drawing.Point(30, 468);
|
||||
this.btnOK.Name = "btnOK";
|
||||
this.btnOK.Size = new System.Drawing.Size(80, 28);
|
||||
this.btnOK.TabIndex = 6;
|
||||
this.btnOK.Text = "OK";
|
||||
this.btnOK.UseVisualStyleBackColor = true;
|
||||
this.btnOK.Click += new System.EventHandler(this.OnOK);
|
||||
|
||||
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.btnCancel.Location = new System.Drawing.Point(120, 468);
|
||||
this.btnCancel.Name = "btnCancel";
|
||||
this.btnCancel.Size = new System.Drawing.Size(80, 28);
|
||||
this.btnCancel.TabIndex = 7;
|
||||
this.btnCancel.Text = "Cancel";
|
||||
this.btnCancel.UseVisualStyleBackColor = true;
|
||||
this.btnCancel.Click += new System.EventHandler(this.OnCancel);
|
||||
|
||||
// ---- Preview Panel (fills remaining space) ----
|
||||
this.pnlPreview.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.pnlPreview.Location = new System.Drawing.Point(0, 25);
|
||||
this.pnlPreview.Name = "pnlPreview";
|
||||
this.pnlPreview.Size = new System.Drawing.Size(580, 503);
|
||||
this.pnlPreview.TabIndex = 3;
|
||||
this.pnlPreview.Paint += new System.Windows.Forms.PaintEventHandler(this.OnPreviewPaint);
|
||||
this.pnlPreview.MouseDown += new System.Windows.Forms.MouseEventHandler(this.OnPreviewMouseDown);
|
||||
this.pnlPreview.MouseMove += new System.Windows.Forms.MouseEventHandler(this.OnPreviewMouseMove);
|
||||
this.pnlPreview.MouseUp += new System.Windows.Forms.MouseEventHandler(this.OnPreviewMouseUp);
|
||||
this.pnlPreview.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.OnPreviewMouseWheel);
|
||||
|
||||
// ---- SplitDrawingForm ----
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
this.CancelButton = this.btnCancel;
|
||||
this.ClientSize = new System.Drawing.Size(800, 550);
|
||||
this.Controls.Add(this.pnlPreview);
|
||||
this.Controls.Add(this.pnlSettings);
|
||||
this.Controls.Add(this.statusStrip);
|
||||
this.Controls.Add(this.toolStrip);
|
||||
this.MinimumSize = new System.Drawing.Size(600, 450);
|
||||
this.Name = "SplitDrawingForm";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Split Drawing";
|
||||
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudPlateWidth)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudPlateHeight)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudEdgeSpacing)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudHorizontalPieces)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudVerticalPieces)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudTabWidth)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudTabHeight)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudTabCount)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudSpikeDepth)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudSpikeAngle)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.nudSpikePairCount)).EndInit();
|
||||
this.pnlSettings.ResumeLayout(false);
|
||||
this.grpMethod.ResumeLayout(false);
|
||||
this.grpMethod.PerformLayout();
|
||||
this.grpAutoFit.ResumeLayout(false);
|
||||
this.grpAutoFit.PerformLayout();
|
||||
this.grpByCount.ResumeLayout(false);
|
||||
this.grpByCount.PerformLayout();
|
||||
this.grpType.ResumeLayout(false);
|
||||
this.grpType.PerformLayout();
|
||||
this.grpTabParams.ResumeLayout(false);
|
||||
this.grpTabParams.PerformLayout();
|
||||
this.grpSpikeParams.ResumeLayout(false);
|
||||
this.grpSpikeParams.PerformLayout();
|
||||
this.toolStrip.ResumeLayout(false);
|
||||
this.toolStrip.PerformLayout();
|
||||
this.statusStrip.ResumeLayout(false);
|
||||
this.statusStrip.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel pnlPreview;
|
||||
private System.Windows.Forms.Panel pnlSettings;
|
||||
private System.Windows.Forms.ToolStrip toolStrip;
|
||||
private System.Windows.Forms.ToolStripButton btnAddLine;
|
||||
private System.Windows.Forms.ToolStripButton btnDeleteLine;
|
||||
private System.Windows.Forms.StatusStrip statusStrip;
|
||||
private System.Windows.Forms.ToolStripStatusLabel lblStatus;
|
||||
private System.Windows.Forms.ToolStripStatusLabel lblCursor;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpMethod;
|
||||
private System.Windows.Forms.RadioButton radManual;
|
||||
private System.Windows.Forms.RadioButton radFitToPlate;
|
||||
private System.Windows.Forms.RadioButton radByCount;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpAutoFit;
|
||||
private System.Windows.Forms.Label lblPlateWidth;
|
||||
private System.Windows.Forms.NumericUpDown nudPlateWidth;
|
||||
private System.Windows.Forms.Label lblPlateHeight;
|
||||
private System.Windows.Forms.NumericUpDown nudPlateHeight;
|
||||
private System.Windows.Forms.Label lblEdgeSpacing;
|
||||
private System.Windows.Forms.NumericUpDown nudEdgeSpacing;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpByCount;
|
||||
private System.Windows.Forms.Label lblHorizontalPieces;
|
||||
private System.Windows.Forms.NumericUpDown nudHorizontalPieces;
|
||||
private System.Windows.Forms.Label lblVerticalPieces;
|
||||
private System.Windows.Forms.NumericUpDown nudVerticalPieces;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpType;
|
||||
private System.Windows.Forms.RadioButton radStraight;
|
||||
private System.Windows.Forms.RadioButton radTabs;
|
||||
private System.Windows.Forms.RadioButton radSpike;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpTabParams;
|
||||
private System.Windows.Forms.Label lblTabWidth;
|
||||
private System.Windows.Forms.NumericUpDown nudTabWidth;
|
||||
private System.Windows.Forms.Label lblTabHeight;
|
||||
private System.Windows.Forms.NumericUpDown nudTabHeight;
|
||||
private System.Windows.Forms.Label lblTabCount;
|
||||
private System.Windows.Forms.NumericUpDown nudTabCount;
|
||||
|
||||
private System.Windows.Forms.GroupBox grpSpikeParams;
|
||||
private System.Windows.Forms.Label lblSpikeDepth;
|
||||
private System.Windows.Forms.NumericUpDown nudSpikeDepth;
|
||||
private System.Windows.Forms.Label lblSpikeAngle;
|
||||
private System.Windows.Forms.NumericUpDown nudSpikeAngle;
|
||||
private System.Windows.Forms.Label lblSpikePairCount;
|
||||
private System.Windows.Forms.NumericUpDown nudSpikePairCount;
|
||||
|
||||
private System.Windows.Forms.Button btnOK;
|
||||
private System.Windows.Forms.Button btnCancel;
|
||||
}
|
||||
}
|
||||
363
OpenNest/Forms/SplitDrawingForm.cs
Normal file
363
OpenNest/Forms/SplitDrawingForm.cs
Normal file
@@ -0,0 +1,363 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Forms;
|
||||
|
||||
public partial class SplitDrawingForm : Form
|
||||
{
|
||||
private readonly Drawing _drawing;
|
||||
private readonly List<Entity> _drawingEntities;
|
||||
private readonly Box _drawingBounds;
|
||||
private readonly List<SplitLine> _splitLines = new();
|
||||
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
|
||||
private bool _placingLine;
|
||||
|
||||
// Zoom/pan state
|
||||
private float _zoom = 1f;
|
||||
private PointF _pan;
|
||||
private Point _lastMouse;
|
||||
private bool _panning;
|
||||
|
||||
// Snap threshold in screen pixels
|
||||
private const double SnapThreshold = 5.0;
|
||||
|
||||
public List<Drawing> ResultDrawings { get; private set; }
|
||||
|
||||
public SplitDrawingForm(Drawing drawing)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Enable double buffering on the preview panel to reduce flicker
|
||||
typeof(Panel).InvokeMember(
|
||||
"DoubleBuffered",
|
||||
System.Reflection.BindingFlags.SetProperty | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
|
||||
null, pnlPreview, new object[] { true });
|
||||
|
||||
_drawing = drawing;
|
||||
_drawingEntities = ConvertProgram.ToGeometry(drawing.Program);
|
||||
_drawingBounds = drawing.Program.BoundingBox();
|
||||
|
||||
Text = $"Split Drawing: {drawing.Name}";
|
||||
UpdateUI();
|
||||
FitToView();
|
||||
}
|
||||
|
||||
// --- Split Method Selection ---
|
||||
|
||||
private void OnMethodChanged(object sender, EventArgs e)
|
||||
{
|
||||
grpAutoFit.Visible = radFitToPlate.Checked;
|
||||
grpByCount.Visible = radByCount.Checked;
|
||||
|
||||
if (radFitToPlate.Checked || radByCount.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
}
|
||||
|
||||
private void RecalculateAutoSplitLines()
|
||||
{
|
||||
_splitLines.Clear();
|
||||
|
||||
if (radFitToPlate.Checked)
|
||||
{
|
||||
var plateW = (double)nudPlateWidth.Value;
|
||||
var plateH = (double)nudPlateHeight.Value;
|
||||
var spacing = (double)nudEdgeSpacing.Value;
|
||||
var overhang = GetCurrentParameters().FeatureOverhang;
|
||||
_splitLines.AddRange(AutoSplitCalculator.FitToPlate(_drawingBounds, plateW, plateH, spacing, overhang));
|
||||
}
|
||||
else if (radByCount.Checked)
|
||||
{
|
||||
var hPieces = (int)nudHorizontalPieces.Value;
|
||||
var vPieces = (int)nudVerticalPieces.Value;
|
||||
_splitLines.AddRange(AutoSplitCalculator.SplitByCount(_drawingBounds, hPieces, vPieces));
|
||||
}
|
||||
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private void OnAutoFitValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
}
|
||||
|
||||
private void OnByCountValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (radByCount.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
}
|
||||
|
||||
// --- Split Type Selection ---
|
||||
|
||||
private void OnTypeChanged(object sender, EventArgs e)
|
||||
{
|
||||
grpTabParams.Visible = radTabs.Checked;
|
||||
grpSpikeParams.Visible = radSpike.Checked;
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines(); // overhang changed
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private SplitParameters GetCurrentParameters()
|
||||
{
|
||||
var p = new SplitParameters();
|
||||
if (radTabs.Checked)
|
||||
{
|
||||
p.Type = SplitType.WeldGapTabs;
|
||||
p.TabWidth = (double)nudTabWidth.Value;
|
||||
p.TabHeight = (double)nudTabHeight.Value;
|
||||
p.TabCount = (int)nudTabCount.Value;
|
||||
}
|
||||
else if (radSpike.Checked)
|
||||
{
|
||||
p.Type = SplitType.SpikeGroove;
|
||||
p.SpikeDepth = (double)nudSpikeDepth.Value;
|
||||
p.SpikeAngle = (double)nudSpikeAngle.Value;
|
||||
p.SpikePairCount = (int)nudSpikePairCount.Value;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Manual Split Line Placement ---
|
||||
|
||||
private void OnPreviewMouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Left && radManual.Checked && _placingLine)
|
||||
{
|
||||
var pt = ScreenToDrawing(e.Location);
|
||||
var snapped = SnapToMidpoint(pt);
|
||||
var position = _currentAxis == CutOffAxis.Vertical ? snapped.X : snapped.Y;
|
||||
_splitLines.Add(new SplitLine(position, _currentAxis));
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
else if (e.Button == MouseButtons.Middle)
|
||||
{
|
||||
_panning = true;
|
||||
_lastMouse = e.Location;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (_panning)
|
||||
{
|
||||
_pan.X += e.X - _lastMouse.X;
|
||||
_pan.Y += e.Y - _lastMouse.Y;
|
||||
_lastMouse = e.Location;
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
var drawPt = ScreenToDrawing(e.Location);
|
||||
lblCursor.Text = $"Cursor: {drawPt.X:F2}, {drawPt.Y:F2}";
|
||||
}
|
||||
|
||||
private void OnPreviewMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Middle)
|
||||
_panning = false;
|
||||
}
|
||||
|
||||
private void OnPreviewMouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
var factor = e.Delta > 0 ? 1.1f : 0.9f;
|
||||
_zoom *= factor;
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
if (keyData == Keys.Space)
|
||||
{
|
||||
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
|
||||
return true;
|
||||
}
|
||||
if (keyData == Keys.Escape)
|
||||
{
|
||||
_placingLine = false;
|
||||
return true;
|
||||
}
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
}
|
||||
|
||||
private Vector SnapToMidpoint(Vector pt)
|
||||
{
|
||||
var midX = _drawingBounds.Center.X;
|
||||
var midY = _drawingBounds.Center.Y;
|
||||
var threshold = SnapThreshold / _zoom;
|
||||
|
||||
if (_currentAxis == CutOffAxis.Vertical && System.Math.Abs(pt.X - midX) < threshold)
|
||||
return new Vector(midX, pt.Y);
|
||||
if (_currentAxis == CutOffAxis.Horizontal && System.Math.Abs(pt.Y - midY) < threshold)
|
||||
return new Vector(pt.X, midY);
|
||||
return pt;
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
|
||||
private void OnPreviewPaint(object sender, PaintEventArgs e)
|
||||
{
|
||||
var g = e.Graphics;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.Clear(Color.FromArgb(26, 26, 26));
|
||||
|
||||
g.TranslateTransform(_pan.X, _pan.Y);
|
||||
g.ScaleTransform(_zoom, -_zoom); // flip Y for CNC coordinates
|
||||
|
||||
// Draw part outline
|
||||
using var partPen = new Pen(Color.LightGray, 1f / _zoom);
|
||||
DrawEntities(g, _drawingEntities, partPen);
|
||||
|
||||
// Draw split lines
|
||||
using var splitPen = new Pen(Color.FromArgb(255, 82, 82), 1f / _zoom);
|
||||
splitPen.DashStyle = DashStyle.Dash;
|
||||
foreach (var sl in _splitLines)
|
||||
{
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
g.DrawLine(splitPen, (float)sl.Position, (float)(_drawingBounds.Bottom - 10), (float)sl.Position, (float)(_drawingBounds.Top + 10));
|
||||
else
|
||||
g.DrawLine(splitPen, (float)(_drawingBounds.Left - 10), (float)sl.Position, (float)(_drawingBounds.Right + 10), (float)sl.Position);
|
||||
}
|
||||
|
||||
// Draw piece color overlays
|
||||
DrawPieceOverlays(g);
|
||||
}
|
||||
|
||||
private void DrawEntities(Graphics g, List<Entity> entities, Pen pen)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (entity is Line line)
|
||||
g.DrawLine(pen, (float)line.StartPoint.X, (float)line.StartPoint.Y, (float)line.EndPoint.X, (float)line.EndPoint.Y);
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
var rect = new RectangleF(
|
||||
(float)(arc.Center.X - arc.Radius),
|
||||
(float)(arc.Center.Y - arc.Radius),
|
||||
(float)(arc.Radius * 2),
|
||||
(float)(arc.Radius * 2));
|
||||
var startDeg = (float)OpenNest.Math.Angle.ToDegrees(arc.StartAngle);
|
||||
var sweepDeg = (float)OpenNest.Math.Angle.ToDegrees(arc.EndAngle - arc.StartAngle);
|
||||
if (rect.Width > 0 && rect.Height > 0)
|
||||
g.DrawArc(pen, rect, startDeg, sweepDeg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Color[] PieceColors =
|
||||
{
|
||||
Color.FromArgb(40, 79, 195, 247),
|
||||
Color.FromArgb(40, 129, 199, 132),
|
||||
Color.FromArgb(40, 255, 183, 77),
|
||||
Color.FromArgb(40, 206, 147, 216),
|
||||
Color.FromArgb(40, 255, 138, 128),
|
||||
Color.FromArgb(40, 128, 222, 234)
|
||||
};
|
||||
|
||||
private void DrawPieceOverlays(Graphics g)
|
||||
{
|
||||
// Simple region overlay based on split lines and bounding box
|
||||
var regions = BuildPreviewRegions();
|
||||
for (var i = 0; i < regions.Count; i++)
|
||||
{
|
||||
var color = PieceColors[i % PieceColors.Length];
|
||||
using var brush = new SolidBrush(color);
|
||||
var r = regions[i];
|
||||
g.FillRectangle(brush, (float)r.Left, (float)r.Bottom, (float)r.Width, (float)r.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Box> BuildPreviewRegions()
|
||||
{
|
||||
var verticals = _splitLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList();
|
||||
var horizontals = _splitLines.Where(l => l.Axis == CutOffAxis.Horizontal).OrderBy(l => l.Position).ToList();
|
||||
|
||||
var xEdges = new List<double> { _drawingBounds.Left };
|
||||
xEdges.AddRange(verticals.Select(v => v.Position));
|
||||
xEdges.Add(_drawingBounds.Right);
|
||||
|
||||
var yEdges = new List<double> { _drawingBounds.Bottom };
|
||||
yEdges.AddRange(horizontals.Select(h => h.Position));
|
||||
yEdges.Add(_drawingBounds.Top);
|
||||
|
||||
var regions = new List<Box>();
|
||||
for (var yi = 0; yi < yEdges.Count - 1; yi++)
|
||||
for (var xi = 0; xi < xEdges.Count - 1; xi++)
|
||||
regions.Add(new Box(xEdges[xi], yEdges[yi], xEdges[xi + 1] - xEdges[xi], yEdges[yi + 1] - yEdges[yi]));
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
// --- Coordinate transforms ---
|
||||
|
||||
private Vector ScreenToDrawing(Point screen)
|
||||
{
|
||||
var x = (screen.X - _pan.X) / _zoom;
|
||||
var y = -(screen.Y - _pan.Y) / _zoom; // flip Y
|
||||
return new Vector(x, y);
|
||||
}
|
||||
|
||||
private void FitToView()
|
||||
{
|
||||
if (_drawingBounds.Width <= 0 || _drawingBounds.Length <= 0) return;
|
||||
var pad = 40f;
|
||||
var scaleX = (pnlPreview.Width - pad * 2) / (float)_drawingBounds.Width;
|
||||
var scaleY = (pnlPreview.Height - pad * 2) / (float)_drawingBounds.Length;
|
||||
_zoom = System.Math.Min(scaleX, scaleY);
|
||||
_pan = new PointF(
|
||||
pad - (float)_drawingBounds.Left * _zoom,
|
||||
pnlPreview.Height - pad + (float)_drawingBounds.Bottom * _zoom);
|
||||
}
|
||||
|
||||
// --- OK/Cancel ---
|
||||
|
||||
private void OnOK(object sender, EventArgs e)
|
||||
{
|
||||
if (_splitLines.Count == 0)
|
||||
{
|
||||
MessageBox.Show("No split lines defined.", "Split Drawing", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
ResultDrawings = DrawingSplitter.Split(_drawing, _splitLines, GetCurrentParameters());
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnCancel(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
|
||||
// --- Toolbar ---
|
||||
|
||||
private void OnAddSplitLine(object sender, EventArgs e)
|
||||
{
|
||||
radManual.Checked = true;
|
||||
_placingLine = true;
|
||||
}
|
||||
|
||||
private void OnDeleteSplitLine(object sender, EventArgs e)
|
||||
{
|
||||
if (_splitLines.Count > 0)
|
||||
{
|
||||
_splitLines.RemoveAt(_splitLines.Count - 1);
|
||||
UpdateUI();
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
var pieceCount = _splitLines.Count == 0 ? 1 : BuildPreviewRegions().Count;
|
||||
lblStatus.Text = $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user