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() {