Files
OpenNest/docs/superpowers/specs/2026-03-12-cutting-strategy-design.md
T
aj 18023cb1cf docs: clarify cutting strategy runs at nest-time, not post-processing
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>
2026-03-12 23:24:23 -04:00

421 lines
16 KiB
Markdown

# 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&lt;Shape&gt;)
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`)