diff --git a/CLAUDE.md b/CLAUDE.md index 60430e0..87d39b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`, `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. diff --git a/OpenNest.Core/Splitting/AutoSplitCalculator.cs b/OpenNest.Core/Splitting/AutoSplitCalculator.cs new file mode 100644 index 0000000..bebced2 --- /dev/null +++ b/OpenNest.Core/Splitting/AutoSplitCalculator.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest; + +public static class AutoSplitCalculator +{ + public static List 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(); + + 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 SplitByCount(Box partBounds, int horizontalPieces, int verticalPieces) + { + var lines = new List(); + + 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; + } +} diff --git a/OpenNest.Core/Splitting/DrawingSplitter.cs b/OpenNest.Core/Splitting/DrawingSplitter.cs new file mode 100644 index 0000000..0a9785b --- /dev/null +++ b/OpenNest.Core/Splitting/DrawingSplitter.cs @@ -0,0 +1,375 @@ +using System.Collections.Generic; +using System.Linq; +using OpenNest.Converters; +using OpenNest.Geometry; + +namespace OpenNest; + +/// +/// Splits a Drawing into multiple pieces along split lines with optional feature geometry. +/// +public static class DrawingSplitter +{ + public static List Split(Drawing drawing, List splitLines, SplitParameters parameters) + { + if (splitLines.Count == 0) + return new List { 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 }; + + // 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(); + 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(); + 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(); + 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 BuildClipRegions(List 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 { bounds.Left }; + xEdges.AddRange(verticals.Select(v => v.Position)); + xEdges.Add(bounds.Right); + + var yEdges = new List { bounds.Bottom }; + yEdges.AddRange(horizontals.Select(h => h.Position)); + yEdges.Add(bounds.Top); + + var regions = new List(); + 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; + } + + /// + /// Clip perimeter to a region using Clipper2, then recover original arcs and stitch in feature edges. + /// + private static List ClipPerimeterToRegion(Shape perimeter, Box region, + List 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(); + + var clippedPoly = NoFitPolygon.FromClipperPath(result[0]); + var clippedEntities = new List(); + + 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(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 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; + } + + /// + /// Search original perimeter for an arc whose circle matches this polygon chord edge. + /// Returns a new arc segment between the chord endpoints if found. + /// + 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 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 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(); + + var clippedPoly = NoFitPolygon.FromClipperPath(result[0]); + var lineEntities = new List(); + 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() + }; + } +} diff --git a/OpenNest.Core/Splitting/ISplitFeature.cs b/OpenNest.Core/Splitting/ISplitFeature.cs new file mode 100644 index 0000000..055d73d --- /dev/null +++ b/OpenNest.Core/Splitting/ISplitFeature.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest; + +public class SplitFeatureResult +{ + public List NegativeSideEdge { get; } + public List PositiveSideEdge { get; } + + public SplitFeatureResult(List negativeSideEdge, List positiveSideEdge) + { + NegativeSideEdge = negativeSideEdge; + PositiveSideEdge = positiveSideEdge; + } +} + +public interface ISplitFeature +{ + string Name { get; } + SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters); +} diff --git a/OpenNest.Core/Splitting/SpikeGrooveSplit.cs b/OpenNest.Core/Splitting/SpikeGrooveSplit.cs new file mode 100644 index 0000000..fa91f63 --- /dev/null +++ b/OpenNest.Core/Splitting/SpikeGrooveSplit.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest; + +/// +/// 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. +/// +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(); + 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 BuildGrooveSide(List pairPositions, double halfWidth, double depth, + double extentStart, double extentEnd, double pos, bool isVertical) + { + var entities = new List(); + 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 BuildSpikeSide(List pairPositions, double halfWidth, double depth, + double extentStart, double extentEnd, double pos, bool isVertical) + { + var entities = new List(); + 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)); + } +} diff --git a/OpenNest.Core/Splitting/SplitLine.cs b/OpenNest.Core/Splitting/SplitLine.cs new file mode 100644 index 0000000..0ef4ec3 --- /dev/null +++ b/OpenNest.Core/Splitting/SplitLine.cs @@ -0,0 +1,17 @@ +namespace OpenNest; + +/// +/// Defines a split line at a position along an axis. +/// For Vertical, Position is the X coordinate. For Horizontal, Position is the Y coordinate. +/// +public class SplitLine +{ + public double Position { get; } + public CutOffAxis Axis { get; } + + public SplitLine(double position, CutOffAxis axis) + { + Position = position; + Axis = axis; + } +} diff --git a/OpenNest.Core/Splitting/SplitParameters.cs b/OpenNest.Core/Splitting/SplitParameters.cs new file mode 100644 index 0000000..d2f25b6 --- /dev/null +++ b/OpenNest.Core/Splitting/SplitParameters.cs @@ -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; + + /// + /// Max protrusion from the split edge (for auto-fit plate size calculation). + /// + public double FeatureOverhang => Type switch + { + SplitType.WeldGapTabs => TabHeight, + SplitType.SpikeGroove => SpikeDepth, + _ => 0 + }; +} diff --git a/OpenNest.Core/Splitting/StraightSplit.cs b/OpenNest.Core/Splitting/StraightSplit.cs new file mode 100644 index 0000000..5ef039d --- /dev/null +++ b/OpenNest.Core/Splitting/StraightSplit.cs @@ -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 { negEdge }, + new List { posEdge }); + } +} diff --git a/OpenNest.Core/Splitting/WeldGapTabSplit.cs b/OpenNest.Core/Splitting/WeldGapTabSplit.cs new file mode 100644 index 0000000..afd5f0c --- /dev/null +++ b/OpenNest.Core/Splitting/WeldGapTabSplit.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest; + +/// +/// 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. +/// +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(); + 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(); + 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); + } +} diff --git a/OpenNest.Tests/Splitting/DrawingSplitterTests.cs b/OpenNest.Tests/Splitting/DrawingSplitterTests.cs new file mode 100644 index 0000000..df986f0 --- /dev/null +++ b/OpenNest.Tests/Splitting/DrawingSplitterTests.cs @@ -0,0 +1,163 @@ +using OpenNest.Converters; +using OpenNest.Geometry; + +namespace OpenNest.Tests.Splitting; + +public class DrawingSplitterTests +{ + /// + /// Helper: creates a Drawing from a rectangular perimeter. + /// + private static Drawing MakeRectangleDrawing(string name, double width, double height) + { + var entities = new List + { + 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 { 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 { 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 + { + 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 { 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 { 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 + { + 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 + { + 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(); + 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 { 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 + { + 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); + } +} diff --git a/OpenNest.Tests/Splitting/SplitFeatureTests.cs b/OpenNest.Tests/Splitting/SplitFeatureTests.cs new file mode 100644 index 0000000..9fcd8f0 --- /dev/null +++ b/OpenNest.Tests/Splitting/SplitFeatureTests.cs @@ -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(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(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().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(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(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(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(e)); + Assert.All(result.PositiveSideEdge, e => Assert.IsType(e)); + + // Spikes protrude in negative-X direction (into the negative side's territory) + var posLines = result.PositiveSideEdge.Cast().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().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); + } +} diff --git a/OpenNest.Tests/Splitting/SplitLineTests.cs b/OpenNest.Tests/Splitting/SplitLineTests.cs new file mode 100644 index 0000000..69b7065 --- /dev/null +++ b/OpenNest.Tests/Splitting/SplitLineTests.cs @@ -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); + } +} diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index 240f047..c5434bf 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -23,6 +23,17 @@ namespace OpenNest.Forms Items = new BindingList(); 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 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(); + 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 Entities { get; set; } + + [Browsable(false)] + public List SplitDrawings { get; set; } } class ColorItem diff --git a/OpenNest/Forms/SplitDrawingForm.Designer.cs b/OpenNest/Forms/SplitDrawingForm.Designer.cs new file mode 100644 index 0000000..dc2ca13 --- /dev/null +++ b/OpenNest/Forms/SplitDrawingForm.Designer.cs @@ -0,0 +1,641 @@ +namespace OpenNest.Forms +{ + partial class SplitDrawingForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + 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; + } +} diff --git a/OpenNest/Forms/SplitDrawingForm.cs b/OpenNest/Forms/SplitDrawingForm.cs new file mode 100644 index 0000000..a982232 --- /dev/null +++ b/OpenNest/Forms/SplitDrawingForm.cs @@ -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 _drawingEntities; + private readonly Box _drawingBounds; + private readonly List _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 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 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 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 { _drawingBounds.Left }; + xEdges.AddRange(verticals.Select(v => v.Position)); + xEdges.Add(_drawingBounds.Right); + + var yEdges = new List { _drawingBounds.Bottom }; + yEdges.AddRange(horizontals.Select(h => h.Position)); + yEdges.Add(_drawingBounds.Top); + + var regions = new List(); + 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"; + } +}