# Cutting Strategy Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add lead-in, lead-out, and tab classes to OpenNest.Core that generate ICode instructions for CNC cutting approach/exit geometry. **Architecture:** New `CuttingStrategy/` folder under `OpenNest.Core/CNC/` containing abstract base classes and concrete implementations for lead-ins, lead-outs, and tabs. A `ContourCuttingStrategy` orchestrator uses `ShapeProfile` + `ClosestPointTo` to sequence and apply cutting parameters. Original Drawing/Program geometry is never modified. **Tech Stack:** .NET 8, C#, OpenNest.Core (ICode, Program, Vector, Shape, ShapeProfile, Angle) **Spec:** `docs/superpowers/specs/2026-03-12-cutting-strategy-design.md` --- ## Chunk 1: LeadIn Hierarchy ### Task 1: LeadIn abstract base class **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs` - [ ] **Step 1: Create the abstract base class** ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public abstract class LeadIn { public abstract List Generate(Vector contourStartPoint, double contourNormalAngle, RotationType winding = RotationType.CW); public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle); } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs git commit -m "feat: add LeadIn abstract base class" ``` --- ### Task 2: NoLeadIn **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs` - [ ] **Step 1: Create NoLeadIn** ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class NoLeadIn : LeadIn { public override List Generate(Vector contourStartPoint, double contourNormalAngle, RotationType winding = RotationType.CW) { return new List { new RapidMove(contourStartPoint) }; } public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle) { return contourStartPoint; } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs git commit -m "feat: add NoLeadIn (Type 0)" ``` --- ### Task 3: LineLeadIn **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs` - [ ] **Step 1: Create LineLeadIn** Pierce point is offset from contour start along `contourNormalAngle + Angle.ToRadians(ApproachAngle)` by `Length`. ```csharp 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 Generate(Vector contourStartPoint, double contourNormalAngle, RotationType winding = RotationType.CW) { var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); return new List { 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)); } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs git commit -m "feat: add LineLeadIn (Type 1)" ``` --- ### Task 4: LineArcLeadIn **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs` - [ ] **Step 1: Create LineArcLeadIn** Geometry: Pierce → [Line] → Arc start → [Arc] → Contour start. Arc center at `contourStartPoint + ArcRadius` along normal. Arc rotation uses `winding` parameter. ```csharp 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 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 { 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)); } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs git commit -m "feat: add LineArcLeadIn (Type 2)" ``` --- ### Task 5: ArcLeadIn **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs` - [ ] **Step 1: Create ArcLeadIn** Pierce point is diametrically opposite contour start on the arc circle. ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class ArcLeadIn : LeadIn { public double Radius { get; set; } public override List 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 { 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)); } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs git commit -m "feat: add ArcLeadIn (Type 3)" ``` --- ### Task 6: LineLineLeadIn **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs` - [ ] **Step 1: Create LineLineLeadIn** Two-segment approach: pierce → midpoint → contour start. ```csharp 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 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 { 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)); } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs git commit -m "feat: add LineLineLeadIn (Type 5)" ``` --- ### Task 7: CleanHoleLeadIn **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs` - [ ] **Step 1: Create CleanHoleLeadIn** Same geometry as LineArcLeadIn but with hard-coded 135° angle. Kerf property stored for the paired lead-out to use. ```csharp 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 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 { 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)); } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs git commit -m "feat: add CleanHoleLeadIn" ``` --- ## Chunk 2: LeadOut Hierarchy ### Task 8: LeadOut abstract base class **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs` - [ ] **Step 1: Create the abstract base class** ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public abstract class LeadOut { public abstract List Generate(Vector contourEndPoint, double contourNormalAngle, RotationType winding = RotationType.CW); } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs git commit -m "feat: add LeadOut abstract base class" ``` --- ### Task 9: NoLeadOut **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs` - [ ] **Step 1: Create NoLeadOut** ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class NoLeadOut : LeadOut { public override List Generate(Vector contourEndPoint, double contourNormalAngle, RotationType winding = RotationType.CW) { return new List(); } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs git commit -m "feat: add NoLeadOut (Type 0)" ``` --- ### Task 10: LineLeadOut **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs` - [ ] **Step 1: Create LineLeadOut** ```csharp 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 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 { new LinearMove(endPoint) }; } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs git commit -m "feat: add LineLeadOut (Type 1)" ``` --- ### Task 11: ArcLeadOut **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs` - [ ] **Step 1: Create ArcLeadOut** Arc curves away from the part. End point is a quarter turn from contour end. ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class ArcLeadOut : LeadOut { public double Radius { get; set; } public override List 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 { new ArcMove(endPoint, arcCenter, winding) }; } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs git commit -m "feat: add ArcLeadOut (Type 3)" ``` --- ### Task 12: MicrotabLeadOut **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/LeadOuts/MicrotabLeadOut.cs` - [ ] **Step 1: Create MicrotabLeadOut** Returns empty list — the `ContourCuttingStrategy` handles trimming the last cutting move. ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class MicrotabLeadOut : LeadOut { public double GapSize { get; set; } = 0.03; public override List Generate(Vector contourEndPoint, double contourNormalAngle, RotationType winding = RotationType.CW) { return new List(); } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/LeadOuts/MicrotabLeadOut.cs git commit -m "feat: add MicrotabLeadOut (Type 4)" ``` --- ## Chunk 3: Tab Hierarchy ### Task 13: Tab abstract base class **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs` - [ ] **Step 1: Create Tab base class** ```csharp 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 Generate( Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, RotationType winding = RotationType.CW); } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs git commit -m "feat: add Tab abstract base class" ``` --- ### Task 14: NormalTab **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs` - [ ] **Step 1: Create NormalTab** ```csharp 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 Generate( Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, RotationType winding = RotationType.CW) { var codes = new List(); 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; } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs git commit -m "feat: add NormalTab" ``` --- ### Task 15: BreakerTab **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs` - [ ] **Step 1: Create BreakerTab** ```csharp 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 Generate( Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, RotationType winding = RotationType.CW) { var codes = new List(); 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; } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs git commit -m "feat: add BreakerTab" ``` --- ### Task 16: MachineTab **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs` - [ ] **Step 1: Create MachineTab** ```csharp using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class MachineTab : Tab { public int MachineTabId { get; set; } public override List Generate( Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, RotationType winding = RotationType.CW) { return new List { new RapidMove(tabEndPoint) }; } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs git commit -m "feat: add MachineTab" ``` --- ## Chunk 4: Configuration Classes ### Task 17: ContourType enum, SequenceParameters, AssignmentParameters **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/ContourType.cs` - Create: `OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs` - Create: `OpenNest.Core/CNC/CuttingStrategy/AssignmentParameters.cs` - [ ] **Step 1: Create ContourType.cs** ```csharp namespace OpenNest.CNC.CuttingStrategy { public enum ContourType { External, Internal, ArcCircle } } ``` - [ ] **Step 2: Create SequenceParameters.cs** ```csharp 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; } } ``` - [ ] **Step 3: Create AssignmentParameters.cs** ```csharp 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; } } ``` - [ ] **Step 4: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 5: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/ContourType.cs \ OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs \ OpenNest.Core/CNC/CuttingStrategy/AssignmentParameters.cs git commit -m "feat: add ContourType, SequenceParameters, AssignmentParameters" ``` --- ### Task 18: CuttingParameters **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs` - [ ] **Step 1: Create CuttingParameters** Note: `InternalLeadIn` default uses `ApproachAngle` (not `Angle`) per the naming convention. ```csharp 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(); } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs git commit -m "feat: add CuttingParameters" ``` --- ## Chunk 5: ContourCuttingStrategy Orchestrator ### Task 19: ContourCuttingStrategy **Files:** - Create: `OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs` **Reference files:** - `OpenNest.Core/Plate.cs` — `Quadrant` (int 1-4), `Size` (Size with `.Width`, `.Length`) - `OpenNest.Core/CNC/Program.cs` — `ToGeometry()` returns `List`, `Codes` field - `OpenNest.Core/Geometry/ShapeProfile.cs` — constructor takes `List`, has `.Perimeter` (Shape) and `.Cutouts` (List<Shape>) - `OpenNest.Core/Geometry/Shape.cs` — `ClosestPointTo(Vector pt, out Entity entity)`, `Entities` list - [ ] **Step 1: Create ContourCuttingStrategy with exit point computation and contour type detection** ```csharp 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)); // Contour re-indexing: split shape entities at closestPt so cutting // starts there, convert to ICode, and add to result.Codes throw new System.NotImplementedException("Contour re-indexing not yet implemented"); 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)); throw new System.NotImplementedException("Contour re-indexing not yet implemented"); 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(0, 0), // Q1 TopRight origin → exit BottomLeft 2 => new Vector(w, 0), // Q2 TopLeft origin → exit BottomRight 3 => new Vector(w, l), // Q3 BottomLeft origin → exit TopRight 4 => new Vector(0, l), // Q4 BottomRight origin → exit TopLeft _ => new Vector(0, 0) }; } private List SequenceCutouts(List cutouts, Vector startPoint) { var remaining = new List(cutouts); var ordered = new List(); 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 }; } } } ``` - [ ] **Step 2: Build** Run: `dotnet build OpenNest.Core` Expected: success. If `Shape.Area()` signed-area convention is wrong for winding detection, adjust `DetermineWinding` — check `Shape.Area()` implementation to confirm sign convention. - [ ] **Step 3: Commit** ```bash git add OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs git commit -m "feat: add ContourCuttingStrategy orchestrator Exit point from plate quadrant, nearest-neighbor cutout sequencing via ShapeProfile + ClosestPointTo, contour type detection, and normal angle computation." ``` --- ### Task 20: Full solution build verification - [ ] **Step 1: Build entire solution** Run: `dotnet build OpenNest.sln` Expected: success with no errors. Warnings are acceptable. - [ ] **Step 2: Verify file structure** Run: `find OpenNest.Core/CNC/CuttingStrategy -name '*.cs' | sort` Expected output should match the spec's file structure (21 files total). - [ ] **Step 3: Final commit if any fixups needed** ```bash git add -A OpenNest.Core/CNC/CuttingStrategy/ git commit -m "chore: fixup cutting strategy build issues" ```