diff --git a/OpenNest.Core/Splitting/SplitLine.cs b/OpenNest.Core/Splitting/SplitLine.cs
index a4e55c3..54435aa 100644
--- a/OpenNest.Core/Splitting/SplitLine.cs
+++ b/OpenNest.Core/Splitting/SplitLine.cs
@@ -1,3 +1,4 @@
+using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest;
@@ -23,4 +24,16 @@ public class SplitLine
Position = position;
Axis = axis;
}
+
+ ///
+ /// Returns a Line entity at the split position spanning the given extent range.
+ /// For Vertical: line from (Position, extentStart) to (Position, extentEnd).
+ /// For Horizontal: line from (extentStart, Position) to (extentEnd, Position).
+ ///
+ public Line ToLine(double extentStart, double extentEnd)
+ {
+ return Axis == CutOffAxis.Vertical
+ ? new Line(Position, extentStart, Position, extentEnd)
+ : new Line(extentStart, Position, extentEnd, Position);
+ }
}
diff --git a/OpenNest.Core/Splitting/SplitLineIntersect.cs b/OpenNest.Core/Splitting/SplitLineIntersect.cs
new file mode 100644
index 0000000..222febd
--- /dev/null
+++ b/OpenNest.Core/Splitting/SplitLineIntersect.cs
@@ -0,0 +1,81 @@
+using OpenNest.Geometry;
+using OpenNest.Math;
+using System.Collections.Generic;
+
+namespace OpenNest;
+
+///
+/// Static helpers for testing entity-splitline intersections.
+///
+public static class SplitLineIntersect
+{
+ ///
+ /// Finds the intersection point between an entity and a split line.
+ /// Returns null if no intersection or the entity doesn't straddle the split line.
+ ///
+ public static Vector? FindIntersection(Entity entity, SplitLine sl)
+ {
+ if (!CrossesSplitLine(entity, sl))
+ return null;
+
+ var bbox = entity.BoundingBox;
+ var margin = 1.0;
+
+ // Create a line at the split position spanning the entity's bbox extent (with margin)
+ Line splitLine;
+
+ if (sl.Axis == CutOffAxis.Vertical)
+ splitLine = sl.ToLine(bbox.Bottom - margin, bbox.Top + margin);
+ else
+ splitLine = sl.ToLine(bbox.Left - margin, bbox.Right + margin);
+
+ switch (entity.Type)
+ {
+ case EntityType.Line:
+ var line = (Line)entity;
+ if (Intersect.Intersects(line, splitLine, out var pt))
+ return pt;
+ return null;
+
+ case EntityType.Arc:
+ var arc = (Arc)entity;
+ if (Intersect.Intersects(arc, splitLine, out var pts))
+ return pts.Count > 0 ? pts[0] : null;
+ return null;
+
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Returns true if the entity's bounding box straddles the split line,
+ /// meaning it extends to both sides of the split position (not just touching).
+ ///
+ public static bool CrossesSplitLine(Entity entity, SplitLine sl)
+ {
+ var bbox = entity.BoundingBox;
+
+ if (sl.Axis == CutOffAxis.Vertical)
+ return bbox.Left < sl.Position - Tolerance.Epsilon
+ && bbox.Right > sl.Position + Tolerance.Epsilon;
+ else
+ return bbox.Bottom < sl.Position - Tolerance.Epsilon
+ && bbox.Top > sl.Position + Tolerance.Epsilon;
+ }
+
+ ///
+ /// Returns -1 if the point is below/left of the split line,
+ /// +1 if above/right, or 0 if on the line (within tolerance).
+ ///
+ public static int SideOf(Vector pt, SplitLine sl)
+ {
+ var value = sl.Axis == CutOffAxis.Vertical ? pt.X : pt.Y;
+ var diff = value - sl.Position;
+
+ if (System.Math.Abs(diff) <= Tolerance.Epsilon)
+ return 0;
+
+ return diff < 0 ? -1 : 1;
+ }
+}
diff --git a/OpenNest.Tests/Splitting/EntitySplitTests.cs b/OpenNest.Tests/Splitting/EntitySplitTests.cs
new file mode 100644
index 0000000..b027873
--- /dev/null
+++ b/OpenNest.Tests/Splitting/EntitySplitTests.cs
@@ -0,0 +1,206 @@
+using OpenNest.Geometry;
+using OpenNest.Math;
+
+namespace OpenNest.Tests.Splitting;
+
+public class EntitySplitTests
+{
+ // --- SplitLine.ToLine ---
+
+ [Fact]
+ public void ToLine_Vertical_ReturnsVerticalLine()
+ {
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+ var line = sl.ToLine(0, 100);
+
+ Assert.Equal(50.0, line.StartPoint.X, 5);
+ Assert.Equal(0.0, line.StartPoint.Y, 5);
+ Assert.Equal(50.0, line.EndPoint.X, 5);
+ Assert.Equal(100.0, line.EndPoint.Y, 5);
+ }
+
+ [Fact]
+ public void ToLine_Horizontal_ReturnsHorizontalLine()
+ {
+ var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
+ var line = sl.ToLine(10, 90);
+
+ Assert.Equal(10.0, line.StartPoint.X, 5);
+ Assert.Equal(30.0, line.StartPoint.Y, 5);
+ Assert.Equal(90.0, line.EndPoint.X, 5);
+ Assert.Equal(30.0, line.EndPoint.Y, 5);
+ }
+
+ // --- FindIntersection: Line crossing vertical split ---
+
+ [Fact]
+ public void FindIntersection_LineCrossesVerticalSplit_ReturnsPoint()
+ {
+ // Diagonal line from (0,0) to (100,100), vertical split at x=50
+ var line = new Line(0, 0, 100, 100);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ var result = SplitLineIntersect.FindIntersection(line, sl);
+
+ Assert.NotNull(result);
+ Assert.Equal(50.0, result.Value.X, 5);
+ Assert.Equal(50.0, result.Value.Y, 5);
+ }
+
+ // --- FindIntersection: Line NOT crossing ---
+
+ [Fact]
+ public void FindIntersection_LineDoesNotCross_ReturnsNull()
+ {
+ // Line entirely to the left of split at x=50
+ var line = new Line(0, 0, 40, 40);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ var result = SplitLineIntersect.FindIntersection(line, sl);
+
+ Assert.Null(result);
+ }
+
+ // --- FindIntersection: Line parallel to split ---
+
+ [Fact]
+ public void FindIntersection_LineParallelToSplit_ReturnsNull()
+ {
+ // Vertical line at x=50 — parallel to vertical split at x=50
+ var line = new Line(50, 0, 50, 100);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ var result = SplitLineIntersect.FindIntersection(line, sl);
+
+ Assert.Null(result);
+ }
+
+ // --- FindIntersection: Arc crossing vertical split ---
+
+ [Fact]
+ public void FindIntersection_ArcCrossesVerticalSplit_ReturnsPoint()
+ {
+ // Arc centered at (60,50), radius 20, from PI to 0 (CCW).
+ // CCW from PI wraps through 3PI/2 (bottom) then 0 (right).
+ // At x=50: (50-60)^2 + (y-50)^2 = 400 => (y-50)^2 = 300
+ // y = 50 - sqrt(300) ≈ 32.68 (bottom intersection, on the arc)
+ // y = 50 + sqrt(300) ≈ 67.32 (top intersection, also on the arc)
+ var arc = new Arc(60, 50, 20, System.Math.PI, 0, false);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ var result = SplitLineIntersect.FindIntersection(arc, sl);
+
+ Assert.NotNull(result);
+ Assert.Equal(50.0, result.Value.X, 1);
+ // The first intersection found by the circle-line algorithm; either ~32.68 or ~67.32
+ var y = result.Value.Y;
+ var expectedLow = 50.0 - System.Math.Sqrt(300);
+ var expectedHigh = 50.0 + System.Math.Sqrt(300);
+ Assert.True(
+ System.Math.Abs(y - expectedLow) < 0.1 || System.Math.Abs(y - expectedHigh) < 0.1,
+ $"Expected Y near {expectedLow:F2} or {expectedHigh:F2}, got {y:F2}");
+ }
+
+ // --- CrossesSplitLine ---
+
+ [Fact]
+ public void CrossesSplitLine_LineStraddles_ReturnsTrue()
+ {
+ var line = new Line(40, 0, 60, 100);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ Assert.True(SplitLineIntersect.CrossesSplitLine(line, sl));
+ }
+
+ [Fact]
+ public void CrossesSplitLine_LineEntirelyOnOneSide_ReturnsFalse()
+ {
+ var line = new Line(10, 0, 40, 100);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ Assert.False(SplitLineIntersect.CrossesSplitLine(line, sl));
+ }
+
+ [Fact]
+ public void CrossesSplitLine_HorizontalSplit_Works()
+ {
+ var line = new Line(0, 10, 100, 60);
+ var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
+
+ Assert.True(SplitLineIntersect.CrossesSplitLine(line, sl));
+ }
+
+ [Fact]
+ public void CrossesSplitLine_LineTouchingButNotStraddling_ReturnsFalse()
+ {
+ // Line endpoint exactly at the split line — bbox right == 50, left < 50
+ // But right must be > pos (strictly), so touching exactly returns false
+ var line = new Line(10, 0, 50, 100);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ Assert.False(SplitLineIntersect.CrossesSplitLine(line, sl));
+ }
+
+ // --- SideOf ---
+
+ [Fact]
+ public void SideOf_PointLeftOfVerticalSplit_ReturnsNegative()
+ {
+ var pt = new Vector(30, 50);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ Assert.Equal(-1, SplitLineIntersect.SideOf(pt, sl));
+ }
+
+ [Fact]
+ public void SideOf_PointRightOfVerticalSplit_ReturnsPositive()
+ {
+ var pt = new Vector(70, 50);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ Assert.Equal(1, SplitLineIntersect.SideOf(pt, sl));
+ }
+
+ [Fact]
+ public void SideOf_PointOnVerticalSplit_ReturnsZero()
+ {
+ var pt = new Vector(50, 50);
+ var sl = new SplitLine(50.0, CutOffAxis.Vertical);
+
+ Assert.Equal(0, SplitLineIntersect.SideOf(pt, sl));
+ }
+
+ [Fact]
+ public void SideOf_PointBelowHorizontalSplit_ReturnsNegative()
+ {
+ var pt = new Vector(50, 10);
+ var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
+
+ Assert.Equal(-1, SplitLineIntersect.SideOf(pt, sl));
+ }
+
+ [Fact]
+ public void SideOf_PointAboveHorizontalSplit_ReturnsPositive()
+ {
+ var pt = new Vector(50, 60);
+ var sl = new SplitLine(30.0, CutOffAxis.Horizontal);
+
+ Assert.Equal(1, SplitLineIntersect.SideOf(pt, sl));
+ }
+
+ // --- FindIntersection: horizontal split ---
+
+ [Fact]
+ public void FindIntersection_LineCrossesHorizontalSplit_ReturnsPoint()
+ {
+ // Diagonal line from (0,0) to (100,100), horizontal split at y=50
+ var line = new Line(0, 0, 100, 100);
+ var sl = new SplitLine(50.0, CutOffAxis.Horizontal);
+
+ var result = SplitLineIntersect.FindIntersection(line, sl);
+
+ Assert.NotNull(result);
+ Assert.Equal(50.0, result.Value.X, 5);
+ Assert.Equal(50.0, result.Value.Y, 5);
+ }
+}