feat(engine): pinned contours use the fixed-lead warning path

A drawing-pinned contour is now a fixed emission like a locked program. A
regenerated placement maps its execution motions onto the emitted contour
choices (one group per choice after the program's scribes, opened by its
rapids) and owns as fixed the lead-in, cut, lead-out and tab of every pinned
contour; the rapids between contours, including the one into a pinned
contour, stay the planner's and strictly checked. If the motions do not
split cleanly into one group per choice, nothing is treated as fixed.

So a pinned lead that meets another part's material or its own part's no
longer rejects the candidate: the feasibility precheck accepts a pinned
entry as given, the search keeps it in place, and replay reports a FixedLead
warning naming the part, cutting contour, pin and the part hit, with the
advice to increase spacing or switch off drawing pins for the part. The
plate is UNVERIFIED and needs acceptance; the lead is never shortened and
the entry never moved. The unpinned control and the same plate with pins
off are Ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-16 15:33:25 -04:00
1 parent 8d17cf8c59
commit a18f8c9b56
7 files changed
+248 -16

No files matched your search

@@ -88,6 +88,10 @@ public sealed class ContourEntryFeasibility
// substitute for the isolated emitter's owned-choice check.
if (!ReferenceEquals(choice.Owner, prepared))
return new(ContourFeasibilityStatus.Incomplete, "Foreign contour choice.");
// A drawing pin's entry is a fixed emission, accepted as given: it is never filtered
// out here. The emitted program's checks report its violations as warnings.
if (prepared.PinnedEntry(choice.ContourOrdinal) == choice)
return new(ContourFeasibilityStatus.Clear, null);
var key = new Key(choice.ContourOrdinal, choice.EntityOrdinal,
choice.Point.X, choice.Point.Y, nodeContext ?? string.Empty);
if (cache.TryGetValue(key, out var known))
@@ -133,17 +133,71 @@ public sealed class FixedProgramPlacement
/// <summary>
/// The motions of <paramref name="execution"/> (this placement's own program read with its
/// incoming rapid first) that are a fixed emission, accepted as given in regeneration mode:
/// everything a locked program does after the planner's rapid into it. Null when nothing is
/// fixed. Cutoffs and unlocked programs a caller left ineligible keep their strict checks.
/// everything a locked program does after the planner's rapid into it, and the lead-in, cut,
/// lead-out and tab of every drawing-pinned contour in a regenerated program, whose
/// <paramref name="choices"/> (default: this proposal's) were emitted. The rapids between
/// contours stay the planner's. Null when nothing is fixed. Cutoffs and unlocked programs a
/// caller left ineligible keep their strict checks.
/// </summary>
internal Func<int, bool> FixedMotions(OwnedExecution execution)
internal Func<int, bool> FixedMotions(OwnedExecution execution, IReadOnlyList<ContourChoice> choices = null)
{
if (IsCutOff || Prepared != null || !LeadInsLocked)
if (IsCutOff)
return null;
if (Prepared != null)
{
var pins = PinsByMotion(execution, choices);
return pins == null ? null : motion => pins[motion] != null;
}
if (!LeadInsLocked)
return null;
var opensWithRapid = execution.Motions.Count != 0 && execution.Motions[0].Rapid;
return motion => motion > 0 || !opensWithRapid;
}
/// <summary>
/// For each motion of a regenerated program emitted from <paramref name="choices"/>, the
/// one-based drawing pin whose contour it cuts (lead-ins, cuts, lead-outs; never the rapid
/// into the contour), or null. Null overall when no choice is pinned, or when the motions do
/// not split into one group per choice after the scribes, so nothing is treated as fixed.
/// </summary>
internal int?[] PinsByMotion(OwnedExecution execution, IReadOnlyList<ContourChoice> choices = null)
{
choices ??= ContourChoices;
if (Prepared == null || !choices.Any(c => Prepared.IsPinned(c.ContourOrdinal)))
return null;
var pins = new int?[execution.Motions.Count];
var group = -1;
var opening = true; // Inside the rapids that open a group.
int? pin = null;
for (var index = 0; index < execution.Motions.Count; index++)
{
var motion = execution.Motions[index];
if (motion.Rapid)
{
if (!opening)
opening = true;
continue;
}
if (opening)
{
opening = false;
// Scribes are emitted first, before any contour.
if (motion.Layer == LayerType.Scribe)
{
pin = null;
if (group >= 0)
return null;
continue;
}
if (++group >= choices.Count)
return null;
pin = Prepared.PinNumberOf(choices[group].ContourOrdinal);
}
pins[index] = pin;
}
return group == choices.Count - 1 ? pins : null;
}
internal FixedProgramPlacement Propose(Program proposed, OwnedExecution execution, IReadOnlyList<ContourChoice> choices,
System.Threading.CancellationToken token = default)
{
@@ -369,7 +369,7 @@ public static class CuttingPlanService
return new(CuttingPlanStatus.UnsupportedGeometry,
findings: [JointCuttingPlanSearch.Finding(source, PostVerificationKind.Incomplete, ex.Message)], expansions: expansions);
}
var isFixed = source.FixedMotions(execution);
var isFixed = proposal.FixedMotions(execution);
var (strict, fixedFindings) = checker.Check(execution, position, source.SourceOrdinal + 1,
source.IsCutOff, isFixed, token);
findings.AddRange(JointCuttingPlanSearch.Map(snapshot, strict));
@@ -380,7 +380,9 @@ public static class CuttingPlanService
if (!lead.IsComplete || !lead.IsClear)
findings.Add(JointCuttingPlanSearch.Finding(source,
lead.IsComplete ? null : PostVerificationKind.Incomplete, lead.Reason));
warnings.AddRange(fixedLeads.Select(f => FixedLeadWarnings.FromLead(source, execution, f, materialOwners)));
var pins = proposal.PinsByMotion(execution);
warnings.AddRange(fixedLeads.Select(f => FixedLeadWarnings.FromLead(source, execution, f, materialOwners,
pins?[f.Motion])));
// A locked program's coverage (a tab gap, a partial or reversed cut) has no certified
// selection to compare with; a regenerated program's never escapes the strict rule.
if (!complete && source.Prepared == null && isFixed != null)
@@ -15,22 +15,30 @@ internal static class FixedLeadWarnings
private const string LockedAdvice = "The locked program is kept as given. Increase spacing, or edit or "
+ "unlock this part's lead-ins and replan.";
private const string PinnedAdvice = "The pinned contour is cut exactly as its drawing says. Increase spacing "
+ "or switch off drawing pins for this part (right-click > Drawing Pins Off).";
/// <summary>A replay-checker finding (rapid crossing or retention) inside the fixed emission.</summary>
internal static CuttingPlanFinding FromChecker(CuttingPlanSnapshot snapshot, FixedProgramPlacement source,
PostVerificationFinding finding)
{
var other = finding.OtherPartNumber is int part ? snapshot.Placements[part - 1] : null;
var owner = source.Prepared == null ? "Locked lead-ins" : "Drawing pins";
var advice = source.Prepared == null ? LockedAdvice : PinnedAdvice;
return new(source.SourceOrdinal, source.SourcePart, other?.SourceOrdinal, other?.SourcePart,
PostVerificationKind.FixedLead, $"Locked lead-ins: {finding.Message} {LockedAdvice}")
PostVerificationKind.FixedLead, $"{owner}: {finding.Message} {advice}")
{
Witness = finding,
FixedLead = new(null, null, other?.SourceOrdinal)
};
}
/// <summary>A lead motion of the fixed emission that meets material or could not be certified.</summary>
/// <summary>
/// A lead motion of the fixed emission that meets material or could not be certified;
/// <paramref name="pin"/> is the drawing pin that fixed it, or null for a locked program.
/// </summary>
internal static CuttingPlanFinding FromLead(FixedProgramPlacement source, OwnedExecution execution,
FixedLeadFailure failure, IReadOnlyList<FixedProgramPlacement> materialOwners)
FixedLeadFailure failure, IReadOnlyList<FixedProgramPlacement> materialOwners, int? pin)
{
var contour = FixedLeadReview.ContourAt(execution, failure.Motion);
var obstacle = failure.Obstacle is int index ? materialOwners[index] : null;
@@ -39,13 +47,15 @@ internal static class FixedLeadWarnings
: obstacle != null ? "contacts or enters another part's material"
: $"contacts or enters its own part's material ({failure.Result.Reason})";
var motion = execution.Motions[failure.Motion];
var lead = pin is int number ? $"Drawing pin {number} lead" : "Locked lead";
return new(source.SourceOrdinal, source.SourcePart, obstacle?.SourceOrdinal, obstacle?.SourcePart,
PostVerificationKind.FixedLead, $"Locked lead on cutting contour {contour} {what}. {LockedAdvice}")
PostVerificationKind.FixedLead, $"{lead} on cutting contour {contour} {what}. "
+ (pin == null ? LockedAdvice : PinnedAdvice))
{
Witness = new(PostVerificationKind.FixedLead, 1, source.SourceOrdinal + 1,
obstacle == null ? null : obstacle.SourceOrdinal + 1, failure.Result.Reason ?? what)
{ Location = motion.Curve?.Midpoint ?? motion.End },
FixedLead = new(contour, null, obstacle?.SourceOrdinal)
FixedLead = new(contour, pin, obstacle?.SourceOrdinal)
};
}
@@ -502,7 +502,7 @@ internal static class JointCuttingPlanSearch
}
// Prefix includes all earlier cuts and scribes. Replay from BEFORE the whole part.
var checker = before.Copy();
if (!Check(source, execution, arrival, checker, attempt)) continue;
if (!Check(source, execution, arrival, checker, attempt, prefix)) continue;
var distance = execution.RapidDistanceFrom(arrival);
var next = prefix.Length == prepared.Count
? new Node([.. node.Order, source.Propose(program, execution, prefix, token)],
@@ -556,7 +556,7 @@ internal static class JointCuttingPlanSearch
var execution = ExecutionMotionReader.Read(program, source.Location, node.Position, token);
var checker = node.Checker.Copy();
CountExpansion(source);
if (!Check(source, execution, node.Position, checker, attempt)) continue;
if (!Check(source, execution, node.Position, checker, attempt, chain)) continue;
var proposal = source.Propose(program, execution, chain, token);
yield return new(new([.. node.Order, proposal], execution.DeparturePoint, checker, null, node),
execution.RapidDistanceFrom(node.Position), source.SourceOrdinal, -1, endpoint, endpoint);
@@ -728,13 +728,14 @@ internal static class JointCuttingPlanSearch
token.ThrowIfCancellationRequested();
}
// choices: the contours a regenerated execution emits, so its pinned ones are known.
private bool Check(FixedProgramPlacement source, OwnedExecution execution, Vector arrival,
ReleasedContourState checker, Attempt attempt)
ReleasedContourState checker, Attempt attempt, IReadOnlyList<ContourChoice> choices = null)
{
// A fixed emission is accepted as given and never constrains the search: its own
// violations neither reject this candidate nor teach an order rule. Final replay
// reports them as warnings. Every planner-chosen motion stays strict.
var isFixed = source.FixedMotions(execution);
var isFixed = source.FixedMotions(execution, choices);
var findings = checker.Check(execution, arrival, source.SourceOrdinal + 1, source.IsCutOff, isFixed, token).Strict;
rejected.AddRange(Map(snapshot, findings));
attempt.NoteCrossings(source.SourceOrdinal, findings);
@@ -0,0 +1,152 @@
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingPlanning;
/// <summary>
/// D1a for drawing pins: a pinned lead that meets material is a named warning on an UNVERIFIED
/// proposal, never relocated or shortened. The rapids between contours stay strict.
/// </summary>
public class PinnedLeadWarningTests
{
[Fact]
public void PinnedLeadIntoNeighbour_IsUnverifiedWithTheRestPlannedAndTheEntryKept()
{
var (plate, pinned, neighbour) = NeighbourFixture(pins: true);
var proposal = CuttingPlanBatch.Capture([plate], PinnedContourTests.NestSettings(), false).Plan();
var planned = Assert.Single(proposal.Plates);
Assert.True(planned.HasOnlyFixedLeadWarnings, string.Join("\n", proposal.Describe("in")));
var warning = Assert.Single(planned.Result.Findings);
Assert.Equal(PostVerificationKind.FixedLead, warning.Kind);
Assert.Same(pinned, warning.SourcePart);
Assert.Same(neighbour, warning.OtherSourcePart);
Assert.Equal(new FixedLeadWarning(1, 1, 1), warning.FixedLead);
Assert.Equal("Drawing pin 1 lead on cutting contour 1 contacts or enters another part's material. "
+ "The pinned contour is cut exactly as its drawing says. Increase spacing or switch off drawing pins "
+ "for this part (right-click > Drawing Pins Off).", warning.Message);
// The search never relocates the pinned entry, and the neighbour is planned normally.
var proposed = planned.Result.ProposedOrder;
var placement = Assert.Single(proposed, p => ReferenceEquals(p.SourcePart, pinned));
Assert.Equal(new Vector(10, 5), Assert.Single(placement.ContourChoices).Point);
var lead = ExecutionMotionReader.Read(placement.CopyProgram(), pinned.Location, null, default).Motions
.Single(m => m.Layer == LayerType.Leadin);
Assert.Equal(1.0, lead.Length, 9); // Not shortened to fit.
Assert.True(Assert.Single(proposed, p => ReferenceEquals(p.SourcePart, neighbour)).IsRegenerated);
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply().Status);
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: true).Status);
}
[Fact]
public void UnpinnedControl_IsReady()
{
var (plate, _, _) = NeighbourFixture(pins: false);
var proposal = CuttingPlanBatch.Capture([plate], PinnedContourTests.NestSettings(), false).Plan();
Assert.True(proposal.CanApply, string.Join("\n", proposal.Describe("in")));
}
[Fact]
public void PinsOffOnThePinnedPart_IsReady()
{
var (plate, pinned, _) = NeighbourFixture(pins: true);
pinned.DrawingPinsOff = true;
var proposal = CuttingPlanBatch.Capture([plate], PinnedContourTests.NestSettings(), false).Plan();
Assert.True(proposal.CanApply, string.Join("\n", proposal.Describe("in")));
}
[Fact]
public void PinnedHoleLeadIntoItsOwnPartsMaterial_Warns()
{
// Two square holes; hole 2's pinned 2.5 lead runs from beyond its right edge.
var program = new Program();
program.MoveTo(2, 2); program.LineTo(4, 2); program.LineTo(4, 4); program.LineTo(2, 4); program.LineTo(2, 2);
program.MoveTo(4.5, 2); program.LineTo(6.5, 2); program.LineTo(6.5, 4); program.LineTo(4.5, 4); program.LineTo(4.5, 2);
program.MoveTo(0, 0); program.LineTo(0, 10); program.LineTo(10, 10); program.LineTo(10, 0); program.LineTo(0, 0);
var drawing = new Drawing("holes", program);
drawing.CutPins.Add(new ContourCutPin(new Vector(4.5, 3), new LineLeadIn { Length = 2.5, ApproachAngle = 90 },
new NoLeadOut()));
var part = new Part(drawing, new Vector(1, 1));
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], new Vector(-2, -2),
confirmedParameters: PinnedContourTests.NestSettings()));
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
Assert.True(result.IndependentlyReplayed);
var warning = Assert.Single(result.Findings);
Assert.StartsWith("Drawing pin 1 lead on cutting contour", warning.Message);
Assert.Contains("contacts or enters its own part's material", warning.Message);
Assert.Null(warning.OtherSourcePart);
Assert.Equal(1, warning.FixedLead!.PinNumber);
}
[Fact]
public void RapidIntoAPinnedContour_StaysStrict()
{
// Holes(): circles at (3, 3) and (7, 3) in a 10 x 10 square; the outside is pinned at
// (10, 3). Cutting the left hole last and leaving from its far side makes the planner's
// rapid to the pinned pierce cross both finished holes: a refusal, never a warning.
var drawing = new Drawing("holes", PreparedContourTests.Holes());
drawing.CutPins.Add(new ContourCutPin(new Vector(10, 3), new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
new NoLeadOut()));
var part = new Part(drawing, Vector.Zero);
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], new Vector(-2, -2),
confirmedParameters: PinnedContourTests.NestSettings()));
var source = snapshot.Placements[0];
var prepared = source.Prepared!;
var choices = new[] { prepared.Entry(1, 0, new Vector(6, 3)), prepared.Entry(0, 0, new Vector(2, 3)),
prepared.PinnedEntry(2)! };
var program = prepared.Emit(choices);
var proposal = source.Propose(program, ExecutionMotionReader.Read(program, Vector.Zero, new Vector(-2, -2), default),
choices);
var replay = CuttingPlanService.ReplayPrograms(snapshot, [proposal], 0, default);
Assert.Equal(CuttingPlanStatus.ConstraintConflict, replay.Status);
Assert.Contains(replay.Findings, f => f.Kind == PostVerificationKind.RapidCrossing);
Assert.DoesNotContain(replay.Findings, f => f.Kind == PostVerificationKind.FixedLead);
}
[Fact]
public void FeasibilityProbe_AcceptsAPinnedEntryAsGiven()
{
var (plate, pinned, _) = NeighbourFixture(pins: true);
var snapshot = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
confirmedParameters: PinnedContourTests.NestSettings()));
var source = snapshot.Placements.Single(p => ReferenceEquals(p.SourcePart, pinned));
var adapter = new ContourEntryFeasibility(source.Prepared!, source.Location, source.Material!,
snapshot.Placements.Select(p => p.Material!).ToArray());
var verdict = adapter.Check(source.Prepared!.PinnedEntry(0)!);
Assert.True(verdict.IsClear);
Assert.Equal(0, adapter.EvaluationCount);
}
/// <summary>
/// A 10 x 10 square pinned at its right edge midpoint with a 1.0 straight lead, and a
/// 2 x 2 neighbour 0.3 to its right that the lead runs through.
/// </summary>
private static (Plate Plate, Part Pinned, Part Neighbour) NeighbourFixture(bool pins)
{
var drawing = new Drawing("pinned", ExplicitContourTests.Square(false));
if (pins)
drawing.CutPins.Add(new ContourCutPin(new Vector(10, 5), new LineLeadIn { Length = 1.0, ApproachAngle = 90 },
new NoLeadOut()));
var pinned = new Part(drawing, Vector.Zero);
var neighbour = new Part(new Drawing("neighbour", LeadPathValidationTests.Rectangle(0, 0, 2, 2)),
new Vector(10.3, 4));
var plate = new Nest().CreatePlate();
plate.Parts.Add(pinned);
plate.Parts.Add(neighbour);
return (plate, pinned, neighbour);
}
}
+10 -1
View File
@@ -356,7 +356,8 @@ its retention gap, so confirmed-parameters planning refuses it in a planned cont
With confirmed parameters, a locked program (Place Lead-in locks, PEP-imported locked
parts) is a fixed emission: everything it does after the planner's rapid into it is
accepted as given (user decision D1a, 2026-10-14). Its violations are not refusals:
accepted as given (user decision D1a, 2026-10-14). So is each drawing-pinned contour of a
regenerated part (see [drawing pins](#drawing-pins)). Their violations are not refusals:
- Its lead-ins and lead-outs meeting another part's material or its own, a rapid inside
it crossing a completed contour, a lead-out after its own open contour, and coverage
@@ -400,6 +401,14 @@ clean contours with `CutPinResolver` and prepares them with
"Drawing pin 2 no longer matches the geometry; planned with nest settings.", which makes the
plate UNVERIFIED. A tab pinned on an internal contour is not honoured either: only the
outside is tabbed.
- A pinned contour's lead-in, cut, lead-out and tab are a fixed emission like a locked program
(see [fixed emissions](#fixed-emissions-warn)): a pinned lead that meets another part's
material or its own part's (an already-cut hole included) is a `FixedLead` warning naming
the part, cutting contour, pin and the part hit, with the advice "Increase spacing or switch
off drawing pins for this part (right-click > Drawing Pins Off)". The entry is never moved
and the lead never shortened to fit; the feasibility precheck accepts it as given. The
rapids between contours, including the one into a pinned contour, stay strict. Plan
Cutting does not model clamps, for pinned leads or any other.
- A placement with `Part.DrawingPinsOff` ignores its drawing's pins and plans entirely from
the plate settings, with no warning (D1's per-part escape hatch). `Part.HasActiveDrawingPins`
is the shared test for "pins apply here".