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()
};
}
}