Compare commits
17 Commits
e4e1d9b5a3
...
66b3aeafc1
| Author | SHA1 | Date | |
|---|---|---|---|
| 66b3aeafc1 | |||
| 616575e0ee | |||
| 2b4cb849ba | |||
| 3de44a7293 | |||
| b2ff704b73 | |||
| f7940efe93 | |||
| 6a2f39530f | |||
| 18023cb1cf | |||
| b4a740a515 | |||
| 441628eff2 | |||
| ac7d90ae17 | |||
| 459738e373 | |||
| b112f70f6a | |||
| f17db1d2f9 | |||
| 9b3cf10222 | |||
| e14382f2f3 | |||
| cb30c20eb9 |
@@ -0,0 +1,9 @@
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class AssignmentParameters
|
||||
{
|
||||
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||
public string Preference { get; set; } = "ILAT";
|
||||
public double MinGeometryLength { get; set; } = 0.01;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class ContourCuttingStrategy
|
||||
{
|
||||
public CuttingParameters Parameters { get; set; }
|
||||
|
||||
public Program Apply(Program partProgram, Plate plate)
|
||||
{
|
||||
var exitPoint = GetExitPoint(plate);
|
||||
var entities = partProgram.ToGeometry();
|
||||
var profile = new ShapeProfile(entities);
|
||||
|
||||
// Find closest point on perimeter from exit point
|
||||
var perimeterPoint = profile.Perimeter.ClosestPointTo(exitPoint, out var perimeterEntity);
|
||||
|
||||
// Chain cutouts by nearest-neighbor from perimeter point, then reverse
|
||||
// so farthest cutouts are cut first, nearest-to-perimeter cut last
|
||||
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
||||
orderedCutouts.Reverse();
|
||||
|
||||
// Build output program: cutouts first (farthest to nearest), perimeter last
|
||||
var result = new Program();
|
||||
var currentPoint = exitPoint;
|
||||
|
||||
foreach (var cutout in orderedCutouts)
|
||||
{
|
||||
var contourType = DetectContourType(cutout);
|
||||
var closestPt = cutout.ClosestPointTo(currentPoint, out var entity);
|
||||
var normal = ComputeNormal(closestPt, entity, contourType);
|
||||
var winding = DetermineWinding(cutout);
|
||||
|
||||
var leadIn = SelectLeadIn(contourType);
|
||||
var leadOut = SelectLeadOut(contourType);
|
||||
|
||||
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
|
||||
var reindexed = cutout.ReindexAt(closestPt, entity);
|
||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
|
||||
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
|
||||
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
|
||||
|
||||
currentPoint = closestPt;
|
||||
}
|
||||
|
||||
// Perimeter last
|
||||
{
|
||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
||||
var winding = DetermineWinding(profile.Perimeter);
|
||||
|
||||
var leadIn = SelectLeadIn(ContourType.External);
|
||||
var leadOut = SelectLeadOut(ContourType.External);
|
||||
|
||||
result.Codes.AddRange(leadIn.Generate(perimeterPt, normal, winding));
|
||||
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
|
||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
|
||||
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
|
||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Vector GetExitPoint(Plate plate)
|
||||
{
|
||||
var w = plate.Size.Width;
|
||||
var l = plate.Size.Length;
|
||||
|
||||
return plate.Quadrant switch
|
||||
{
|
||||
1 => new Vector(w, l), // Q1 origin BottomLeft -> exit TopRight
|
||||
2 => new Vector(0, l), // Q2 origin BottomRight -> exit TopLeft
|
||||
3 => new Vector(0, 0), // Q3 origin TopRight -> exit BottomLeft
|
||||
4 => new Vector(w, 0), // Q4 origin TopLeft -> exit BottomRight
|
||||
_ => new Vector(w, l)
|
||||
};
|
||||
}
|
||||
|
||||
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
||||
{
|
||||
var remaining = new List<Shape>(cutouts);
|
||||
var ordered = new List<Shape>();
|
||||
var currentPoint = startPoint;
|
||||
|
||||
while (remaining.Count > 0)
|
||||
{
|
||||
var nearest = remaining[0];
|
||||
var nearestPt = nearest.ClosestPointTo(currentPoint);
|
||||
var nearestDist = nearestPt.DistanceTo(currentPoint);
|
||||
|
||||
for (var i = 1; i < remaining.Count; i++)
|
||||
{
|
||||
var pt = remaining[i].ClosestPointTo(currentPoint);
|
||||
var dist = pt.DistanceTo(currentPoint);
|
||||
if (dist < nearestDist)
|
||||
{
|
||||
nearest = remaining[i];
|
||||
nearestPt = pt;
|
||||
nearestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
ordered.Add(nearest);
|
||||
remaining.Remove(nearest);
|
||||
currentPoint = nearestPt;
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private ContourType DetectContourType(Shape cutout)
|
||||
{
|
||||
if (cutout.Entities.Count == 1 && cutout.Entities[0] is Circle)
|
||||
return ContourType.ArcCircle;
|
||||
|
||||
return ContourType.Internal;
|
||||
}
|
||||
|
||||
private double ComputeNormal(Vector point, Entity entity, ContourType contourType)
|
||||
{
|
||||
double normal;
|
||||
|
||||
if (entity is Line line)
|
||||
{
|
||||
// Perpendicular to line direction
|
||||
var tangent = line.EndPoint.AngleFrom(line.StartPoint);
|
||||
normal = tangent + Math.Angle.HalfPI;
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
// Radial direction from center to point
|
||||
normal = point.AngleFrom(arc.Center);
|
||||
}
|
||||
else if (entity is Circle circle)
|
||||
{
|
||||
normal = point.AngleFrom(circle.Center);
|
||||
}
|
||||
else
|
||||
{
|
||||
normal = 0;
|
||||
}
|
||||
|
||||
// For internal contours, flip the normal (point into scrap)
|
||||
if (contourType == ContourType.Internal || contourType == ContourType.ArcCircle)
|
||||
normal += System.Math.PI;
|
||||
|
||||
return Math.Angle.NormalizeRad(normal);
|
||||
}
|
||||
|
||||
private RotationType DetermineWinding(Shape shape)
|
||||
{
|
||||
// Use signed area: positive = CCW, negative = CW
|
||||
var area = shape.Area();
|
||||
return area >= 0 ? RotationType.CCW : RotationType.CW;
|
||||
}
|
||||
|
||||
private LeadIn SelectLeadIn(ContourType contourType)
|
||||
{
|
||||
return contourType switch
|
||||
{
|
||||
ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn,
|
||||
ContourType.Internal => Parameters.InternalLeadIn,
|
||||
_ => Parameters.ExternalLeadIn
|
||||
};
|
||||
}
|
||||
|
||||
private LeadOut SelectLeadOut(ContourType contourType)
|
||||
{
|
||||
return contourType switch
|
||||
{
|
||||
ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut,
|
||||
ContourType.Internal => Parameters.InternalLeadOut,
|
||||
_ => Parameters.ExternalLeadOut
|
||||
};
|
||||
}
|
||||
|
||||
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
|
||||
{
|
||||
var moves = new List<ICode>();
|
||||
|
||||
foreach (var entity in shape.Entities)
|
||||
{
|
||||
if (entity is Line line)
|
||||
{
|
||||
moves.Add(new LinearMove(line.EndPoint));
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW));
|
||||
}
|
||||
else if (entity is Circle circle)
|
||||
{
|
||||
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new System.InvalidOperationException($"Unsupported entity type: {entity.Type}");
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public enum ContourType
|
||||
{
|
||||
External,
|
||||
Internal,
|
||||
ArcCircle
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class CuttingParameters
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string MachineName { get; set; }
|
||||
public string MaterialName { get; set; }
|
||||
public string Grade { get; set; }
|
||||
public double Thickness { get; set; }
|
||||
|
||||
public double Kerf { get; set; }
|
||||
public double PartSpacing { get; set; }
|
||||
|
||||
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
|
||||
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
|
||||
|
||||
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, ApproachAngle = 90 };
|
||||
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
|
||||
|
||||
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
||||
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
|
||||
|
||||
public Tab TabConfig { get; set; }
|
||||
public bool TabsEnabled { get; set; }
|
||||
|
||||
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
|
||||
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class ArcLeadIn : LeadIn
|
||||
{
|
||||
public double Radius { get; set; }
|
||||
|
||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||
|
||||
var arcCenter = new Vector(
|
||||
contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle),
|
||||
contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle));
|
||||
|
||||
return new List<ICode>
|
||||
{
|
||||
new RapidMove(piercePoint),
|
||||
new ArcMove(contourStartPoint, arcCenter, winding)
|
||||
};
|
||||
}
|
||||
|
||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||
{
|
||||
var arcCenterX = contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle);
|
||||
var arcCenterY = contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
|
||||
|
||||
return new Vector(
|
||||
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
|
||||
arcCenterY + Radius * System.Math.Sin(contourNormalAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class CleanHoleLeadIn : LeadIn
|
||||
{
|
||||
public double LineLength { get; set; }
|
||||
public double ArcRadius { get; set; }
|
||||
public double Kerf { get; set; }
|
||||
|
||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||
|
||||
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||
var arcCenter = new Vector(arcCenterX, arcCenterY);
|
||||
|
||||
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
|
||||
var arcStart = new Vector(
|
||||
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
||||
arcCenterY + ArcRadius * System.Math.Sin(lineAngle));
|
||||
|
||||
return new List<ICode>
|
||||
{
|
||||
new RapidMove(piercePoint),
|
||||
new LinearMove(arcStart),
|
||||
new ArcMove(contourStartPoint, arcCenter, winding)
|
||||
};
|
||||
}
|
||||
|
||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||
{
|
||||
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||
|
||||
var lineAngle = contourNormalAngle + Angle.ToRadians(135.0);
|
||||
var arcStartX = arcCenterX + ArcRadius * System.Math.Cos(lineAngle);
|
||||
var arcStartY = arcCenterY + ArcRadius * System.Math.Sin(lineAngle);
|
||||
|
||||
return new Vector(
|
||||
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public abstract class LeadIn
|
||||
{
|
||||
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW);
|
||||
|
||||
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class LineArcLeadIn : LeadIn
|
||||
{
|
||||
public double LineLength { get; set; }
|
||||
public double ApproachAngle { get; set; } = 135.0;
|
||||
public double ArcRadius { get; set; }
|
||||
|
||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||
|
||||
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||
var arcCenter = new Vector(arcCenterX, arcCenterY);
|
||||
|
||||
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||
var arcStart = new Vector(
|
||||
arcCenterX + ArcRadius * System.Math.Cos(lineAngle),
|
||||
arcCenterY + ArcRadius * System.Math.Sin(lineAngle));
|
||||
|
||||
return new List<ICode>
|
||||
{
|
||||
new RapidMove(piercePoint),
|
||||
new LinearMove(arcStart),
|
||||
new ArcMove(contourStartPoint, arcCenter, winding)
|
||||
};
|
||||
}
|
||||
|
||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||
{
|
||||
var arcCenterX = contourStartPoint.X + ArcRadius * System.Math.Cos(contourNormalAngle);
|
||||
var arcCenterY = contourStartPoint.Y + ArcRadius * System.Math.Sin(contourNormalAngle);
|
||||
|
||||
var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||
var arcStartX = arcCenterX + ArcRadius * System.Math.Cos(lineAngle);
|
||||
var arcStartY = arcCenterY + ArcRadius * System.Math.Sin(lineAngle);
|
||||
|
||||
return new Vector(
|
||||
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class LineLeadIn : LeadIn
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public double ApproachAngle { get; set; } = 90.0;
|
||||
|
||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||
|
||||
return new List<ICode>
|
||||
{
|
||||
new RapidMove(piercePoint),
|
||||
new LinearMove(contourStartPoint)
|
||||
};
|
||||
}
|
||||
|
||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||
{
|
||||
var approachAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||
return new Vector(
|
||||
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
|
||||
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class LineLineLeadIn : LeadIn
|
||||
{
|
||||
public double Length1 { get; set; }
|
||||
public double ApproachAngle1 { get; set; } = 90.0;
|
||||
public double Length2 { get; set; }
|
||||
public double ApproachAngle2 { get; set; } = 90.0;
|
||||
|
||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||
|
||||
var secondAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle1);
|
||||
var midPoint = new Vector(
|
||||
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
|
||||
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle));
|
||||
|
||||
return new List<ICode>
|
||||
{
|
||||
new RapidMove(piercePoint),
|
||||
new LinearMove(midPoint),
|
||||
new LinearMove(contourStartPoint)
|
||||
};
|
||||
}
|
||||
|
||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||
{
|
||||
var secondAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle1);
|
||||
var midX = contourStartPoint.X + Length2 * System.Math.Cos(secondAngle);
|
||||
var midY = contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle);
|
||||
|
||||
var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2);
|
||||
return new Vector(
|
||||
midX + Length1 * System.Math.Cos(firstAngle),
|
||||
midY + Length1 * System.Math.Sin(firstAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class NoLeadIn : LeadIn
|
||||
{
|
||||
public override List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
return new List<ICode>
|
||||
{
|
||||
new RapidMove(contourStartPoint)
|
||||
};
|
||||
}
|
||||
|
||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||
{
|
||||
return contourStartPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class ArcLeadOut : LeadOut
|
||||
{
|
||||
public double Radius { get; set; }
|
||||
|
||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle);
|
||||
var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle);
|
||||
var arcCenter = new Vector(arcCenterX, arcCenterY);
|
||||
|
||||
var endPoint = new Vector(
|
||||
arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2),
|
||||
arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2));
|
||||
|
||||
return new List<ICode>
|
||||
{
|
||||
new ArcMove(endPoint, arcCenter, winding)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public abstract class LeadOut
|
||||
{
|
||||
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class LineLeadOut : LeadOut
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public double ApproachAngle { get; set; } = 90.0;
|
||||
|
||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var overcutAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
||||
var endPoint = new Vector(
|
||||
contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
|
||||
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle));
|
||||
|
||||
return new List<ICode>
|
||||
{
|
||||
new LinearMove(endPoint)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class MicrotabLeadOut : LeadOut
|
||||
{
|
||||
public double GapSize { get; set; } = 0.03;
|
||||
|
||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
return new List<ICode>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class NoLeadOut : LeadOut
|
||||
{
|
||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
return new List<ICode>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
// Values match PEP Technology's numbering scheme (value 6 intentionally skipped)
|
||||
public enum SequenceMethod
|
||||
{
|
||||
RightSide = 1,
|
||||
LeastCode = 2,
|
||||
Advanced = 3,
|
||||
BottomSide = 4,
|
||||
EdgeStart = 5,
|
||||
LeftSide = 7,
|
||||
RightSideAlt = 8
|
||||
}
|
||||
|
||||
public class SequenceParameters
|
||||
{
|
||||
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||
public double SmallCutoutWidth { get; set; } = 1.5;
|
||||
public double SmallCutoutHeight { get; set; } = 1.5;
|
||||
public double MediumCutoutWidth { get; set; } = 8.0;
|
||||
public double MediumCutoutHeight { get; set; } = 8.0;
|
||||
public double DistanceMediumSmall { get; set; }
|
||||
public bool AlternateRowsColumns { get; set; } = true;
|
||||
public bool AlternateCutoutsWithinRowColumn { get; set; } = true;
|
||||
public double MinDistanceBetweenRowsColumns { get; set; } = 0.25;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class BreakerTab : Tab
|
||||
{
|
||||
public double BreakerDepth { get; set; }
|
||||
public double BreakerLeadInLength { get; set; }
|
||||
public double BreakerAngle { get; set; }
|
||||
|
||||
public override List<ICode> Generate(
|
||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var codes = new List<ICode>();
|
||||
|
||||
if (TabLeadOut != null)
|
||||
codes.AddRange(TabLeadOut.Generate(tabStartPoint, contourNormalAngle, winding));
|
||||
|
||||
var scoreAngle = contourNormalAngle + System.Math.PI;
|
||||
var scoreEnd = new Vector(
|
||||
tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle),
|
||||
tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle));
|
||||
codes.Add(new LinearMove(scoreEnd));
|
||||
codes.Add(new RapidMove(tabEndPoint));
|
||||
|
||||
if (TabLeadIn != null)
|
||||
codes.AddRange(TabLeadIn.Generate(tabEndPoint, contourNormalAngle, winding));
|
||||
|
||||
return codes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class MachineTab : Tab
|
||||
{
|
||||
public int MachineTabId { get; set; }
|
||||
|
||||
public override List<ICode> Generate(
|
||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
return new List<ICode>
|
||||
{
|
||||
new RapidMove(tabEndPoint)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public class NormalTab : Tab
|
||||
{
|
||||
public double CutoutMinWidth { get; set; }
|
||||
public double CutoutMinHeight { get; set; }
|
||||
public double CutoutMaxWidth { get; set; }
|
||||
public double CutoutMaxHeight { get; set; }
|
||||
|
||||
public override List<ICode> Generate(
|
||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW)
|
||||
{
|
||||
var codes = new List<ICode>();
|
||||
|
||||
if (TabLeadOut != null)
|
||||
codes.AddRange(TabLeadOut.Generate(tabStartPoint, contourNormalAngle, winding));
|
||||
|
||||
codes.Add(new RapidMove(tabEndPoint));
|
||||
|
||||
if (TabLeadIn != null)
|
||||
codes.AddRange(TabLeadIn.Generate(tabEndPoint, contourNormalAngle, winding));
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
public bool AppliesToCutout(double cutoutWidth, double cutoutHeight)
|
||||
{
|
||||
return cutoutWidth >= CutoutMinWidth && cutoutWidth <= CutoutMaxWidth
|
||||
&& cutoutHeight >= CutoutMinHeight && cutoutHeight <= CutoutMaxHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
public abstract class Tab
|
||||
{
|
||||
public double Size { get; set; } = 0.03;
|
||||
public LeadIn TabLeadIn { get; set; }
|
||||
public LeadOut TabLeadOut { get; set; }
|
||||
|
||||
public abstract List<ICode> Generate(
|
||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW);
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,28 @@ namespace OpenNest.Geometry
|
||||
Center.Y + Radius * System.Math.Sin(EndAngle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the arc at the given point, returning two sub-arcs.
|
||||
/// Either half may be null if the split point coincides with an endpoint.
|
||||
/// </summary>
|
||||
/// <param name="point">The point at which to split the arc.</param>
|
||||
/// <returns>A tuple of (first, second) sub-arcs.</returns>
|
||||
public (Arc first, Arc second) SplitAt(Vector point)
|
||||
{
|
||||
if (point.DistanceTo(StartPoint()) < Tolerance.Epsilon)
|
||||
return (null, new Arc(Center, Radius, StartAngle, EndAngle, IsReversed));
|
||||
|
||||
if (point.DistanceTo(EndPoint()) < Tolerance.Epsilon)
|
||||
return (new Arc(Center, Radius, StartAngle, EndAngle, IsReversed), null);
|
||||
|
||||
var splitAngle = Angle.NormalizeRad(Center.AngleTo(point));
|
||||
|
||||
var firstArc = new Arc(Center, Radius, StartAngle, splitAngle, IsReversed);
|
||||
var secondArc = new Arc(Center, Radius, splitAngle, EndAngle, IsReversed);
|
||||
|
||||
return (firstArc, secondArc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given arc has the same center point and radius as this.
|
||||
/// </summary>
|
||||
|
||||
@@ -414,6 +414,25 @@ namespace OpenNest.Geometry
|
||||
return OffsetEntity(distance, side);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the line at the given point, returning two sub-lines.
|
||||
/// Either half may be null if the split point coincides with an endpoint.
|
||||
/// </summary>
|
||||
/// <param name="point">The point at which to split the line.</param>
|
||||
/// <returns>A tuple of (first, second) sub-lines.</returns>
|
||||
public (Line first, Line second) SplitAt(Vector point)
|
||||
{
|
||||
var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon
|
||||
? null
|
||||
: new Line(StartPoint, point);
|
||||
|
||||
var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon
|
||||
? null
|
||||
: new Line(point, EndPoint);
|
||||
|
||||
return (first, second);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the closest point on the line to the given point.
|
||||
/// </summary>
|
||||
|
||||
@@ -200,6 +200,68 @@ namespace OpenNest.Geometry
|
||||
return closestPt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new shape with entities reordered so that the given point on
|
||||
/// the given entity becomes the new start point of the contour.
|
||||
/// </summary>
|
||||
/// <param name="point">The point on the entity to reindex at.</param>
|
||||
/// <param name="entity">The entity containing the point.</param>
|
||||
/// <returns>A new reindexed shape.</returns>
|
||||
public Shape ReindexAt(Vector point, Entity entity)
|
||||
{
|
||||
// Circle case: return a new shape with just the circle
|
||||
if (entity is Circle)
|
||||
{
|
||||
var result = new Shape();
|
||||
result.Entities.Add(entity);
|
||||
return result;
|
||||
}
|
||||
|
||||
var i = Entities.IndexOf(entity);
|
||||
if (i < 0)
|
||||
throw new ArgumentException("Entity not found in shape", nameof(entity));
|
||||
|
||||
// Split the entity at the point
|
||||
Entity firstHalf = null;
|
||||
Entity secondHalf = null;
|
||||
|
||||
if (entity is Line line)
|
||||
{
|
||||
var (f, s) = line.SplitAt(point);
|
||||
firstHalf = f;
|
||||
secondHalf = s;
|
||||
}
|
||||
else if (entity is Arc arc)
|
||||
{
|
||||
var (f, s) = arc.SplitAt(point);
|
||||
firstHalf = f;
|
||||
secondHalf = s;
|
||||
}
|
||||
|
||||
// Build reindexed entity list
|
||||
var entities = new List<Entity>();
|
||||
|
||||
// secondHalf of split entity (if not null)
|
||||
if (secondHalf != null)
|
||||
entities.Add(secondHalf);
|
||||
|
||||
// Entities after the split index (wrapping)
|
||||
for (var j = i + 1; j < Entities.Count; j++)
|
||||
entities.Add(Entities[j]);
|
||||
|
||||
// Entities before the split index (wrapping)
|
||||
for (var j = 0; j < i; j++)
|
||||
entities.Add(Entities[j]);
|
||||
|
||||
// firstHalf of split entity (if not null)
|
||||
if (firstHalf != null)
|
||||
entities.Add(firstHalf);
|
||||
|
||||
var reindexed = new Shape();
|
||||
reindexed.Entities.AddRange(entities);
|
||||
return reindexed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the shape to a polygon.
|
||||
/// </summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
# Contour Re-Indexing Design
|
||||
|
||||
## Overview
|
||||
|
||||
Add entity-splitting primitives and a `Shape.ReindexAt` method so that a closed contour can be reordered to start (and end) at an arbitrary point. Then wire this into `ContourCuttingStrategy.Apply()` to replace the `NotImplementedException` stubs.
|
||||
|
||||
All geometry additions live on existing classes in `OpenNest.Geometry`. The strategy wiring is a change to the existing `ContourCuttingStrategy` in `OpenNest.CNC.CuttingStrategy`.
|
||||
|
||||
## Entity Splitting Primitives
|
||||
|
||||
### Line.SplitAt(Vector point)
|
||||
|
||||
```csharp
|
||||
public (Line first, Line second) SplitAt(Vector point)
|
||||
```
|
||||
|
||||
- Returns two lines: `StartPoint → point` and `point → EndPoint`.
|
||||
- If the point is at `StartPoint` (within `Tolerance.Epsilon` distance), `first` is null.
|
||||
- If the point is at `EndPoint` (within `Tolerance.Epsilon` distance), `second` is null.
|
||||
- The point is assumed to lie on the line (caller is responsible — it comes from `ClosestPointTo`).
|
||||
|
||||
### Arc.SplitAt(Vector point)
|
||||
|
||||
```csharp
|
||||
public (Arc first, Arc second) SplitAt(Vector point)
|
||||
```
|
||||
|
||||
- Computes `splitAngle = Center.AngleTo(point)`, normalized via `Angle.NormalizeRad`.
|
||||
- First arc: same center, radius, direction — `StartAngle → splitAngle`.
|
||||
- Second arc: same center, radius, direction — `splitAngle → EndAngle`.
|
||||
- **Endpoint tolerance**: compare `point.DistanceTo(arc.StartPoint())` and `point.DistanceTo(arc.EndPoint())` rather than comparing angles directly. This avoids wrap-around issues at the 0/2π boundary.
|
||||
- If the point is at `StartPoint()` (within `Tolerance.Epsilon` distance), `first` is null.
|
||||
- If the point is at `EndPoint()` (within `Tolerance.Epsilon` distance), `second` is null.
|
||||
|
||||
### Circle — no conversion needed
|
||||
|
||||
Circles are kept as-is in `ReindexAt`. The `ConvertShapeToMoves` method handles circles directly by emitting an `ArcMove` from the start point back to itself (a full circle), matching the existing `ConvertGeometry.AddCircle` pattern. This avoids the problem of constructing a "full-sweep arc" where `StartAngle == EndAngle` would produce zero sweep.
|
||||
|
||||
## Shape.ReindexAt
|
||||
|
||||
```csharp
|
||||
public Shape ReindexAt(Vector point, Entity entity)
|
||||
```
|
||||
|
||||
- `point`: the start/end point for the reindexed contour (from `ClosestPointTo`).
|
||||
- `entity`: the entity containing `point` (from `ClosestPointTo`'s `out` parameter).
|
||||
- Returns a **new** Shape (does not modify the original). The new shape shares entity references with the original for unsplit entities — callers must not mutate either.
|
||||
- Throws `ArgumentException` if `entity` is not found in `Entities`.
|
||||
|
||||
### Algorithm
|
||||
|
||||
1. If `entity` is a `Circle`:
|
||||
- Return a new Shape with that single `Circle` entity and `point` stored for `ConvertShapeToMoves` to use as the start point.
|
||||
|
||||
2. Find the index `i` of `entity` in `Entities`. Throw `ArgumentException` if not found.
|
||||
|
||||
3. Split the entity at `point`:
|
||||
- `Line` → `line.SplitAt(point)` → `(firstHalf, secondHalf)`
|
||||
- `Arc` → `arc.SplitAt(point)` → `(firstHalf, secondHalf)`
|
||||
|
||||
4. Build the new entity list (skip null entries):
|
||||
- `secondHalf` (if not null)
|
||||
- `Entities[i+1]`, `Entities[i+2]`, ..., `Entities[count-1]` (after the split)
|
||||
- `Entities[0]`, `Entities[1]`, ..., `Entities[i-1]` (before the split, wrapping around)
|
||||
- `firstHalf` (if not null)
|
||||
|
||||
5. Return a new Shape with this entity list.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- **Point lands on entity boundary** (start/end of an entity): one half of the split is null. The reordering still works — it just starts from the next full entity.
|
||||
- **Single-entity shape that is an Arc**: split produces two arcs, reorder is just `[secondHalf, firstHalf]`.
|
||||
- **Single-entity Circle**: handled by step 1 — kept as Circle, converted to a full-circle ArcMove in `ConvertShapeToMoves`.
|
||||
|
||||
## Wiring into ContourCuttingStrategy
|
||||
|
||||
### Entity-to-ICode Conversion
|
||||
|
||||
Add a private method to `ContourCuttingStrategy`:
|
||||
|
||||
```csharp
|
||||
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
|
||||
```
|
||||
|
||||
The `startPoint` parameter is needed for the Circle case (to know where the full-circle ArcMove starts).
|
||||
|
||||
Iterates `shape.Entities` and converts each to cutting moves using **absolute coordinates** (consistent with `ConvertGeometry`):
|
||||
- `Line` → `LinearMove(line.EndPoint)`
|
||||
- `Arc` → `ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW)`
|
||||
- `Circle` → `ArcMove(startPoint, circle.Center, circle.Rotation)` — full circle from start point back to itself, matching `ConvertGeometry.AddCircle`
|
||||
- Any other entity type → throw `InvalidOperationException`
|
||||
|
||||
No `RapidMove` between entities — they are contiguous in a reindexed shape. The lead-in already positions the head at the shape's start point.
|
||||
|
||||
### Replace NotImplementedException
|
||||
|
||||
In `ContourCuttingStrategy.Apply()`, replace the two `throw new NotImplementedException(...)` blocks:
|
||||
|
||||
**Cutout loop** (uses `cutout` shape variable):
|
||||
```csharp
|
||||
var reindexed = cutout.ReindexAt(closestPt, entity);
|
||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
|
||||
```
|
||||
|
||||
**Perimeter block** (uses `profile.Perimeter`):
|
||||
```csharp
|
||||
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
|
||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
|
||||
```
|
||||
|
||||
The full sequence for each contour becomes:
|
||||
1. Lead-in codes (rapid to pierce point, cutting moves to contour start)
|
||||
2. Contour body (reindexed entity moves from `ConvertShapeToMoves`)
|
||||
3. Lead-out codes (overcut moves away from contour)
|
||||
|
||||
### MicrotabLeadOut Handling
|
||||
|
||||
When the lead-out is `MicrotabLeadOut`, the last cutting move must be trimmed by `GapSize`. This is a separate concern from re-indexing — stub it with a TODO comment for now. The trimming logic will shorten the last `LinearMove` or `ArcMove` in the contour body.
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `OpenNest.Core/Geometry/Line.cs` | Add `SplitAt(Vector)` method |
|
||||
| `OpenNest.Core/Geometry/Arc.cs` | Add `SplitAt(Vector)` method |
|
||||
| `OpenNest.Core/Geometry/Shape.cs` | Add `ReindexAt(Vector, Entity)` method |
|
||||
| `OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs` | Add `ConvertShapeToMoves`, replace `NotImplementedException` blocks |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- **MicrotabLeadOut trimming** (trim last move by gap size — stubbed with TODO)
|
||||
- **Tab insertion** (inserting tab codes mid-contour — already stubbed)
|
||||
- **Lead-in editor UI** (interactive start point selection — separate feature)
|
||||
- **Contour re-indexing for open shapes** (only closed contours supported)
|
||||
@@ -0,0 +1,420 @@
|
||||
# CNC Cutting Strategy Design
|
||||
|
||||
## Overview
|
||||
|
||||
Add lead-in, lead-out, and tab classes to `OpenNest.Core` that generate `ICode` instructions for CNC cutting approach/exit geometry. The strategy runs at nest-time — `ContourCuttingStrategy.Apply()` produces a new `Program` with lead-ins, lead-outs, start points, and contour ordering baked in. This modified program is what gets saved to the nest file and later fed to the post-processor for machine-specific G-code translation. The original `Drawing.Program` stays untouched; the strategy output lives on the `Part`.
|
||||
|
||||
All new code lives in `OpenNest.Core/CNC/CuttingStrategy/`.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
OpenNest.Core/CNC/CuttingStrategy/
|
||||
├── LeadIns/
|
||||
│ ├── LeadIn.cs
|
||||
│ ├── NoLeadIn.cs
|
||||
│ ├── LineLeadIn.cs
|
||||
│ ├── LineArcLeadIn.cs
|
||||
│ ├── ArcLeadIn.cs
|
||||
│ ├── LineLineLeadIn.cs
|
||||
│ └── CleanHoleLeadIn.cs
|
||||
├── LeadOuts/
|
||||
│ ├── LeadOut.cs
|
||||
│ ├── NoLeadOut.cs
|
||||
│ ├── LineLeadOut.cs
|
||||
│ ├── ArcLeadOut.cs
|
||||
│ └── MicrotabLeadOut.cs
|
||||
├── Tabs/
|
||||
│ ├── Tab.cs
|
||||
│ ├── NormalTab.cs
|
||||
│ ├── BreakerTab.cs
|
||||
│ └── MachineTab.cs
|
||||
├── ContourType.cs
|
||||
├── CuttingParameters.cs
|
||||
├── ContourCuttingStrategy.cs
|
||||
├── SequenceParameters.cs
|
||||
└── AssignmentParameters.cs
|
||||
```
|
||||
|
||||
## Namespace
|
||||
|
||||
All classes use `namespace OpenNest.CNC.CuttingStrategy`.
|
||||
|
||||
## Type Mappings from Original Spec
|
||||
|
||||
The original spec used placeholder names. These are the correct codebase types:
|
||||
|
||||
| Spec type | Actual type | Notes |
|
||||
|-----------|------------|-------|
|
||||
| `PointD` | `Vector` | `OpenNest.Geometry.Vector` — struct with `X`, `Y` fields |
|
||||
| `CircularMove` | `ArcMove` | Constructor: `ArcMove(Vector endPoint, Vector centerPoint, RotationType rotation)` |
|
||||
| `CircularDirection` | `RotationType` | Enum with `CW`, `CCW` |
|
||||
| `value.ToRadians()` | `Angle.ToRadians(value)` | Static method on `OpenNest.Math.Angle` |
|
||||
| `new Program(codes)` | Build manually | Create `Program()`, add to `.Codes` list |
|
||||
|
||||
## LeadIn Hierarchy
|
||||
|
||||
### Abstract Base: `LeadIn`
|
||||
|
||||
```csharp
|
||||
public abstract class LeadIn
|
||||
{
|
||||
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW);
|
||||
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
||||
}
|
||||
```
|
||||
|
||||
- `contourStartPoint`: where the contour cut begins (first point of the part profile).
|
||||
- `contourNormalAngle`: normal angle (radians) at the contour start point, pointing **away from the part material** (outward from perimeter, into scrap for cutouts).
|
||||
- `winding`: contour winding direction — arc-based lead-ins use this for their `ArcMove` rotation.
|
||||
- `Generate` returns ICode instructions starting with a `RapidMove` to the pierce point, followed by cutting moves to reach the contour start.
|
||||
- `GetPiercePoint` computes where the head rapids to before firing — useful for visualization and collision detection.
|
||||
|
||||
### NoLeadIn (Type 0)
|
||||
|
||||
Pierce directly on the contour start point. Returns a single `RapidMove(contourStartPoint)`.
|
||||
|
||||
### LineLeadIn (Type 1)
|
||||
|
||||
Straight line approach.
|
||||
|
||||
Properties:
|
||||
- `Length` (double): distance from pierce point to contour start (inches)
|
||||
- `ApproachAngle` (double): approach angle in degrees relative to contour tangent. 90 = perpendicular, 135 = acute angle (common for plasma). Default: 90.
|
||||
|
||||
Pierce point offset: `contourStartPoint + Length` along `contourNormalAngle + Angle.ToRadians(ApproachAngle)`.
|
||||
|
||||
Generates: `RapidMove(piercePoint)` → `LinearMove(contourStartPoint)`.
|
||||
|
||||
> **Note:** Properties are named `ApproachAngle` (not `Angle`) to avoid shadowing the `OpenNest.Math.Angle` static class. This applies to all lead-in/lead-out/tab classes.
|
||||
|
||||
### LineArcLeadIn (Type 2)
|
||||
|
||||
Line followed by tangential arc meeting the contour. Most common for plasma.
|
||||
|
||||
Properties:
|
||||
- `LineLength` (double): straight approach segment length
|
||||
- `ApproachAngle` (double): line angle relative to contour. Default: 135.
|
||||
- `ArcRadius` (double): radius of tangential arc
|
||||
|
||||
Geometry: Pierce → [Line] → Arc start → [Arc] → Contour start. Arc center is at `contourStartPoint + ArcRadius` along normal. Arc rotation direction matches contour winding (CW for CW contours, CCW for CCW).
|
||||
|
||||
Generates: `RapidMove(piercePoint)` → `LinearMove(arcStart)` → `ArcMove(contourStartPoint, arcCenter, rotation)`.
|
||||
|
||||
### ArcLeadIn (Type 3)
|
||||
|
||||
Pure arc approach, no straight line segment.
|
||||
|
||||
Properties:
|
||||
- `Radius` (double): arc radius
|
||||
|
||||
Pierce point is diametrically opposite the contour start on the arc circle. Arc center at `contourStartPoint + Radius` along normal.
|
||||
|
||||
Arc rotation direction matches contour winding.
|
||||
|
||||
Generates: `RapidMove(piercePoint)` → `ArcMove(contourStartPoint, arcCenter, rotation)`.
|
||||
|
||||
### LineLineLeadIn (Type 5)
|
||||
|
||||
Two-segment straight line approach.
|
||||
|
||||
Properties:
|
||||
- `Length1` (double): first segment length
|
||||
- `ApproachAngle1` (double): first segment angle. Default: 90.
|
||||
- `Length2` (double): second segment length
|
||||
- `ApproachAngle2` (double): direction change. Default: 90.
|
||||
|
||||
Generates: `RapidMove(piercePoint)` → `LinearMove(midPoint)` → `LinearMove(contourStartPoint)`.
|
||||
|
||||
### CleanHoleLeadIn
|
||||
|
||||
Specialized for precision circular holes. Same geometry as `LineArcLeadIn` but with hard-coded 135° angle and a `Kerf` property. The overcut (cutting past start to close the hole) is handled at the lead-out, not here.
|
||||
|
||||
Properties:
|
||||
- `LineLength` (double)
|
||||
- `ArcRadius` (double)
|
||||
- `Kerf` (double)
|
||||
|
||||
## LeadOut Hierarchy
|
||||
|
||||
### Abstract Base: `LeadOut`
|
||||
|
||||
```csharp
|
||||
public abstract class LeadOut
|
||||
{
|
||||
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||
RotationType winding = RotationType.CW);
|
||||
}
|
||||
```
|
||||
|
||||
- `contourEndPoint`: where the contour cut ends. For closed contours, same as start.
|
||||
- Returns ICode instructions appended after the contour's last cut point.
|
||||
|
||||
### NoLeadOut (Type 0)
|
||||
|
||||
Returns empty list. Cut ends exactly at contour end.
|
||||
|
||||
### LineLeadOut (Type 1)
|
||||
|
||||
Straight line overcut past contour end.
|
||||
|
||||
Properties:
|
||||
- `Length` (double): overcut distance
|
||||
- `ApproachAngle` (double): direction relative to contour tangent. Default: 90.
|
||||
|
||||
Generates: `LinearMove(endPoint)` where endPoint is offset from contourEndPoint.
|
||||
|
||||
### ArcLeadOut (Type 3)
|
||||
|
||||
Arc overcut curving away from the part.
|
||||
|
||||
Properties:
|
||||
- `Radius` (double)
|
||||
|
||||
Arc center at `contourEndPoint + Radius` along normal. End point is a quarter turn away. Arc rotation direction matches contour winding.
|
||||
|
||||
Generates: `ArcMove(endPoint, arcCenter, rotation)`.
|
||||
|
||||
### MicrotabLeadOut (Type 4)
|
||||
|
||||
Stops short of contour end, leaving an uncut bridge. Laser only.
|
||||
|
||||
Properties:
|
||||
- `GapSize` (double): uncut material length. Default: 0.03".
|
||||
|
||||
Does NOT add instructions — returns empty list. The `ContourCuttingStrategy` detects this type and trims the last cutting move by `GapSize` instead.
|
||||
|
||||
## Tab Hierarchy
|
||||
|
||||
Tabs are mid-contour features that temporarily lift the beam to leave bridges holding the part in place.
|
||||
|
||||
### Abstract Base: `Tab`
|
||||
|
||||
```csharp
|
||||
public abstract class Tab
|
||||
{
|
||||
public double Size { get; set; } = 0.03;
|
||||
public LeadIn TabLeadIn { get; set; }
|
||||
public LeadOut TabLeadOut { get; set; }
|
||||
|
||||
public abstract List<ICode> Generate(
|
||||
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle);
|
||||
}
|
||||
```
|
||||
|
||||
### NormalTab
|
||||
|
||||
Standard tab: cut up to tab start, lift/rapid over gap, resume cutting.
|
||||
|
||||
Additional properties:
|
||||
- `CutoutMinWidth`, `CutoutMinHeight` (double): minimum cutout size to receive this tab
|
||||
- `CutoutMaxWidth`, `CutoutMaxHeight` (double): maximum cutout size to receive this tab
|
||||
- `AppliesToCutout(double width, double height)` method for size filtering
|
||||
|
||||
Generates: TabLeadOut codes → `RapidMove(tabEndPoint)` → TabLeadIn codes.
|
||||
|
||||
### BreakerTab
|
||||
|
||||
Like NormalTab but adds a scoring cut into the part at the tab location to make snapping easier.
|
||||
|
||||
Additional properties:
|
||||
- `BreakerDepth` (double): how far the score cuts into the part
|
||||
- `BreakerLeadInLength` (double)
|
||||
- `BreakerAngle` (double)
|
||||
|
||||
Generates: TabLeadOut codes → `LinearMove(scoreEnd)` → `RapidMove(tabEndPoint)` → TabLeadIn codes.
|
||||
|
||||
### MachineTab
|
||||
|
||||
Tab behavior configured at the CNC controller level. OpenNest just signals the controller.
|
||||
|
||||
Additional properties:
|
||||
- `MachineTabId` (int): passed to post-processor for M-code translation
|
||||
|
||||
Returns a placeholder `RapidMove(tabEndPoint)` — the post-processor plugin replaces this with machine-specific commands.
|
||||
|
||||
## CuttingParameters
|
||||
|
||||
One instance per material/machine combination. Ties everything together.
|
||||
|
||||
```csharp
|
||||
public class CuttingParameters
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
// Material/Machine identification
|
||||
public string MachineName { get; set; }
|
||||
public string MaterialName { get; set; }
|
||||
public string Grade { get; set; }
|
||||
public double Thickness { get; set; }
|
||||
|
||||
// Kerf and spacing
|
||||
public double Kerf { get; set; }
|
||||
public double PartSpacing { get; set; }
|
||||
|
||||
// External contour lead-in/out
|
||||
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
|
||||
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
|
||||
|
||||
// Internal contour lead-in/out
|
||||
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, Angle = 90 };
|
||||
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
|
||||
|
||||
// Arc/circle specific (overrides internal for circular features)
|
||||
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
||||
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
|
||||
|
||||
// Tab configuration
|
||||
public Tab TabConfig { get; set; }
|
||||
public bool TabsEnabled { get; set; } = false;
|
||||
|
||||
// Sequencing and assignment
|
||||
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
|
||||
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
|
||||
}
|
||||
```
|
||||
|
||||
## SequenceParameters and AssignmentParameters
|
||||
|
||||
```csharp
|
||||
// Values match PEP Technology's numbering scheme (value 6 intentionally skipped)
|
||||
public enum SequenceMethod
|
||||
{
|
||||
RightSide = 1, LeastCode = 2, Advanced = 3,
|
||||
BottomSide = 4, EdgeStart = 5, LeftSide = 7, RightSideAlt = 8
|
||||
}
|
||||
|
||||
public class SequenceParameters
|
||||
{
|
||||
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||
public double SmallCutoutWidth { get; set; } = 1.5;
|
||||
public double SmallCutoutHeight { get; set; } = 1.5;
|
||||
public double MediumCutoutWidth { get; set; } = 8.0;
|
||||
public double MediumCutoutHeight { get; set; } = 8.0;
|
||||
public double DistanceMediumSmall { get; set; }
|
||||
public bool AlternateRowsColumns { get; set; } = true;
|
||||
public bool AlternateCutoutsWithinRowColumn { get; set; } = true;
|
||||
public double MinDistanceBetweenRowsColumns { get; set; } = 0.25;
|
||||
}
|
||||
|
||||
public class AssignmentParameters
|
||||
{
|
||||
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
|
||||
public string Preference { get; set; } = "ILAT";
|
||||
public double MinGeometryLength { get; set; } = 0.01;
|
||||
}
|
||||
```
|
||||
|
||||
## ContourCuttingStrategy
|
||||
|
||||
The orchestrator. Uses `ShapeProfile` to decompose a part into perimeter + cutouts, then sequences and applies cutting parameters using nearest-neighbor chaining from an exit point.
|
||||
|
||||
### Exit Point from Plate Quadrant
|
||||
|
||||
The exit point is the **opposite corner** of the plate from the quadrant origin. This is where the head ends up after traversing the plate, and is the starting point for backwards nearest-neighbor sequencing.
|
||||
|
||||
| Quadrant | Origin | Exit Point |
|
||||
|----------|--------|------------|
|
||||
| 1 | TopRight | BottomLeft (0, 0) |
|
||||
| 2 | TopLeft | BottomRight (width, 0) |
|
||||
| 3 | BottomLeft | TopRight (width, length) |
|
||||
| 4 | BottomRight | TopLeft (0, length) |
|
||||
|
||||
The exit point is derived from `Plate.Quadrant` and `Plate.Size` — not passed in manually.
|
||||
|
||||
### Approach
|
||||
|
||||
Instead of requiring `Program.GetStartPoint()` / `GetNormalAtStart()` (which don't exist), the strategy:
|
||||
|
||||
1. Computes the **exit point** from the plate's quadrant and size
|
||||
2. Converts the program to geometry via `Program.ToGeometry()`
|
||||
3. Builds a `ShapeProfile` from the geometry — gives `Perimeter` (Shape) and `Cutouts` (List<Shape>)
|
||||
4. Uses `Shape.ClosestPointTo(point, out Entity entity)` to find lead-in points and the entity for normal computation
|
||||
5. Chains cutouts by nearest-neighbor distance from the perimeter closest point
|
||||
6. Reverses the chain → cut order is cutouts first (nearest-last), perimeter last
|
||||
|
||||
### Contour Re-Indexing
|
||||
|
||||
After `ClosestPointTo` finds the lead-in point on a shape, the shape's entity list must be reordered so that cutting starts at that point. This means:
|
||||
|
||||
1. Find which entity in `Shape.Entities` contains the closest point
|
||||
2. Split that entity at the closest point into two segments
|
||||
3. Reorder: second half of split entity → remaining entities in order → first half of split entity
|
||||
4. The contour now starts and ends at the lead-in point (for closed contours)
|
||||
|
||||
This produces the `List<ICode>` for the contour body that goes between the lead-in and lead-out codes.
|
||||
|
||||
### ContourType Detection
|
||||
|
||||
- `ShapeProfile.Perimeter` → `ContourType.External`
|
||||
- Each cutout in `ShapeProfile.Cutouts`:
|
||||
- If single entity and entity is `Circle` → `ContourType.ArcCircle`
|
||||
- Otherwise → `ContourType.Internal`
|
||||
|
||||
### Normal Angle Computation
|
||||
|
||||
Derived from the `out Entity` returned by `ClosestPointTo`:
|
||||
|
||||
- **Line**: normal is perpendicular to line direction. Use the line's tangent angle, then add π/2 for the normal pointing away from the part interior.
|
||||
- **Arc/Circle**: normal is radial direction from arc center to the closest point: `closestPoint.AngleFrom(arc.Center)`.
|
||||
|
||||
Normal direction convention: always points **away from the part material** (outward from perimeter, inward toward scrap for cutouts). The lead-in approaches from this direction.
|
||||
|
||||
### Arc Rotation Direction
|
||||
|
||||
Lead-in/lead-out arcs must match the **contour winding direction**, not be hardcoded CW. Determine winding from the shape's entity traversal order. Pass the appropriate `RotationType` to `ArcMove`.
|
||||
|
||||
### Method Signature
|
||||
|
||||
```csharp
|
||||
public class ContourCuttingStrategy
|
||||
{
|
||||
public CuttingParameters Parameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Apply cutting strategy to a part's program.
|
||||
/// </summary>
|
||||
/// <param name="partProgram">Original part program (unmodified).</param>
|
||||
/// <param name="plate">Plate for quadrant/size to compute exit point.</param>
|
||||
/// <returns>New Program with lead-ins, lead-outs, and tabs applied. Cutouts first, perimeter last.</returns>
|
||||
public Program Apply(Program partProgram, Plate plate)
|
||||
{
|
||||
// 1. Compute exit point from plate quadrant + size
|
||||
// 2. Convert to geometry, build ShapeProfile
|
||||
// 3. Find closest point on perimeter from exitPoint
|
||||
// 4. Chain cutouts by nearest-neighbor from perimeter point
|
||||
// 5. Reverse chain → cut order
|
||||
// 6. For each contour:
|
||||
// a. Re-index shape entities to start at closest point
|
||||
// b. Detect ContourType
|
||||
// c. Compute normal angle from entity
|
||||
// d. Select lead-in/out from CuttingParameters by ContourType
|
||||
// e. Generate lead-in codes + contour body + lead-out codes
|
||||
// 7. Handle MicrotabLeadOut by trimming last segment
|
||||
// 8. Assemble and return new Program
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ContourType Enum
|
||||
|
||||
```csharp
|
||||
public enum ContourType
|
||||
{
|
||||
External,
|
||||
Internal,
|
||||
ArcCircle
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Point
|
||||
|
||||
`ContourCuttingStrategy.Apply()` runs at nest-time (when parts are placed or cutting parameters are assigned), not at post-processing time. The output `Program` — with lead-ins, lead-outs, start points, and contour ordering — is stored on the `Part` and saved through the normal `NestWriter` path. The post-processor receives this already-complete program and only translates it to machine-specific G-code.
|
||||
|
||||
## Out of Scope (Deferred)
|
||||
|
||||
- **Serialization** of CuttingParameters (JSON/XML discriminators)
|
||||
- **UI integration** (parameter editor forms in WinForms app)
|
||||
- **Part.CutProgram property** (storing the strategy-applied program on `Part`, separate from `Drawing.Program`)
|
||||
- **Tab insertion logic** (`InsertTabs` / `TrimLastSegment` — stubbed with `NotImplementedException`)
|
||||
Reference in New Issue
Block a user