The strategy output (lead-ins, start points, contour ordering) must be saved in the nest file, so Apply() runs when parts are placed — not during post-processing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
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
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 theirArcMoverotation.Generatereturns ICode instructions starting with aRapidMoveto the pierce point, followed by cutting moves to reach the contour start.GetPiercePointcomputes 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(notAngle) to avoid shadowing theOpenNest.Math.Anglestatic 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 lengthApproachAngle(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 lengthApproachAngle1(double): first segment angle. Default: 90.Length2(double): second segment lengthApproachAngle2(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
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 distanceApproachAngle(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
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 tabCutoutMaxWidth,CutoutMaxHeight(double): maximum cutout size to receive this tabAppliesToCutout(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 partBreakerLeadInLength(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.
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
// 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:
- Computes the exit point from the plate's quadrant and size
- Converts the program to geometry via
Program.ToGeometry() - Builds a
ShapeProfilefrom the geometry — givesPerimeter(Shape) andCutouts(List<Shape>) - Uses
Shape.ClosestPointTo(point, out Entity entity)to find lead-in points and the entity for normal computation - Chains cutouts by nearest-neighbor distance from the perimeter closest point
- 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:
- Find which entity in
Shape.Entitiescontains the closest point - Split that entity at the closest point into two segments
- Reorder: second half of split entity → remaining entities in order → first half of split entity
- 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
- If single entity and entity is
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
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
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 fromDrawing.Program) - Tab insertion logic (
InsertTabs/TrimLastSegment— stubbed withNotImplementedException)