Files
OpenNest/docs/superpowers/specs/2026-03-16-leadin-ui-design.md
2026-03-16 11:04:42 -04:00

11 KiB

Lead-In Assignment UI Design (Revised)

Overview

Add a dialog and menu item for assigning lead-ins to parts on a plate. The dialog provides separate parameter sets for external (perimeter) and internal (cutout/hole) contours. Lead-in/lead-out moves are tagged with the existing LayerType.Leadin/LayerType.Leadout enum on each code, making them distinguishable from normal cut code and easy to strip and re-apply.

Design Principles

  • LayerType tagging. Every lead-in move gets Layer = LayerType.Leadin, every lead-out move gets Layer = LayerType.Leadout. Normal contour cuts keep Layer = LayerType.Cut (the default). This uses the existing LayerType enum and LinearMove.Layer/ArcMove.Layer properties — no new enums or flags.
  • Always rebuild from base. ContourCuttingStrategy.Apply converts the input program to geometry via Program.ToGeometry() and ShapeProfile. These do NOT filter by layer — all entities (including lead-in/out codes if present) would be processed. Therefore, the strategy must always receive a clean program (cut codes only). The flow always clones from Part.BaseDrawing.Program and re-rotates before applying.
  • Non-destructive. Part.BaseDrawing.Program is never modified. The strategy builds a fresh Program with lead-ins baked in. Part.HasManualLeadIns (existing property) is set to true when lead-ins are assigned, so the automated PlateProcessor pipeline skips these parts.

Lead-In Dialog (LeadInForm)

A WinForms dialog in OpenNest/Forms/LeadInForm.cs with two parameter groups, one checkbox, and OK/Cancel buttons.

External Group (Perimeter)

  • Lead-in angle (degrees) — default 90
  • Lead-in length (inches) — default 0.125
  • Overtravel (inches) — default 0.03

Internal Group (Cutouts & Holes)

  • Lead-in angle (degrees) — default 90
  • Lead-in length (inches) — default 0.125
  • Overtravel (inches) — default 0.03

Update Existing Checkbox

  • "Update existing lead-ins" — checked by default
  • When checked: strip all existing lead-in/lead-out codes from every part before re-applying
  • When unchecked: only process parts that have no LayerType.Leadin codes in their program

Dialog Result

public class LeadInSettings
{
    // External (perimeter) parameters
    public double ExternalLeadInAngle { get; set; } = 90;
    public double ExternalLeadInLength { get; set; } = 0.125;
    public double ExternalOvertravel { get; set; } = 0.03;

    // Internal (cutout/hole) parameters
    public double InternalLeadInAngle { get; set; } = 90;
    public double InternalLeadInLength { get; set; } = 0.125;
    public double InternalOvertravel { get; set; } = 0.03;

    // Behavior
    public bool UpdateExisting { get; set; } = true;
}

Note: LineLeadIn.ApproachAngle and LineLeadOut.ApproachAngle store degrees (not radians), converting internally via Angle.ToRadians(). The LeadInSettings values are degrees and can be passed directly.

LeadInSettings to CuttingParameters Mapping

The caller builds one CuttingParameters instance with separate external and internal settings. ArcCircle shares the internal settings:

ExternalLeadIn  = new LineLeadIn { ApproachAngle = settings.ExternalLeadInAngle, Length = settings.ExternalLeadInLength }
ExternalLeadOut = new LineLeadOut { Length = settings.ExternalOvertravel }
InternalLeadIn  = new LineLeadIn { ApproachAngle = settings.InternalLeadInAngle, Length = settings.InternalLeadInLength }
InternalLeadOut = new LineLeadOut { Length = settings.InternalOvertravel }
ArcCircleLeadIn = (same as Internal)
ArcCircleLeadOut = (same as Internal)

Detecting Existing Lead-Ins

Check whether a part's program contains lead-in codes by inspecting LayerType:

bool HasLeadIns(Program program)
{
    foreach (var code in program.Codes)
    {
        if (code is LinearMove lm && lm.Layer == LayerType.Leadin)
            return true;
        if (code is ArcMove am && am.Layer == LayerType.Leadin)
            return true;
    }
    return false;
}

Preparing a Clean Program

Important: Program.ToGeometry() and ShapeProfile process ALL entities regardless of layer. They do NOT filter out lead-in/lead-out codes. If the strategy receives a program that already has lead-in codes baked in, those codes would be converted to geometry entities and corrupt the perimeter/cutout detection.

Therefore, the flow always starts from a clean base:

var cleanProgram = part.BaseDrawing.Program.Clone() as Program;
cleanProgram.Rotate(part.Rotation);

This produces a program with only the original cut geometry at the part's current rotation angle, safe to feed into ContourCuttingStrategy.Apply.

Menu Integration

Add "Assign Lead-Ins" to the Plate menu in MainForm, after "Sequence Parts" and before "Calculate Cut Time".

Click handler in MainForm delegates to EditNestForm.AssignLeadIns().

AssignLeadIns Flow (EditNestForm)

1. Open LeadInForm dialog
2. If user clicks OK:
   a. Get LeadInSettings from dialog (includes UpdateExisting flag)
   b. Build one ContourCuttingStrategy with CuttingParameters from settings
   c. Get exit point: PlateHelper.GetExitPoint(plate)  [now public]
   d. Set currentPoint = exitPoint
   e. For each part on the current plate (in sequence order):
      - If !updateExisting and part already has lead-in codes → skip
      - Build clean program: clone BaseDrawing.Program, rotate to part.Rotation
      - Compute localApproach = currentPoint - part.Location
      - Call strategy.Apply(cleanProgram, localApproach) → CuttingResult
      - Call part.ApplyLeadIns(cutResult.Program)
        (this sets Program, HasManualLeadIns = true, and recalculates bounds)
      - Update currentPoint = cutResult.LastCutPoint + part.Location
   f. Invalidate PlateView to show updated geometry

Note: The clean program is always rebuilt from BaseDrawing.Program — never from the current Part.Program — because Program.ToGeometry() and ShapeProfile do not filter by layer and would be corrupted by existing lead-in codes.

Note: Setting Part.Program requires a public method since the setter is private. See Model Changes below.

Model Changes

Part (OpenNest.Core)

Add a method to apply lead-ins and mark the part:

public void ApplyLeadIns(Program processedProgram)
{
    Program = processedProgram;
    HasManualLeadIns = true;
    UpdateBounds();
}

This atomically sets the processed program, marks HasManualLeadIns = true (so PlateProcessor skips this part), and recalculates bounds. The private setter on Program stays private — ApplyLeadIns is the public API.

PlateHelper (OpenNest.Engine)

Change PlateHelper from internal static to public static so the UI project can access GetExitPoint.

ContourCuttingStrategy Changes

LayerType Tagging

When emitting lead-in moves, stamp each code with Layer = LayerType.Leadin. When emitting lead-out moves, stamp with Layer = LayerType.Leadout. This applies to all move types (LinearMove, ArcMove) generated by LeadIn.Generate() and LeadOut.Generate().

The LeadIn.Generate() and LeadOut.Generate() methods return List<ICode>. After calling them, the strategy sets the Layer property on each returned code:

var leadInCodes = leadIn.Generate(piercePoint, normal, winding);
foreach (var code in leadInCodes)
{
    if (code is LinearMove lm) lm.Layer = LayerType.Leadin;
    else if (code is ArcMove am) am.Layer = LayerType.Leadin;
}
result.Codes.AddRange(leadInCodes);

Same pattern for lead-out codes with LayerType.Leadout.

Corner vs Mid-Entity Auto-Detection

When generating the lead-out, the strategy detects whether the pierce point landed on a corner or mid-entity. Detection uses the out Entity from ClosestPointTo with type-specific endpoint checks:

private static bool IsCornerPierce(Vector closestPt, Entity entity)
{
    if (entity is Line line)
        return closestPt.DistanceTo(line.StartPoint) < Tolerance.Epsilon
            || closestPt.DistanceTo(line.EndPoint) < Tolerance.Epsilon;
    if (entity is Arc arc)
        return closestPt.DistanceTo(arc.StartPoint()) < Tolerance.Epsilon
            || closestPt.DistanceTo(arc.EndPoint()) < Tolerance.Epsilon;
    return false;
}

Note: Entity has no polymorphic StartPoint/EndPointLine has properties, Arc has methods, Circle has neither.

Corner Lead-Out

Delegates to LeadOut.Generate() as normal — LineLeadOut extends past the corner along the contour normal. Moves are tagged LayerType.Leadout.

Mid-Entity Lead-Out (Contour-Follow Overtravel)

Handled at the ContourCuttingStrategy level, NOT via LeadOut.Generate() (which lacks access to the contour shape). The overtravel distance is read from the selected LeadOut for the current contour type — SelectLeadOut(contourType). Since external and internal have separate LineLeadOut instances in CuttingParameters, the overtravel distance automatically varies by contour type.

var leadOut = SelectLeadOut(contourType);
if (IsCornerPierce(closestPt, entity))
{
    // Corner: delegate to LeadOut.Generate() as normal
    var codes = leadOut.Generate(closestPt, normal, winding);
    // tag as LayerType.Leadout
}
else if (leadOut is LineLeadOut lineLeadOut && lineLeadOut.Length > 0)
{
    // Mid-entity: retrace the start of the contour for overtravel distance
    var codes = GenerateOvertravelMoves(reindexed, lineLeadOut.Length);
    // tag as LayerType.Leadout
}

The contour-follow retraces the beginning of the reindexed shape:

  1. Walking the reindexed shape's entities from the start
  2. Accumulating distance until overtravel is reached
  3. Emitting LinearMove/ArcMove codes for those segments (splitting the last segment if needed)
  4. Tagging all emitted moves as LayerType.Leadout

This produces a clean overcut that ensures the contour fully closes.

Lead-out behavior summary

Contour Type Pierce Location Lead-Out Behavior
External Corner LineLeadOut.Generate() — extends past corner
External Mid-entity Contour-follow overtravel moves
Internal Corner LineLeadOut.Generate() — extends past corner
Internal Mid-entity Contour-follow overtravel moves
ArcCircle N/A (always mid-entity) Contour-follow overtravel moves

File Structure

OpenNest.Core/
├── Part.cs                                    # add ApplyLeadIns method
└── CNC/CuttingStrategy/
    └── ContourCuttingStrategy.cs              # LayerType tagging, Overtravel, corner detection

OpenNest.Engine/
└── Sequencing/
    └── PlateHelper.cs                         # change internal → public

OpenNest/
├── Forms/
│   ├── LeadInForm.cs                          # new dialog
│   ├── LeadInForm.Designer.cs                 # new dialog designer
│   ├── MainForm.Designer.cs                   # add menu item
│   ├── MainForm.cs                            # add click handler
│   └── EditNestForm.cs                        # add AssignLeadIns method
└── LeadInSettings.cs                          # settings DTO

Out of Scope

  • Tabbed (V lead-in/out) parameters and Part.IsTabbed — deferred until tab assignment UI
  • Slug destruct for internal cutouts
  • Lead-in visualization colors in PlateView (separate enhancement)
  • Database storage of lead-in presets by material/thickness
  • MicrotabLeadOut integration
  • Nest file serialization changes