From 765a86244019520479cfcae9de4e3b6b7183d06e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 11:56:00 -0400 Subject: [PATCH 1/9] feat: add SplitLine and SplitParameters models Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Core/Splitting/SplitLine.cs | 17 +++++++++++ OpenNest.Core/Splitting/SplitParameters.cs | 33 ++++++++++++++++++++++ OpenNest.Tests/Splitting/SplitLineTests.cs | 31 ++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 OpenNest.Core/Splitting/SplitLine.cs create mode 100644 OpenNest.Core/Splitting/SplitParameters.cs create mode 100644 OpenNest.Tests/Splitting/SplitLineTests.cs 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.Tests/Splitting/SplitLineTests.cs b/OpenNest.Tests/Splitting/SplitLineTests.cs new file mode 100644 index 0000000..1abb55e --- /dev/null +++ b/OpenNest.Tests/Splitting/SplitLineTests.cs @@ -0,0 +1,31 @@ +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); + } +} From 5afb311ac719e95b46bfcdcd306735f24fce5e24 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 11:58:00 -0400 Subject: [PATCH 2/9] feat: add ISplitFeature interface and StraightSplit implementation Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Core/Splitting/ISplitFeature.cs | 22 ++++++++ OpenNest.Core/Splitting/StraightSplit.cs | 22 ++++++++ OpenNest.Tests/Splitting/SplitFeatureTests.cs | 52 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 OpenNest.Core/Splitting/ISplitFeature.cs create mode 100644 OpenNest.Core/Splitting/StraightSplit.cs create mode 100644 OpenNest.Tests/Splitting/SplitFeatureTests.cs 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/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.Tests/Splitting/SplitFeatureTests.cs b/OpenNest.Tests/Splitting/SplitFeatureTests.cs new file mode 100644 index 0000000..5818744 --- /dev/null +++ b/OpenNest.Tests/Splitting/SplitFeatureTests.cs @@ -0,0 +1,52 @@ +using OpenNest.Geometry; + +namespace OpenNest.Tests.Splitting; + +public class SplitFeatureTests +{ + [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); + } +} From c2c3e23024fa52d03575785c452b5a22a801a8dd Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 11:59:45 -0400 Subject: [PATCH 3/9] feat: add WeldGapTabSplit implementation Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Core/Splitting/WeldGapTabSplit.cs | 82 +++++++++++++++++++ OpenNest.Tests/Splitting/SplitFeatureTests.cs | 44 ++++++++++ 2 files changed, 126 insertions(+) create mode 100644 OpenNest.Core/Splitting/WeldGapTabSplit.cs 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/SplitFeatureTests.cs b/OpenNest.Tests/Splitting/SplitFeatureTests.cs index 5818744..eef2c5c 100644 --- a/OpenNest.Tests/Splitting/SplitFeatureTests.cs +++ b/OpenNest.Tests/Splitting/SplitFeatureTests.cs @@ -1,9 +1,53 @@ +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() { From 499e0425b5c33a236f186ab0aa647e535025ca9f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 12:01:40 -0400 Subject: [PATCH 4/9] feat: add SpikeGrooveSplit implementation Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Core/Splitting/SpikeGrooveSplit.cs | 105 ++++++++++++++++++ OpenNest.Tests/Splitting/SplitFeatureTests.cs | 40 +++++++ 2 files changed, 145 insertions(+) create mode 100644 OpenNest.Core/Splitting/SpikeGrooveSplit.cs 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.Tests/Splitting/SplitFeatureTests.cs b/OpenNest.Tests/Splitting/SplitFeatureTests.cs index eef2c5c..9fcd8f0 100644 --- a/OpenNest.Tests/Splitting/SplitFeatureTests.cs +++ b/OpenNest.Tests/Splitting/SplitFeatureTests.cs @@ -93,4 +93,44 @@ public class SplitFeatureTests { 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); + } } From d7b095cf2dfb2d9e5cc053f31960d7cfc4c3e0d6 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 12:03:18 -0400 Subject: [PATCH 5/9] feat: add AutoSplitCalculator for fit-to-plate and split-by-count Co-Authored-By: Claude Sonnet 4.6 --- .../Splitting/AutoSplitCalculator.cs | 59 +++++++++++++++++++ OpenNest.Tests/Splitting/SplitLineTests.cs | 56 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 OpenNest.Core/Splitting/AutoSplitCalculator.cs 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.Tests/Splitting/SplitLineTests.cs b/OpenNest.Tests/Splitting/SplitLineTests.cs index 1abb55e..69b7065 100644 --- a/OpenNest.Tests/Splitting/SplitLineTests.cs +++ b/OpenNest.Tests/Splitting/SplitLineTests.cs @@ -1,3 +1,5 @@ +using OpenNest.Geometry; + namespace OpenNest.Tests.Splitting; public class SplitLineTests @@ -29,3 +31,57 @@ public class SplitLineTests 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); + } +} From 4acd8b8bad2b01f652908e74e085bc6ee7b4687d Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 12:13:37 -0400 Subject: [PATCH 6/9] feat: add DrawingSplitter core split pipeline Implements the main drawing splitting algorithm that orchestrates splitting a Drawing into multiple pieces along split lines using Clipper2 polygon clipping. After clipping, recovers original arcs by matching clipped edges back to perimeter entities, stitches in feature edges from ISplitFeature where polygon edges lie on split lines, and normalizes each piece's origin. Key fix from plan: filters rapid-layer entities before ShapeProfile construction so cutouts are properly separated from perimeters. Includes 7 integration tests covering vertical/horizontal splits, three-way splits, property copying, origin normalization, cutout assignment, and grid (cross) splits. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Splitting/DrawingSplitter.cs | 375 ++++++++++++++++++ .../Splitting/DrawingSplitterTests.cs | 163 ++++++++ 2 files changed, 538 insertions(+) create mode 100644 OpenNest.Core/Splitting/DrawingSplitter.cs create mode 100644 OpenNest.Tests/Splitting/DrawingSplitterTests.cs 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.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); + } +} From adb8ed12d76fb1b37b4c803b7af886fb5fbe135c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 12:18:31 -0400 Subject: [PATCH 7/9] feat: add SplitDrawingForm UI dialog Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Forms/SplitDrawingForm.Designer.cs | 641 ++++++++++++++++++++ OpenNest/Forms/SplitDrawingForm.cs | 363 +++++++++++ 2 files changed, 1004 insertions(+) create mode 100644 OpenNest/Forms/SplitDrawingForm.Designer.cs create mode 100644 OpenNest/Forms/SplitDrawingForm.cs 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"; + } +} From d91ffccfa3aeb5950138cca54ec36b8110cbfd2b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 12:20:22 -0400 Subject: [PATCH 8/9] feat: add Split button to CadConverterForm to open SplitDrawingForm Adds a Split button column to the DXF converter grid. Clicking it builds a temporary Drawing from the item's visible entities and opens SplitDrawingForm; if the user confirms, the split results replace the original item in GetDrawings(). Co-Authored-By: Claude Sonnet 4.6 --- OpenNest/Forms/CadConverterForm.cs | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) 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 From addd7acc3cf0283e1e4b87ca72847f7892330d6b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 24 Mar 2026 12:21:45 -0400 Subject: [PATCH 9/9] docs: add split drawing feature to architecture documentation --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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.