feat: replace Clipper2 with direct entity splitting in DrawingSplitter
Replace polygon boolean clipping with direct entity splitting using bounding box filtering and exact intersection math. Eliminates Clipper2 precision drift that caused contour gaps (0.0035") breaking area calculation and ShapeBuilder chaining. Also fixes SpikeGrooveSplit: spike depth is now grooveDepth + weldGap (spike protrudes past groove), both V-shapes use same angle formula, and weldGap no longer double-subtracted from tip depth. SplitDrawingForm: fix parameter mapping (GrooveDepth direct from nud, not inflated), remove redundant Spike Depth display, add feature contour preview and trimmed split lines at feature positions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,20 +15,12 @@ public static class DrawingSplitter
|
||||
if (splitLines.Count == 0)
|
||||
return new List<Drawing> { 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()
|
||||
var profile = BuildProfile(drawing);
|
||||
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)
|
||||
@@ -37,64 +29,25 @@ public static class DrawingSplitter
|
||||
if (sortedLines.Count == 0)
|
||||
return new List<Drawing> { 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<Drawing>();
|
||||
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<Entity>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
var cutoutEntities = CollectCutouts(profile.Cutouts, region, sortedLines);
|
||||
|
||||
// Normalize origin: translate so bounding box starts at (0,0)
|
||||
var allEntities = new List<Entity>();
|
||||
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;
|
||||
|
||||
var piece = BuildPieceDrawing(drawing, allEntities, pieceIndex);
|
||||
results.Add(piece);
|
||||
pieceIndex++;
|
||||
}
|
||||
@@ -102,6 +55,52 @@ public static class DrawingSplitter
|
||||
return results;
|
||||
}
|
||||
|
||||
private static ShapeProfile BuildProfile(Drawing drawing)
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.ToList();
|
||||
return new ShapeProfile(entities);
|
||||
}
|
||||
|
||||
private static List<Entity> CollectCutouts(List<Shape> cutouts, Box region, List<SplitLine> splitLines)
|
||||
{
|
||||
var entities = new List<Entity>();
|
||||
foreach (var cutout in cutouts)
|
||||
{
|
||||
if (IsCutoutInRegion(cutout, region))
|
||||
entities.AddRange(cutout.Entities);
|
||||
else if (DoesCutoutCrossSplitLine(cutout, splitLines))
|
||||
{
|
||||
var clipped = ClipCutoutToRegion(cutout, region, splitLines);
|
||||
if (clipped.Count > 0)
|
||||
entities.AddRange(clipped);
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static Drawing BuildPieceDrawing(Drawing source, List<Entity> entities, int pieceIndex)
|
||||
{
|
||||
var pieceBounds = entities.Select(e => e.BoundingBox).ToList().GetBoundingBox();
|
||||
var offsetX = -pieceBounds.X;
|
||||
var offsetY = -pieceBounds.Y;
|
||||
|
||||
foreach (var e in entities)
|
||||
e.Offset(offsetX, offsetY);
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
var piece = new Drawing($"{source.Name}-{pieceIndex}", pgm);
|
||||
piece.Color = source.Color;
|
||||
piece.Priority = source.Priority;
|
||||
piece.Material = source.Material;
|
||||
piece.Constraints = source.Constraints;
|
||||
piece.Customer = source.Customer;
|
||||
piece.Source = source.Source;
|
||||
piece.Quantity.Required = source.Quantity.Required;
|
||||
return piece;
|
||||
}
|
||||
|
||||
private static void DecomposeCircles(ShapeProfile profile)
|
||||
{
|
||||
DecomposeCirclesInShape(profile.Perimeter);
|
||||
@@ -155,138 +154,248 @@ public static class DrawingSplitter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clip perimeter to a region using Clipper2, then recover original arcs and stitch in feature edges.
|
||||
/// Clip perimeter to a region by walking entities, splitting at split line crossings,
|
||||
/// and stitching in feature edges. No polygon clipping library needed.
|
||||
/// </summary>
|
||||
private static List<Entity> ClipPerimeterToRegion(Shape perimeter, Box region,
|
||||
List<SplitLine> splitLines, ISplitFeature feature, SplitParameters parameters)
|
||||
{
|
||||
var perimPoly = perimeter.ToPolygonWithTolerance(0.01);
|
||||
var boundarySplitLines = GetBoundarySplitLines(region, splitLines);
|
||||
var entities = new List<Entity>();
|
||||
var splitPoints = new List<(Vector Point, SplitLine Line, bool IsExit)>();
|
||||
|
||||
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();
|
||||
foreach (var entity in perimeter.Entities)
|
||||
{
|
||||
ProcessEntity(entity, region, boundarySplitLines, entities, splitPoints);
|
||||
}
|
||||
|
||||
// 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)
|
||||
if (entities.Count == 0)
|
||||
return new List<Entity>();
|
||||
|
||||
var clippedPoly = NoFitPolygon.FromClipperPath(result[0]);
|
||||
var clippedEntities = new List<Entity>();
|
||||
InsertFeatureEdges(entities, splitPoints, region, boundarySplitLines, feature, parameters);
|
||||
EnsurePerimeterWinding(entities);
|
||||
return entities;
|
||||
}
|
||||
|
||||
var verts = clippedPoly.Vertices;
|
||||
for (var i = 0; i < verts.Count - 1; i++)
|
||||
private static void ProcessEntity(Entity entity, Box region,
|
||||
List<SplitLine> boundarySplitLines, List<Entity> entities,
|
||||
List<(Vector Point, SplitLine Line, bool IsExit)> splitPoints)
|
||||
{
|
||||
// Find the first boundary split line this entity crosses
|
||||
SplitLine crossedLine = null;
|
||||
Vector? intersectionPt = null;
|
||||
|
||||
foreach (var sl in boundarySplitLines)
|
||||
{
|
||||
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)
|
||||
if (SplitLineIntersect.CrossesSplitLine(entity, sl))
|
||||
{
|
||||
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 pt = SplitLineIntersect.FindIntersection(entity, sl);
|
||||
if (pt != null)
|
||||
{
|
||||
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<Entity>(featureEdge);
|
||||
featureEdge.Reverse();
|
||||
foreach (var e in featureEdge)
|
||||
e.Reverse();
|
||||
}
|
||||
crossedLine = sl;
|
||||
intersectionPt = pt;
|
||||
break;
|
||||
}
|
||||
|
||||
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();
|
||||
if (crossedLine != null)
|
||||
{
|
||||
// Entity crosses a split line — split it and keep the half inside the region
|
||||
var regionSide = RegionSideOf(region, crossedLine);
|
||||
var startPt = GetStartPoint(entity);
|
||||
var startSide = SplitLineIntersect.SideOf(startPt, crossedLine);
|
||||
var startInRegion = startSide == regionSide || startSide == 0;
|
||||
|
||||
return shape.Entities;
|
||||
SplitEntityAtPoint(entity, intersectionPt.Value, startInRegion, crossedLine, entities, splitPoints);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Entity doesn't cross any boundary split line — check if it's inside the region
|
||||
var mid = MidPoint(entity);
|
||||
if (region.Contains(mid))
|
||||
entities.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private static SplitLine FindSplitLineForEdge(Vector start, Vector end, List<SplitLine> splitLines)
|
||||
private static void SplitEntityAtPoint(Entity entity, Vector point, bool startInRegion,
|
||||
SplitLine crossedLine, List<Entity> entities,
|
||||
List<(Vector Point, SplitLine Line, bool IsExit)> splitPoints)
|
||||
{
|
||||
if (entity is Line line)
|
||||
{
|
||||
var (first, second) = line.SplitAt(point);
|
||||
if (startInRegion)
|
||||
{
|
||||
if (first != null) entities.Add(first);
|
||||
splitPoints.Add((point, crossedLine, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
splitPoints.Add((point, crossedLine, false));
|
||||
if (second != null) entities.Add(second);
|
||||
}
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
var (first, second) = arc.SplitAt(point);
|
||||
if (startInRegion)
|
||||
{
|
||||
if (first != null) entities.Add(first);
|
||||
splitPoints.Add((point, crossedLine, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
splitPoints.Add((point, crossedLine, false));
|
||||
if (second != null) entities.Add(second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns split lines whose position matches a boundary edge of the region.
|
||||
/// </summary>
|
||||
private static List<SplitLine> GetBoundarySplitLines(Box region, List<SplitLine> splitLines)
|
||||
{
|
||||
var result = new List<SplitLine>();
|
||||
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;
|
||||
if (System.Math.Abs(sl.Position - region.Left) < OpenNest.Math.Tolerance.Epsilon
|
||||
|| System.Math.Abs(sl.Position - region.Right) < OpenNest.Math.Tolerance.Epsilon)
|
||||
result.Add(sl);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (System.Math.Abs(start.Y - sl.Position) < 0.1 && System.Math.Abs(end.Y - sl.Position) < 0.1)
|
||||
return sl;
|
||||
if (System.Math.Abs(sl.Position - region.Bottom) < OpenNest.Math.Tolerance.Epsilon
|
||||
|| System.Math.Abs(sl.Position - region.Top) < OpenNest.Math.Tolerance.Epsilon)
|
||||
result.Add(sl);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search original perimeter for an arc whose circle matches this polygon chord edge.
|
||||
/// Returns a new arc segment between the chord endpoints if found.
|
||||
/// Returns -1 or +1 indicating which side of the split line the region center is on.
|
||||
/// </summary>
|
||||
private static Arc FindMatchingArc(Vector start, Vector end, Shape perimeter)
|
||||
private static int RegionSideOf(Box region, SplitLine sl)
|
||||
{
|
||||
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;
|
||||
return SplitLineIntersect.SideOf(region.Center, sl);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns the midpoint of an entity. For lines: average of endpoints.
|
||||
/// For arcs: point at the mid-angle.
|
||||
/// </summary>
|
||||
private static Vector MidPoint(Entity entity)
|
||||
{
|
||||
if (entity is Line line)
|
||||
return line.MidPoint;
|
||||
|
||||
if (entity is Arc arc)
|
||||
{
|
||||
var midAngle = (arc.StartAngle + arc.EndAngle) / 2;
|
||||
return new Vector(
|
||||
arc.Center.X + arc.Radius * System.Math.Cos(midAngle),
|
||||
arc.Center.Y + arc.Radius * System.Math.Sin(midAngle));
|
||||
}
|
||||
|
||||
return new Vector(0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups split points by split line, pairs exits with entries, and generates feature edges.
|
||||
/// </summary>
|
||||
private static void InsertFeatureEdges(List<Entity> entities,
|
||||
List<(Vector Point, SplitLine Line, bool IsExit)> splitPoints,
|
||||
Box region, List<SplitLine> boundarySplitLines,
|
||||
ISplitFeature feature, SplitParameters parameters)
|
||||
{
|
||||
// Group split points by their split line
|
||||
var groups = new Dictionary<SplitLine, List<(Vector Point, bool IsExit)>>();
|
||||
foreach (var sp in splitPoints)
|
||||
{
|
||||
if (!groups.ContainsKey(sp.Line))
|
||||
groups[sp.Line] = new List<(Vector, bool)>();
|
||||
groups[sp.Line].Add((sp.Point, sp.IsExit));
|
||||
}
|
||||
|
||||
foreach (var kvp in groups)
|
||||
{
|
||||
var sl = kvp.Key;
|
||||
var points = kvp.Value;
|
||||
|
||||
// Pair each exit with the next entry
|
||||
var exits = points.Where(p => p.IsExit).Select(p => p.Point).ToList();
|
||||
var entries = points.Where(p => !p.IsExit).Select(p => p.Point).ToList();
|
||||
|
||||
if (exits.Count == 0 || entries.Count == 0)
|
||||
continue;
|
||||
|
||||
// For each exit, find the matching entry to form the feature edge span
|
||||
// Sort exits and entries by their position along the split line
|
||||
var isVertical = sl.Axis == CutOffAxis.Vertical;
|
||||
exits = exits.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
entries = entries.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
|
||||
// Pair them up: each exit with the next entry (or vice versa)
|
||||
var pairCount = System.Math.Min(exits.Count, entries.Count);
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
{
|
||||
var exitPt = exits[i];
|
||||
var entryPt = entries[i];
|
||||
|
||||
var extentStart = isVertical
|
||||
? System.Math.Min(exitPt.Y, entryPt.Y)
|
||||
: System.Math.Min(exitPt.X, entryPt.X);
|
||||
var extentEnd = isVertical
|
||||
? System.Math.Max(exitPt.Y, entryPt.Y)
|
||||
: System.Math.Max(exitPt.X, entryPt.X);
|
||||
|
||||
var featureResult = feature.GenerateFeatures(sl, extentStart, extentEnd, parameters);
|
||||
|
||||
var isNegativeSide = RegionSideOf(region, sl) < 0;
|
||||
var featureEdge = isNegativeSide ? featureResult.NegativeSideEdge : featureResult.PositiveSideEdge;
|
||||
|
||||
if (featureEdge.Count > 0)
|
||||
featureEdge = AlignFeatureDirection(featureEdge, exitPt, entryPt, sl.Axis);
|
||||
|
||||
entities.AddRange(featureEdge);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<Entity> AlignFeatureDirection(List<Entity> featureEdge, Vector start, Vector end, CutOffAxis axis)
|
||||
{
|
||||
var featureStart = GetStartPoint(featureEdge[0]);
|
||||
var featureEnd = GetEndPoint(featureEdge[^1]);
|
||||
var isVertical = axis == CutOffAxis.Vertical;
|
||||
|
||||
var edgeGoesForward = isVertical ? start.Y < end.Y : start.X < end.X;
|
||||
var featureGoesForward = isVertical ? featureStart.Y < featureEnd.Y : featureStart.X < featureEnd.X;
|
||||
|
||||
if (edgeGoesForward != featureGoesForward)
|
||||
{
|
||||
featureEdge = new List<Entity>(featureEdge);
|
||||
featureEdge.Reverse();
|
||||
foreach (var e in featureEdge)
|
||||
e.Reverse();
|
||||
}
|
||||
|
||||
return featureEdge;
|
||||
}
|
||||
|
||||
private static void EnsurePerimeterWinding(List<Entity> entities)
|
||||
{
|
||||
var shape = new Shape();
|
||||
shape.Entities.AddRange(entities);
|
||||
var poly = shape.ToPolygon();
|
||||
if (poly != null && poly.RotationDirection() != RotationType.CW)
|
||||
shape.Reverse();
|
||||
|
||||
entities.Clear();
|
||||
entities.AddRange(shape.Entities);
|
||||
}
|
||||
|
||||
private static bool IsCutoutInRegion(Shape cutout, Box region)
|
||||
@@ -309,32 +418,53 @@ public static class DrawingSplitter
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Entity> ClipCutoutToRegion(Shape cutout, Box region)
|
||||
/// <summary>
|
||||
/// Clip a cutout shape to a region by walking entities, splitting at split line
|
||||
/// intersections, keeping portions inside the region, and closing gaps with
|
||||
/// straight lines. No polygon clipping library needed.
|
||||
/// </summary>
|
||||
private static List<Entity> ClipCutoutToRegion(Shape cutout, Box region, List<SplitLine> splitLines)
|
||||
{
|
||||
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 boundarySplitLines = GetBoundarySplitLines(region, splitLines);
|
||||
var entities = new List<Entity>();
|
||||
var splitPoints = new List<(Vector Point, SplitLine Line, bool IsExit)>();
|
||||
|
||||
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);
|
||||
foreach (var entity in cutout.Entities)
|
||||
{
|
||||
ProcessEntity(entity, region, boundarySplitLines, entities, splitPoints);
|
||||
}
|
||||
|
||||
if (result.Count == 0)
|
||||
if (entities.Count == 0)
|
||||
return new List<Entity>();
|
||||
|
||||
var clippedPoly = NoFitPolygon.FromClipperPath(result[0]);
|
||||
var lineEntities = new List<Entity>();
|
||||
var verts = clippedPoly.Vertices;
|
||||
for (var i = 0; i < verts.Count - 1; i++)
|
||||
lineEntities.Add(new Line(verts[i], verts[i + 1]));
|
||||
// Close gaps with straight lines (connect exit→entry pairs)
|
||||
var groups = new Dictionary<SplitLine, List<(Vector Point, bool IsExit)>>();
|
||||
foreach (var sp in splitPoints)
|
||||
{
|
||||
if (!groups.ContainsKey(sp.Line))
|
||||
groups[sp.Line] = new List<(Vector, bool)>();
|
||||
groups[sp.Line].Add((sp.Point, sp.IsExit));
|
||||
}
|
||||
|
||||
foreach (var kvp in groups)
|
||||
{
|
||||
var sl = kvp.Key;
|
||||
var points = kvp.Value;
|
||||
var isVertical = sl.Axis == CutOffAxis.Vertical;
|
||||
|
||||
var exits = points.Where(p => p.IsExit).Select(p => p.Point)
|
||||
.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
var entries = points.Where(p => !p.IsExit).Select(p => p.Point)
|
||||
.OrderBy(p => isVertical ? p.Y : p.X).ToList();
|
||||
|
||||
var pairCount = System.Math.Min(exits.Count, entries.Count);
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
entities.Add(new Line(exits[i], entries[i]));
|
||||
}
|
||||
|
||||
// Ensure CCW winding for cutouts
|
||||
var shape = new Shape();
|
||||
shape.Entities.AddRange(lineEntities);
|
||||
shape.Entities.AddRange(entities);
|
||||
var poly = shape.ToPolygon();
|
||||
if (poly != null && poly.RotationDirection() != RotationType.CCW)
|
||||
shape.Reverse();
|
||||
@@ -347,7 +477,7 @@ public static class DrawingSplitter
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.StartPoint,
|
||||
Arc a => a.StartPoint(), // Arc.StartPoint() is a method
|
||||
Arc a => a.StartPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
@@ -357,7 +487,7 @@ public static class DrawingSplitter
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.EndPoint,
|
||||
Arc a => a.EndPoint(), // Arc.EndPoint() is a method
|
||||
Arc a => a.EndPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.
|
||||
/// The weld gap (grooveDepth - spikeDepth) is the clearance at the tip when assembled.
|
||||
/// </summary>
|
||||
public class SpikeGrooveSplit : ISplitFeature
|
||||
{
|
||||
@@ -18,7 +19,6 @@ public class SpikeGrooveSplit : ISplitFeature
|
||||
var pairCount = parameters.SpikePairCount;
|
||||
var spikeDepth = parameters.SpikeDepth;
|
||||
var grooveDepth = parameters.GrooveDepth;
|
||||
var weldGap = parameters.SpikeWeldGap;
|
||||
var angleRad = OpenNest.Math.Angle.ToRadians(parameters.SpikeAngle / 2);
|
||||
var spikeHalfWidth = spikeDepth * System.Math.Tan(angleRad);
|
||||
var grooveHalfWidth = grooveDepth * System.Math.Tan(angleRad);
|
||||
@@ -44,10 +44,8 @@ public class SpikeGrooveSplit : ISplitFeature
|
||||
pairPositions.Add(extentStart + margin + usable * i / (pairCount - 1));
|
||||
}
|
||||
|
||||
// Groove side: V-groove cut deeper than the spike so the spike fits inside
|
||||
var negEntities = BuildGrooveSide(pairPositions, grooveHalfWidth, grooveDepth, extentStart, extentEnd, pos, isVertical);
|
||||
// Spike side: spike protrudes but stops short of the split line by weldGap
|
||||
var posEntities = BuildSpikeSide(pairPositions, spikeHalfWidth, spikeDepth, weldGap, extentStart, extentEnd, pos, isVertical);
|
||||
var posEntities = BuildSpikeSide(pairPositions, spikeHalfWidth, spikeDepth, extentStart, extentEnd, pos, isVertical);
|
||||
|
||||
return new SplitFeatureResult(negEntities, posEntities);
|
||||
}
|
||||
@@ -79,10 +77,8 @@ public class SpikeGrooveSplit : ISplitFeature
|
||||
}
|
||||
|
||||
private static List<Entity> BuildSpikeSide(List<double> pairPositions, double halfWidth, double depth,
|
||||
double weldGap, double extentStart, double extentEnd, double pos, bool isVertical)
|
||||
double extentStart, double extentEnd, double pos, bool isVertical)
|
||||
{
|
||||
// Spike tip stops short of the split line by weldGap
|
||||
var tipDepth = depth - weldGap;
|
||||
var entities = new List<Entity>();
|
||||
var cursor = extentEnd;
|
||||
|
||||
@@ -95,8 +91,8 @@ public class SpikeGrooveSplit : ISplitFeature
|
||||
if (cursor > spikeEnd + OpenNest.Math.Tolerance.Epsilon)
|
||||
entities.Add(MakeLine(pos, cursor, pos, spikeEnd, isVertical));
|
||||
|
||||
entities.Add(MakeLine(pos, spikeEnd, pos - tipDepth, center, isVertical));
|
||||
entities.Add(MakeLine(pos - tipDepth, center, pos, spikeStart, isVertical));
|
||||
entities.Add(MakeLine(pos, spikeEnd, pos - depth, center, isVertical));
|
||||
entities.Add(MakeLine(pos - depth, center, pos, spikeStart, isVertical));
|
||||
|
||||
cursor = spikeStart;
|
||||
}
|
||||
|
||||
127
OpenNest.Tests/Splitting/SplitIntegrationTest.cs
Normal file
127
OpenNest.Tests/Splitting/SplitIntegrationTest.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
public class SplitIntegrationTest
|
||||
{
|
||||
[Fact]
|
||||
public void Split_SpikeGroove_NoContinuityGaps()
|
||||
{
|
||||
// Create a rectangle
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
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 pgm = ConvertGeometry.ToProgram(entities);
|
||||
var drawing = new Drawing("TEST", pgm);
|
||||
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
sl.FeaturePositions.Add(12.5);
|
||||
sl.FeaturePositions.Add(37.5);
|
||||
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.SpikeGroove,
|
||||
GrooveDepth = 0.625,
|
||||
SpikeDepth = 0.75,
|
||||
SpikeWeldGap = 0.125,
|
||||
SpikeAngle = 45,
|
||||
SpikePairCount = 2
|
||||
};
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, new List<SplitLine> { sl }, parameters);
|
||||
Assert.Equal(2, results.Count);
|
||||
|
||||
foreach (var piece in results)
|
||||
{
|
||||
// Get cut entities only (no rapids)
|
||||
var pieceEntities = ConvertProgram.ToGeometry(piece.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
|
||||
// Check that consecutive entity endpoints connect (no gaps)
|
||||
for (var i = 0; i < pieceEntities.Count - 1; i++)
|
||||
{
|
||||
var end = GetEndPoint(pieceEntities[i]);
|
||||
var start = GetStartPoint(pieceEntities[i + 1]);
|
||||
var gap = end.DistanceTo(start);
|
||||
Assert.True(gap < 0.01,
|
||||
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
|
||||
}
|
||||
|
||||
// Area should be non-zero
|
||||
Assert.True(piece.Area > 0, $"{piece.Name} has zero area");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_SpikeGroove_Horizontal_NoContinuityGaps()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
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 pgm = ConvertGeometry.ToProgram(entities);
|
||||
var drawing = new Drawing("TEST", pgm);
|
||||
|
||||
var sl = new SplitLine(25.0, CutOffAxis.Horizontal);
|
||||
sl.FeaturePositions.Add(25.0);
|
||||
sl.FeaturePositions.Add(75.0);
|
||||
|
||||
var parameters = new SplitParameters
|
||||
{
|
||||
Type = SplitType.SpikeGroove,
|
||||
GrooveDepth = 0.625,
|
||||
SpikeDepth = 0.75,
|
||||
SpikeWeldGap = 0.125,
|
||||
SpikeAngle = 45,
|
||||
SpikePairCount = 2
|
||||
};
|
||||
|
||||
var results = DrawingSplitter.Split(drawing, new List<SplitLine> { sl }, parameters);
|
||||
Assert.Equal(2, results.Count);
|
||||
|
||||
foreach (var piece in results)
|
||||
{
|
||||
var pieceEntities = ConvertProgram.ToGeometry(piece.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
|
||||
for (var i = 0; i < pieceEntities.Count - 1; i++)
|
||||
{
|
||||
var end = GetEndPoint(pieceEntities[i]);
|
||||
var start = GetStartPoint(pieceEntities[i + 1]);
|
||||
var gap = end.DistanceTo(start);
|
||||
Assert.True(gap < 0.01,
|
||||
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
|
||||
}
|
||||
|
||||
Assert.True(piece.Area > 0, $"{piece.Name} has zero area");
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector GetStartPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.StartPoint,
|
||||
Arc a => a.StartPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector GetEndPoint(Entity entity)
|
||||
{
|
||||
return entity switch
|
||||
{
|
||||
Line l => l.EndPoint,
|
||||
Arc a => a.EndPoint(),
|
||||
_ => new Vector(0, 0)
|
||||
};
|
||||
}
|
||||
}
|
||||
48
OpenNest/Forms/SplitDrawingForm.Designer.cs
generated
48
OpenNest/Forms/SplitDrawingForm.Designer.cs
generated
@@ -41,8 +41,6 @@ namespace OpenNest.Forms
|
||||
lblGrooveDepth = new System.Windows.Forms.Label();
|
||||
nudSpikeAngle = new System.Windows.Forms.NumericUpDown();
|
||||
lblSpikeAngle = new System.Windows.Forms.Label();
|
||||
nudSpikeDepth = new System.Windows.Forms.NumericUpDown();
|
||||
lblSpikeDepth = new System.Windows.Forms.Label();
|
||||
grpTabParams = new System.Windows.Forms.GroupBox();
|
||||
nudTabCount = new System.Windows.Forms.NumericUpDown();
|
||||
lblTabCount = new System.Windows.Forms.Label();
|
||||
@@ -85,7 +83,6 @@ namespace OpenNest.Forms
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeWeldGap).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudGrooveDepth).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeAngle).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeDepth).BeginInit();
|
||||
grpTabParams.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabCount).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabHeight).BeginInit();
|
||||
@@ -162,12 +159,10 @@ namespace OpenNest.Forms
|
||||
grpSpikeParams.Controls.Add(lblGrooveDepth);
|
||||
grpSpikeParams.Controls.Add(nudSpikeAngle);
|
||||
grpSpikeParams.Controls.Add(lblSpikeAngle);
|
||||
grpSpikeParams.Controls.Add(nudSpikeDepth);
|
||||
grpSpikeParams.Controls.Add(lblSpikeDepth);
|
||||
grpSpikeParams.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
grpSpikeParams.Location = new System.Drawing.Point(6, 511);
|
||||
grpSpikeParams.Name = "grpSpikeParams";
|
||||
grpSpikeParams.Size = new System.Drawing.Size(191, 159);
|
||||
grpSpikeParams.Size = new System.Drawing.Size(191, 132);
|
||||
grpSpikeParams.TabIndex = 5;
|
||||
grpSpikeParams.TabStop = false;
|
||||
grpSpikeParams.Text = "Spike Parameters";
|
||||
@@ -175,7 +170,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// nudSpikePairCount
|
||||
//
|
||||
nudSpikePairCount.Location = new System.Drawing.Point(110, 128);
|
||||
nudSpikePairCount.Location = new System.Drawing.Point(110, 101);
|
||||
nudSpikePairCount.Maximum = new decimal(new int[] { 50, 0, 0, 0 });
|
||||
nudSpikePairCount.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
nudSpikePairCount.Name = "nudSpikePairCount";
|
||||
@@ -187,7 +182,7 @@ namespace OpenNest.Forms
|
||||
// lblSpikePairCount
|
||||
//
|
||||
lblSpikePairCount.AutoSize = true;
|
||||
lblSpikePairCount.Location = new System.Drawing.Point(10, 130);
|
||||
lblSpikePairCount.Location = new System.Drawing.Point(10, 103);
|
||||
lblSpikePairCount.Name = "lblSpikePairCount";
|
||||
lblSpikePairCount.Size = new System.Drawing.Size(66, 15);
|
||||
lblSpikePairCount.TabIndex = 5;
|
||||
@@ -196,7 +191,7 @@ namespace OpenNest.Forms
|
||||
// nudSpikeWeldGap
|
||||
//
|
||||
nudSpikeWeldGap.DecimalPlaces = 3;
|
||||
nudSpikeWeldGap.Location = new System.Drawing.Point(110, 74);
|
||||
nudSpikeWeldGap.Location = new System.Drawing.Point(110, 47);
|
||||
nudSpikeWeldGap.Maximum = new decimal(new int[] { 10, 0, 0, 0 });
|
||||
nudSpikeWeldGap.Name = "nudSpikeWeldGap";
|
||||
nudSpikeWeldGap.Size = new System.Drawing.Size(88, 23);
|
||||
@@ -207,7 +202,7 @@ namespace OpenNest.Forms
|
||||
// lblSpikeWeldGap
|
||||
//
|
||||
lblSpikeWeldGap.AutoSize = true;
|
||||
lblSpikeWeldGap.Location = new System.Drawing.Point(10, 76);
|
||||
lblSpikeWeldGap.Location = new System.Drawing.Point(10, 49);
|
||||
lblSpikeWeldGap.Name = "lblSpikeWeldGap";
|
||||
lblSpikeWeldGap.Size = new System.Drawing.Size(61, 15);
|
||||
lblSpikeWeldGap.TabIndex = 6;
|
||||
@@ -216,18 +211,18 @@ namespace OpenNest.Forms
|
||||
// nudGrooveDepth
|
||||
//
|
||||
nudGrooveDepth.DecimalPlaces = 3;
|
||||
nudGrooveDepth.Location = new System.Drawing.Point(110, 47);
|
||||
nudGrooveDepth.Location = new System.Drawing.Point(110, 20);
|
||||
nudGrooveDepth.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
nudGrooveDepth.Name = "nudGrooveDepth";
|
||||
nudGrooveDepth.Size = new System.Drawing.Size(88, 23);
|
||||
nudGrooveDepth.TabIndex = 1;
|
||||
nudGrooveDepth.Value = new decimal(new int[] { 125, 0, 0, 196608 });
|
||||
nudGrooveDepth.Value = new decimal(new int[] { 625, 0, 0, 196608 });
|
||||
nudGrooveDepth.ValueChanged += OnSpikeParamChanged;
|
||||
//
|
||||
// lblGrooveDepth
|
||||
//
|
||||
lblGrooveDepth.AutoSize = true;
|
||||
lblGrooveDepth.Location = new System.Drawing.Point(10, 49);
|
||||
lblGrooveDepth.Location = new System.Drawing.Point(10, 22);
|
||||
lblGrooveDepth.Name = "lblGrooveDepth";
|
||||
lblGrooveDepth.Size = new System.Drawing.Size(83, 15);
|
||||
lblGrooveDepth.TabIndex = 7;
|
||||
@@ -236,7 +231,7 @@ namespace OpenNest.Forms
|
||||
// nudSpikeAngle
|
||||
//
|
||||
nudSpikeAngle.DecimalPlaces = 1;
|
||||
nudSpikeAngle.Location = new System.Drawing.Point(110, 101);
|
||||
nudSpikeAngle.Location = new System.Drawing.Point(110, 74);
|
||||
nudSpikeAngle.Maximum = new decimal(new int[] { 89, 0, 0, 0 });
|
||||
nudSpikeAngle.Minimum = new decimal(new int[] { 10, 0, 0, 0 });
|
||||
nudSpikeAngle.Name = "nudSpikeAngle";
|
||||
@@ -247,32 +242,12 @@ namespace OpenNest.Forms
|
||||
// lblSpikeAngle
|
||||
//
|
||||
lblSpikeAngle.AutoSize = true;
|
||||
lblSpikeAngle.Location = new System.Drawing.Point(10, 103);
|
||||
lblSpikeAngle.Location = new System.Drawing.Point(10, 76);
|
||||
lblSpikeAngle.Name = "lblSpikeAngle";
|
||||
lblSpikeAngle.Size = new System.Drawing.Size(72, 15);
|
||||
lblSpikeAngle.TabIndex = 8;
|
||||
lblSpikeAngle.Text = "Spike Angle:";
|
||||
//
|
||||
// nudSpikeDepth
|
||||
//
|
||||
nudSpikeDepth.DecimalPlaces = 2;
|
||||
nudSpikeDepth.Enabled = false;
|
||||
nudSpikeDepth.Location = new System.Drawing.Point(110, 20);
|
||||
nudSpikeDepth.Minimum = new decimal(new int[] { 1, 0, 0, 131072 });
|
||||
nudSpikeDepth.Name = "nudSpikeDepth";
|
||||
nudSpikeDepth.Size = new System.Drawing.Size(88, 23);
|
||||
nudSpikeDepth.TabIndex = 0;
|
||||
nudSpikeDepth.Value = new decimal(new int[] { 25, 0, 0, 131072 });
|
||||
//
|
||||
// lblSpikeDepth
|
||||
//
|
||||
lblSpikeDepth.AutoSize = true;
|
||||
lblSpikeDepth.Location = new System.Drawing.Point(10, 22);
|
||||
lblSpikeDepth.Name = "lblSpikeDepth";
|
||||
lblSpikeDepth.Size = new System.Drawing.Size(73, 15);
|
||||
lblSpikeDepth.TabIndex = 9;
|
||||
lblSpikeDepth.Text = "Spike Depth:";
|
||||
//
|
||||
// grpTabParams
|
||||
//
|
||||
grpTabParams.Controls.Add(nudTabCount);
|
||||
@@ -674,7 +649,6 @@ namespace OpenNest.Forms
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeWeldGap).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudGrooveDepth).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeAngle).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudSpikeDepth).EndInit();
|
||||
grpTabParams.ResumeLayout(false);
|
||||
grpTabParams.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)nudTabCount).EndInit();
|
||||
@@ -747,8 +721,6 @@ namespace OpenNest.Forms
|
||||
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;
|
||||
|
||||
@@ -141,16 +141,8 @@ public partial class SplitDrawingForm : Form
|
||||
pnlPreview.Invalidate();
|
||||
}
|
||||
|
||||
private void UpdateSpikeDepth()
|
||||
{
|
||||
var grooveDepth = (double)nudGrooveDepth.Value;
|
||||
var weldGap = (double)nudSpikeWeldGap.Value;
|
||||
nudSpikeDepth.Value = (decimal)(grooveDepth + weldGap);
|
||||
}
|
||||
|
||||
private void OnSpikeParamChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateSpikeDepth();
|
||||
if (radFitToPlate.Checked)
|
||||
RecalculateAutoSplitLines();
|
||||
pnlPreview.Invalidate();
|
||||
@@ -175,9 +167,9 @@ public partial class SplitDrawingForm : Form
|
||||
else if (radSpike.Checked)
|
||||
{
|
||||
p.Type = SplitType.SpikeGroove;
|
||||
p.SpikeDepth = (double)nudSpikeDepth.Value;
|
||||
p.GrooveDepth = p.SpikeDepth + (double)nudGrooveDepth.Value;
|
||||
p.GrooveDepth = (double)nudGrooveDepth.Value;
|
||||
p.SpikeWeldGap = (double)nudSpikeWeldGap.Value;
|
||||
p.SpikeDepth = p.GrooveDepth + p.SpikeWeldGap;
|
||||
p.SpikeAngle = (double)nudSpikeAngle.Value;
|
||||
p.SpikePairCount = (int)nudSpikePairCount.Value;
|
||||
}
|
||||
@@ -389,23 +381,72 @@ public partial class SplitDrawingForm : Form
|
||||
System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y));
|
||||
}
|
||||
|
||||
// Split lines
|
||||
// Split lines — trimmed at feature positions with feature contours
|
||||
var parameters = GetCurrentParameters();
|
||||
var feature = GetSplitFeature(parameters.Type);
|
||||
using var splitPen = new Pen(Color.FromArgb(255, 82, 82));
|
||||
splitPen.DashStyle = DashStyle.Dash;
|
||||
using var featurePen = new Pen(Color.FromArgb(200, 255, 82, 82), 1.5f);
|
||||
|
||||
foreach (var sl in _splitLines)
|
||||
{
|
||||
PointF p1, p2;
|
||||
if (sl.Axis == CutOffAxis.Vertical)
|
||||
GetExtent(sl, out var extStart, out var extEnd);
|
||||
var isVert = sl.Axis == CutOffAxis.Vertical;
|
||||
var margin = 10.0;
|
||||
|
||||
if (sl.FeaturePositions.Count == 0 || radStraight.Checked)
|
||||
{
|
||||
p1 = pnlPreview.PointWorldToGraph(sl.Position, _drawingBounds.Bottom - 10);
|
||||
p2 = pnlPreview.PointWorldToGraph(sl.Position, _drawingBounds.Top + 10);
|
||||
// No features — draw one continuous line
|
||||
var p1 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, extStart - margin)
|
||||
: pnlPreview.PointWorldToGraph(extStart - margin, sl.Position);
|
||||
var p2 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, extEnd + margin)
|
||||
: pnlPreview.PointWorldToGraph(extEnd + margin, sl.Position);
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
else
|
||||
{
|
||||
p1 = pnlPreview.PointWorldToGraph(_drawingBounds.Left - 10, sl.Position);
|
||||
p2 = pnlPreview.PointWorldToGraph(_drawingBounds.Right + 10, sl.Position);
|
||||
// Generate feature geometry and draw contours
|
||||
var featureResult = feature.GenerateFeatures(sl, extStart, extEnd, parameters);
|
||||
DrawFeatureEdge(g, featurePen, featureResult.NegativeSideEdge, isVert);
|
||||
DrawFeatureEdge(g, featurePen, featureResult.PositiveSideEdge, isVert);
|
||||
|
||||
// Draw split line in segments between features
|
||||
var halfExt = GetFeatureHalfExtent(parameters);
|
||||
var sorted = new List<double>(sl.FeaturePositions);
|
||||
sorted.Sort();
|
||||
|
||||
var cursor = extStart - margin;
|
||||
foreach (var fc in sorted)
|
||||
{
|
||||
var gapStart = fc - halfExt;
|
||||
if (gapStart > cursor)
|
||||
{
|
||||
var p1 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, cursor)
|
||||
: pnlPreview.PointWorldToGraph(cursor, sl.Position);
|
||||
var p2 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, gapStart)
|
||||
: pnlPreview.PointWorldToGraph(gapStart, sl.Position);
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
cursor = fc + halfExt;
|
||||
}
|
||||
|
||||
// Final segment after last feature
|
||||
var end = extEnd + margin;
|
||||
if (end > cursor)
|
||||
{
|
||||
var p1 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, cursor)
|
||||
: pnlPreview.PointWorldToGraph(cursor, sl.Position);
|
||||
var p2 = isVert
|
||||
? pnlPreview.PointWorldToGraph(sl.Position, end)
|
||||
: pnlPreview.PointWorldToGraph(end, sl.Position);
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
}
|
||||
g.DrawLine(splitPen, p1, p2);
|
||||
}
|
||||
|
||||
// Feature position handles
|
||||
@@ -509,6 +550,41 @@ public partial class SplitDrawingForm : Form
|
||||
lblStatus.Text = $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces";
|
||||
}
|
||||
|
||||
// --- Feature rendering helpers ---
|
||||
|
||||
private static ISplitFeature GetSplitFeature(SplitType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
SplitType.WeldGapTabs => new WeldGapTabSplit(),
|
||||
SplitType.SpikeGroove => new SpikeGrooveSplit(),
|
||||
_ => new StraightSplit()
|
||||
};
|
||||
}
|
||||
|
||||
private static double GetFeatureHalfExtent(SplitParameters p)
|
||||
{
|
||||
return p.Type switch
|
||||
{
|
||||
SplitType.WeldGapTabs => p.TabWidth / 2,
|
||||
SplitType.SpikeGroove => p.GrooveDepth * System.Math.Tan(OpenNest.Math.Angle.ToRadians(p.SpikeAngle / 2)),
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawFeatureEdge(Graphics g, Pen pen, List<Geometry.Entity> entities, bool isVertical)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (entity is Geometry.Line line)
|
||||
{
|
||||
var p1 = pnlPreview.PointWorldToGraph(line.StartPoint.X, line.StartPoint.Y);
|
||||
var p2 = pnlPreview.PointWorldToGraph(line.EndPoint.X, line.EndPoint.Y);
|
||||
g.DrawLine(pen, p1, p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- SplitPreview control ---
|
||||
|
||||
private class SplitPreview : EntityView
|
||||
|
||||
Reference in New Issue
Block a user