mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 02:21:35 -05:00
Capture now honours a placement's pins only while they are active: a part with DrawingPinsOff plans from the plate settings with no warning (D1's per-part escape hatch), and Part.HasActiveDrawingPins is the shared test. A pinned part is regenerated like any unlocked part whatever lead-ins or settings it carried. A locked part with active pins keeps its exact program and warns "Locked lead-ins override drawing pins on part N; Clear Lead-ins to apply the pins." instead of being refused; an unlocked part a caller left ineligible warns that its pins are not applied. Apply's freshness record now holds each drawing's pin list (reference and an explicit StateFingerprint of every pin's point, leads and tab) and each part's pins-off flag, so editing either after capture returns Stale. The fingerprint writer is shared between settings and pins; a reflection test pins ContourCutPin's public surface to what the fingerprint covers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
8.0 KiB
C#
164 lines
8.0 KiB
C#
using System.Reflection;
|
|
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>
|
|
/// Capture reads a placement's active drawing pins (not when switched off), a locked or
|
|
/// unregenerated part keeps its program with a warning, and a proposal goes stale when the
|
|
/// drawing's pins or the part's pins-off flag change after capture.
|
|
/// </summary>
|
|
public class DrawingPinCaptureTests
|
|
{
|
|
[Fact]
|
|
public void PinsOff_PlansThePartWithTheNestSettingsAndNoWarning()
|
|
{
|
|
var (plate, part) = PinnedPlate();
|
|
part.DrawingPinsOff = true;
|
|
var plain = new Part(new Drawing("plain", ExplicitContourTests.Square(false)), part.Location);
|
|
|
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
|
|
confirmedParameters: PinnedContourTests.NestSettings()));
|
|
var automatic = CuttingPlanService.Plan(new CuttingPlanRequest([plain],
|
|
confirmedParameters: PinnedContourTests.NestSettings()));
|
|
|
|
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
|
Assert.Empty(result.Findings);
|
|
Assert.Equal(ExplicitContourTests.Fingerprint(automatic.ProposedOrder[0].CopyProgram()),
|
|
ExplicitContourTests.Fingerprint(result.ProposedOrder[0].CopyProgram()));
|
|
}
|
|
|
|
[Fact]
|
|
public void LockedPartWithActivePins_KeepsItsProgramWithAWarningNotARefusal()
|
|
{
|
|
var (plate, part) = PinnedPlate();
|
|
var prepared = PreparedContours.Capture(part.BaseDrawing.Program, ExplicitContourTests.Parameters());
|
|
var locked = prepared.Emit([prepared.ClosestEntry(0, new Vector(-1, 5))]);
|
|
Assert.True(part.RestoreLeadInProgram(locked, true));
|
|
var program = part.Program;
|
|
|
|
var proposal = CuttingPlanBatch.Capture([plate], PinnedContourTests.NestSettings(), false).Plan();
|
|
|
|
var planned = Assert.Single(proposal.Plates);
|
|
Assert.True(planned.HasOnlyFixedLeadWarnings);
|
|
var warning = Assert.Single(planned.Result.Findings);
|
|
Assert.Equal(PostVerificationKind.FixedLead, warning.Kind);
|
|
Assert.Equal("Locked lead-ins override drawing pins on part 1; Clear Lead-ins to apply the pins.", warning.Message);
|
|
Assert.False(Assert.Single(planned.Result.ProposedOrder).IsRegenerated);
|
|
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: true).Status);
|
|
Assert.Same(program, part.Program);
|
|
|
|
// Switching the pins off for this part leaves nothing to warn about.
|
|
part.DrawingPinsOff = true;
|
|
Assert.True(CuttingPlanBatch.Capture([plate], PinnedContourTests.NestSettings(), false).Plan().CanApply);
|
|
}
|
|
|
|
[Fact]
|
|
public void IneligiblePartWithActivePins_IsNotRegeneratedAndWarns()
|
|
{
|
|
var (plate, part) = PinnedPlate();
|
|
var other = new Part(new Drawing("other", ExplicitContourTests.Square(false)), new Vector(30, 0));
|
|
plate.Parts.Add(other);
|
|
|
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
|
|
confirmedParameters: PinnedContourTests.NestSettings(), eligibleParts: [other]));
|
|
|
|
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
|
|
Assert.Equal("Drawing pins on part 1 are not applied: the part is not regenerated.",
|
|
Assert.Single(result.Findings).Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void PinnedPartWithEarlierLeadInsFromOtherSettings_IsRegeneratedWithItsPins()
|
|
{
|
|
var (plate, part) = PinnedPlate();
|
|
var earlier = ExplicitContourTests.Parameters();
|
|
var prepared = PreparedContours.Capture(part.BaseDrawing.Program, earlier);
|
|
Assert.True(part.RestoreLeadInProgram(prepared.Emit([prepared.ClosestEntry(0, new Vector(11, 5))]), false));
|
|
part.CuttingParameters = earlier;
|
|
|
|
var proposal = CuttingPlanBatch.Capture([plate], PinnedContourTests.NestSettings(), false).Plan();
|
|
|
|
Assert.True(proposal.CanApply, string.Join("\n", proposal.Describe("in")));
|
|
var placement = Assert.Single(proposal.Plates[0].Result.ProposedOrder);
|
|
Assert.True(placement.IsRegenerated);
|
|
Assert.Equal(new Vector(0, 0), Assert.Single(placement.ContourChoices).Point);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("unchanged")]
|
|
[InlineData("pins-off")]
|
|
[InlineData("pin-added")]
|
|
[InlineData("pin-replaced")]
|
|
[InlineData("pin-removed")]
|
|
[InlineData("list-replaced")]
|
|
public void Apply_IsStaleWhenPinsOrPinsOffChangeAfterCapture(string change)
|
|
{
|
|
var (plate, part) = PinnedPlate();
|
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
|
|
confirmedParameters: PinnedContourTests.NestSettings()));
|
|
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
|
var pins = part.BaseDrawing.CutPins;
|
|
switch (change)
|
|
{
|
|
case "pins-off": part.DrawingPinsOff = true; break;
|
|
case "pin-added": pins.Add(PinnedContourTests.Corner(new Vector(10, 10))); break;
|
|
case "pin-replaced": pins[0] = PinnedContourTests.Corner(new Vector(10, 0)); break;
|
|
case "pin-removed": pins.Clear(); break;
|
|
case "list-replaced": part.BaseDrawing.CutPins = [.. pins]; break;
|
|
}
|
|
|
|
var commit = CuttingPlanService.Apply([result]);
|
|
|
|
Assert.Equal(change == "unchanged" ? CuttingCommitStatus.Applied : CuttingCommitStatus.Stale, commit.Status);
|
|
if (change != "unchanged")
|
|
Assert.Contains("drawing pins", result.Snapshot!.PlateState!.Difference(), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void PinFingerprint_SeesEveryPublicPropertyOfAPin()
|
|
{
|
|
// ContourCutPin is immutable: every public property must come from a constructor
|
|
// argument the fingerprint writes. A new property means extending StateFingerprint.
|
|
var properties = typeof(ContourCutPin).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
.Select(p => p.Name).Order().ToArray();
|
|
Assert.Equal(["HasTab", "LeadIn", "LeadOut", "Point", "Tab"], properties);
|
|
Assert.Empty(typeof(ContourCutPin).GetFields(BindingFlags.Public | BindingFlags.Instance));
|
|
|
|
var baseline = new ContourCutPin(new Vector(1, 2), new LineLeadIn { Length = 0.25 }, new NoLeadOut(),
|
|
new NormalTab { Size = 0.03 });
|
|
ContourCutPin[] variants =
|
|
[
|
|
new(new Vector(1.5, 2), baseline.LeadIn, baseline.LeadOut, baseline.Tab),
|
|
new(new Vector(1, 2.5), baseline.LeadIn, baseline.LeadOut, baseline.Tab),
|
|
new(baseline.Point, new LineLeadIn { Length = 0.3 }, baseline.LeadOut, baseline.Tab),
|
|
new(baseline.Point, new ArcLeadIn { Radius = 0.25 }, baseline.LeadOut, baseline.Tab),
|
|
new(baseline.Point, baseline.LeadIn, baseline.LeadOut, new NormalTab { Size = 0.04 }),
|
|
new(baseline.Point, baseline.LeadIn, baseline.LeadOut, new BreakerTab { Size = 0.03 }),
|
|
new(baseline.Point, baseline.LeadIn, baseline.LeadOut), // No tab.
|
|
new(baseline.Point, baseline.LeadIn, new LineLeadOut { Length = 0.1 }),
|
|
];
|
|
var fingerprints = variants.Select(v => StateFingerprint.Of([v]))
|
|
.Append(StateFingerprint.Of([baseline])).ToArray();
|
|
Assert.Equal(fingerprints.Length, fingerprints.Distinct().Count());
|
|
Assert.Equal(StateFingerprint.Of([baseline]), StateFingerprint.Of([new ContourCutPin(
|
|
new Vector(1, 2), new LineLeadIn { Length = 0.25 }, new NoLeadOut(), new NormalTab { Size = 0.03 })]));
|
|
Assert.NotEqual(StateFingerprint.Of((IReadOnlyList<ContourCutPin>)null!), StateFingerprint.Of([]));
|
|
Assert.NotEqual(StateFingerprint.Of([baseline, variants[0]]), StateFingerprint.Of([variants[0], baseline]));
|
|
}
|
|
|
|
private static (Plate Plate, Part Part) PinnedPlate()
|
|
{
|
|
var part = new Part(PinnedContourTests.SquareWithPin(new Vector(0, 0), PinnedContourTests.Corner()),
|
|
new Vector(5, 5));
|
|
var plate = new Nest().CreatePlate();
|
|
plate.Parts.Add(part);
|
|
return (plate, part);
|
|
}
|
|
}
|