1b62f7af04
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
261 lines
11 KiB
Markdown
261 lines
11 KiB
Markdown
# 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
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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`/`EndPoint` — `Line` 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.
|
|
|
|
```csharp
|
|
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
|