fix(cutting): certify complete selected contour programs

This commit is contained in:
aj committed 2026-10-04 22:02:56 -04:00
1 parent 9f7580a486
commit e8d974a2db
8 files changed
+538 -47

No files matched your search

@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
using Curve = OpenNest.Diagnostics.PostVerificationGeometry.Curve;
namespace OpenNest.CNC.CuttingPlanning;
/// <summary>Independently constructed expected geometry, frozen before final replay.
/// It is not a search verdict or the payload returned to the caller.</summary>
internal sealed class SelectedContourProgram
{
private readonly PreparedContours owner;
private readonly ContourChoice[] choices;
internal OwnedExecution Expected { get; }
internal double TabSize { get; }
internal SelectedContourProgram(PreparedContours owner, IReadOnlyList<ContourChoice> choices,
OwnedExecution expected, double tabSize)
{
this.owner = owner;
this.choices = choices.ToArray();
Expected = expected;
TabSize = tabSize;
}
internal bool Matches(PreparedContours prepared, IReadOnlyList<ContourChoice> selected)
{
if (!ReferenceEquals(owner, prepared) || !choices.SequenceEqual(selected)) return false;
prepared.ValidateCompleteChoices(selected);
return true;
}
}
/// <summary>Directed native boundary accounting and selected-emission correspondence.
/// No tessellation, topology inference, live source reads or final emission.</summary>
internal static class ContourProgramVerifier
{
private const double Epsilon = PostVerificationGeometry.Epsilon;
internal static bool Verify(OwnedExecution actual, LeadMaterialSnapshot material,
SelectedContourProgram selected, CancellationToken token)
{
var budget = 1000000;
if (material == null || !material.IsComplete) return false;
if (selected != null && !SameEmission(actual, selected.Expected, token, ref budget)) return false;
var visited = new HashSet<int>();
foreach (var run in Runs(actual).Where(r => !r.Rapid && r.Kind == LayerType.Cut))
{
token.ThrowIfCancellationRequested();
if (run.Curves.Count == 0) return false;
var first = run.Curves[0];
var ringIndex = -1;
var entityIndex = -1;
for (var r = 0; r < material.Rings.Count && ringIndex < 0; r++)
for (var e = 0; e < material.Rings[r].Length; e++)
{
Query(token, ref budget);
var curve = material.Rings[r][e];
if (curve.SameDirection(first) && curve.Contains(first.Start)
&& curve.DistanceAlong(first.Start) < curve.Length - Epsilon)
{
ringIndex = r;
entityIndex = e;
break;
}
}
if (ringIndex < 0 || !visited.Add(ringIndex)) return false;
var ring = material.Rings[ringIndex];
var nominalLength = ring.Sum(c => c.Length);
var cutLength = run.Curves.Sum(c => c.Length);
var missing = nominalLength - cutLength;
if (missing < -Epsilon) return false; // Never a second circuit/retrace.
if (missing > Epsilon)
{
// Fixed legacy payloads have no certified selected tab/entry metadata.
// A merely open contour or a caller's current tab switch proves nothing.
if (selected == null || ringIndex != 0 || selected.TabSize <= 0
|| System.Math.Abs(first.Start.DistanceTo(run.Curves[^1].End) - selected.TabSize) > Epsilon)
return false;
// SameEmission has already bound this exact terminal gap to the owned
// settings' independently emitted, rounded/clamped selected geometry.
}
if (!Follows(run.Curves, ring, entityIndex, ring[entityIndex].DistanceAlong(first.Start),
true, token, ref budget)) return false;
}
return visited.Count == material.Rings.Count;
}
private static bool SameEmission(OwnedExecution actual, OwnedExecution expected,
CancellationToken token, ref int budget)
{
var left = Runs(actual);
var right = Runs(expected);
if (left.Count != right.Count) return false;
for (var i = 0; i < left.Count; i++)
{
Query(token, ref budget);
var a = left[i];
var b = right[i];
if (a.Kind != b.Kind || a.Rapid != b.Rapid) return false;
if (a.Rapid)
{
// Arrival is supplied anew by replay, not captured expected geometry.
if (a.End.DistanceTo(b.End) > Epsilon) return false;
}
else if (System.Math.Abs(a.Curves.Sum(c => c.Length) - b.Curves.Sum(c => c.Length)) > Epsilon
|| !Follows(a.Curves, b.Curves, 0, 0, false, token, ref budget)) return false;
}
return true;
}
// Consume directed native arc-length from both streams. Subdivision, merged
// collinear moves and a cyclic ring's reindexing do not change coverage.
private static bool Follows(IReadOnlyList<Curve> actual, IReadOnlyList<Curve> expected,
int index, double offset, bool cyclic, CancellationToken token, ref int budget)
{
foreach (var curve in actual)
{
var consumed = 0.0;
while (consumed < curve.Length - Epsilon)
{
Query(token, ref budget);
if (index >= expected.Count)
{
if (!cyclic) return false;
index = 0;
}
var nominal = expected[index];
var remaining = nominal.Length - offset;
if (remaining <= 0)
{
index++;
offset = 0;
continue;
}
var length = System.Math.Min(remaining, curve.Length - consumed);
if (!nominal.SameDirection(curve)
|| nominal.PointAtLength(offset).DistanceTo(curve.PointAtLength(consumed)) > Epsilon
|| nominal.PointAtLength(offset + length).DistanceTo(curve.PointAtLength(consumed + length)) > Epsilon)
return false;
consumed += length;
offset += length;
if (offset >= nominal.Length - Epsilon)
{
index++;
offset = 0;
}
}
}
return cyclic || index == expected.Count || index == expected.Count - 1
&& expected[index].Length - offset <= Epsilon;
}
private static List<Run> Runs(OwnedExecution execution)
{
var runs = new List<Run>();
foreach (var motion in execution.Motions)
{
var kind = motion.Layer is LayerType.Cut or LayerType.Display ? LayerType.Cut : motion.Layer;
if (motion.Rapid)
{
runs.Add(new Run(true, kind, motion.End));
continue;
}
if (motion.Length <= Epsilon) continue; // Zero travel contributes no coverage.
if (runs.Count == 0 || runs[^1].Rapid || runs[^1].Kind != kind)
runs.Add(new Run(false, kind, motion.End));
runs[^1].Curves.Add(motion.Curve);
}
return runs;
}
private static void Query(CancellationToken token, ref int budget)
{
token.ThrowIfCancellationRequested();
if (--budget < 0) throw new NotSupportedException("Replay contour accounting exceeds the native query limit.");
}
private sealed record Run(bool Rapid, LayerType Kind, Vector End)
{
internal List<Curve> Curves { get; } = new();
}
}
@@ -171,6 +171,18 @@ public sealed class PreparedContours
shapes.Select(s => (Shape)s.Clone()).ToArray(), scribes.Select(e => e.Clone()).ToList(), choices);
}
internal void ValidateCompleteChoices(IReadOnlyList<ContourChoice> choices) => Validate(choices, true);
// Build expected-emission metadata independently of the selected payload BEFORE
// replay. Final verification only reads this immutable execution and clean geometry.
internal SelectedContourProgram CaptureSelectedProgram(IReadOnlyList<ContourChoice> choices,
Vector location, CancellationToken token)
{
token.ThrowIfCancellationRequested();
var expected = ExecutionMotionReader.ReadSupported(Emit(choices), location, null, token);
return new(this, choices, expected, parameters.TabsEnabled ? parameters.TabConfig.Size : 0);
}
private void Validate(IReadOnlyList<ContourChoice> choices, bool complete)
{
if (choices == null || (complete && choices.Count != Count) || choices.Count > Count)
@@ -99,6 +99,26 @@ internal static class PostVerificationGeometry
&& System.Math.Abs(Cross(other.End - Start, direction)) <= Epsilon;
}
// Directed native arc-length coordinates for contour replay, not collision queries.
internal bool SameDirection(Curve other) => SameSupport(other)
&& (Center.HasValue ? System.Math.Sign(Sweep) == System.Math.Sign(other.Sweep)
: Dot(End - Start, other.End - other.Start) > 0);
internal double DistanceAlong(Vector point) => Center is { } center
? point.DistanceTo(Start) <= Epsilon ? 0
: Travel(System.Math.Atan2(point.Y - center.Y, point.X - center.X)) * Radius
: Dot(point - Start, (End - Start) * (1 / Length));
internal Vector PointAtLength(double distance)
{
if (distance <= 0) return Start;
if (distance >= Length) return End;
return Center is { } center
? center + new Vector(System.Math.Cos(StartAngle + System.Math.Sign(Sweep) * distance / Radius),
System.Math.Sin(StartAngle + System.Math.Sign(Sweep) * distance / Radius)) * Radius
: Start + (End - Start) * (distance / Length);
}
internal bool Contains(Vector point) => ToEntity().ClosestPointTo(point).DistanceTo(point) <= Epsilon;
internal IReadOnlyList<Vector> Contacts(Curve other, out bool overlap)
@@ -94,8 +94,27 @@ public sealed class FixedProgramPlacement
public bool IsRegenerated => ContourChoices.Count != 0;
/// <summary>Returns an independent deep copy; never an alias to captured/proposed code.</summary>
public Program CopyProgram() => program == null ? null : OwnedProgramCopy.Copy(program);
internal FixedProgramPlacement Propose(Program proposed, OwnedExecution execution, IReadOnlyList<ContourChoice> choices) =>
new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution, OwnedProgramCopy.Copy(proposed), Prepared, Material, choices);
internal SelectedContourProgram SelectedProgram { get; private init; }
internal FixedProgramPlacement Propose(Program proposed, OwnedExecution execution, IReadOnlyList<ContourChoice> choices,
System.Threading.CancellationToken token = default)
{
SelectedContourProgram selected = null;
try
{
// The expected program is constructed from owned choices/settings, NEVER
// from proposed or its cached execution. Counterfeit payloads remain subject
// to independent final checks, including callers of this internal test seam.
selected = Prepared?.CaptureSelectedProgram(choices, Location, token);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or InvalidOperationException or ArithmeticException)
{
// Invalid metadata is a refused proposal, not an exception from final replay.
}
return new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution,
OwnedProgramCopy.Copy(proposed, token), Prepared, Material, choices)
{ SelectedProgram = selected };
}
public Part SourcePart { get; }
public int SourceOrdinal { get; }
@@ -201,18 +201,15 @@ public static class CuttingPlanService
if (!ReferenceEquals(source, proposal))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
}
else if (proposal.ContourChoices.Count != source.Prepared.Count
|| proposal.ContourChoices.Any(c => !ReferenceEquals(c.Owner, source.Prepared)
|| c.ContourOrdinal < 0 || c.ContourOrdinal >= source.Prepared.Count)
|| proposal.ContourChoices.Select(c => c.ContourOrdinal).Distinct().Count() != source.Prepared.Count
|| proposal.ContourChoices[^1].ContourOrdinal != source.Prepared.PerimeterOrdinal)
else if (proposal.SelectedProgram == null
|| !proposal.SelectedProgram.Matches(source.Prepared, proposal.ContourChoices))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
OwnedExecution execution;
bool complete;
try
{
execution = ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, position, token);
if (source.Prepared != null && !HasCompleteContourAccounting(execution, source.Material, token))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
complete = ContourProgramVerifier.Verify(execution, source.Material, proposal.SelectedProgram, token);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
@@ -225,6 +222,11 @@ public static class CuttingPlanService
if (!lead.IsComplete || !lead.IsClear)
findings.Add(JointCuttingPlanSearch.Finding(source,
lead.IsComplete ? null : PostVerificationKind.Incomplete, lead.Reason));
if (!complete && findings.Count == 0)
return new(CuttingPlanStatus.InvalidInput,
findings: [JointCuttingPlanSearch.Finding(source, null,
"Selected program does not preserve complete directed contours, certified tabs or selected entry/style geometry.")],
expansions: expansions);
distance += execution.RapidDistanceFrom(position);
position = execution.DeparturePoint;
}
@@ -237,42 +239,6 @@ public static class CuttingPlanService
rapidDistance: distance, independentlyReplayed: true);
}
private static bool HasCompleteContourAccounting(OwnedExecution execution, LeadMaterialSnapshot material, CancellationToken token)
{
var visited = new HashSet<int>();
var current = -1;
var budget = 1000000;
foreach (var move in execution.Motions)
{
token.ThrowIfCancellationRequested();
if (move.Rapid || move.Layer is not (LayerType.Cut or LayerType.Display))
{
current = -1;
continue;
}
var ring = -1;
for (var i = 0; i < material.Rings.Count; i++)
if (material.Rings[i].Any(c => Matches(c, move)))
{
ring = i;
break;
}
if (ring < 0 || current >= 0 && ring != current || current < 0 && !visited.Add(ring))
return false;
current = ring;
}
return visited.Count == material.Rings.Count;
bool Matches(PostVerificationGeometry.Curve curve, ExecutionMotion move)
{
token.ThrowIfCancellationRequested();
if (--budget < 0) throw new NotSupportedException("Replay contour accounting exceeds the native query limit.");
return curve.SameSupport(move.Curve) && curve.Contains(move.Curve.Start)
&& curve.Contains(move.End) && curve.Contains(move.Curve.Midpoint)
&& move.Length <= curve.Length + PostVerificationGeometry.Epsilon;
}
}
private static bool SameBits(double source, double proposed) =>
BitConverter.DoubleToInt64Bits(source) == BitConverter.DoubleToInt64Bits(proposed);
@@ -105,7 +105,7 @@ internal static class JointCuttingPlanSearch
if (!Check(source, execution, arrival, checker)) continue;
var distance = execution.RapidDistanceFrom(arrival);
var next = prefix.Length == prepared.Count
? new Node([.. node.Order, source.Propose(program, execution, prefix)], execution.DeparturePoint, checker, null)
? new Node([.. node.Order, source.Propose(program, execution, prefix, token)], execution.DeparturePoint, checker, null)
: new Node(node.Order, execution.DeparturePoint, checker,
new(source, prefix, arrival, before, distance));
yield return new(next, distance - (node.Active?.Distance ?? 0), source.SourceOrdinal, contour, entry);
@@ -0,0 +1,287 @@
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;
public class CuttingReplayContractTests
{
private static readonly Vector Start = new(-2, 5);
[Theory]
[InlineData("partial")]
[InlineData("retrace")]
[InlineData("reverse")]
[InlineData("twice")]
[InlineData("wrong-point")]
[InlineData("wrong-index")]
[InlineData("different-entry")]
[InlineData("short-lead")]
public void Replay_RejectsCounterfeitSelectedPayload(string fault)
{
var (part, settings) = Fixture();
var before = ExplicitContourTests.Fingerprint(part.Program);
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], Start, confirmedParameters: settings));
var source = snapshot.Placements[0];
var choices = new[] { source.Prepared.ClosestEntry(0, Start) };
var program = source.Prepared.Emit(choices);
switch (fault)
{
case "partial": program = Path(new Vector(0, 10)); break;
case "retrace": program = Path(new(0, 8), new(0, 7), new(0, 10), new(10, 10), new(10, 0), new(0, 0), new(0, 5)); break;
case "reverse": program = Path(new(0, 0), new(10, 0), new(10, 10), new(0, 10), new(0, 5)); break;
case "twice": program = Path(new(0, 10), new(10, 10), new(10, 0), new(0, 0), new(0, 5), new(0, 10), new(10, 10), new(10, 0), new(0, 0), new(0, 5)); break;
case "wrong-point": choices[0] = choices[0] with { Point = new Vector(999, 999) }; break;
case "wrong-index": choices[0] = choices[0] with { EntityOrdinal = 999 }; break;
case "different-entry": choices[0] = source.Prepared.Entry(0, 0, new Vector(0, 6)); break;
case "short-lead":
program.Mode = Mode.Absolute;
var rapid = program.Codes.OfType<RapidMove>().First();
var lead = program.Codes.OfType<LinearMove>().First(m => m.Layer == LayerType.Leadin);
rapid.EndPoint = lead.EndPoint + (rapid.EndPoint - lead.EndPoint) * 0.5;
break;
}
var result = CuttingPlanService.ReplayPrograms(snapshot, [source.Propose(program, Read(program), choices)], 0, default);
Assert.NotEqual(CuttingPlanStatus.Ready, result.Status);
Assert.Empty(result.ProposedOrder);
Assert.False(result.IndependentlyReplayed);
Assert.Equal(before, ExplicitContourTests.Fingerprint(part.Program));
}
[Theory]
[InlineData(true, "partial")]
[InlineData(false, "partial")]
[InlineData(true, "reverse")]
[InlineData(false, "retrace")]
public void ConfirmedFixedProgram_RejectsIncompleteOrWrongDirectedCoverageWithoutRepair(bool locked, string fault)
{
var (part, settings) = Fixture();
var program = fault switch
{
"reverse" => Path(new(0, 0), new(10, 0), new(10, 10), new(0, 10), new(0, 5)),
"retrace" => Path(new(0, 8), new(0, 7), new(0, 10), new(10, 10), new(10, 0), new(0, 0), new(0, 5)),
_ => Path(new Vector(0, 10))
};
Assert.True(part.RestoreLeadInProgram(program, locked));
var before = ExplicitContourTests.Fingerprint(part.Program);
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], Start, confirmedParameters: settings,
eligibleParts: locked ? null : []));
var result = CuttingPlanService.Plan(snapshot);
Assert.NotEqual(CuttingPlanStatus.Ready, result.Status);
Assert.NotEqual(CuttingPlanStatus.Ready, CuttingPlanService.ReplayPrograms(snapshot, snapshot.Placements, 0, default).Status);
Assert.Empty(result.ProposedOrder);
Assert.Equal(before, ExplicitContourTests.Fingerprint(part.Program));
}
[Fact]
public void Replay_RejectsDifferentActualContourOrderWithUnchangedChoices()
{
var settings = ExplicitContourTests.Parameters();
var part = new Part(new Drawing("holes", PreparedContourTests.Holes()));
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], Start, confirmedParameters: settings));
var source = snapshot.Placements[0];
var choices = Enumerable.Range(0, 3).Select(i => source.Prepared.ClosestEntry(i, Start)).ToArray();
var swapped = new[] { choices[1], choices[0], choices[2] };
var program = source.Prepared.Emit(swapped);
Assert.NotEqual(CuttingPlanStatus.Ready,
CuttingPlanService.ReplayPrograms(snapshot, [source.Propose(program, Read(program), choices)], 0, default).Status);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Replay_PreservesFullAndExactConfiguredTabWithSubdivisions(bool tabbed)
{
var (part, settings) = Fixture();
settings.TabsEnabled = tabbed;
settings.TabConfig = new NormalTab { Size = 0.2 };
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], Start, confirmedParameters: settings));
var ready = CuttingPlanService.Plan(snapshot);
Assert.True(ready.Status == CuttingPlanStatus.Ready, string.Join("; ", ready.Findings.Select(f => f.Message)));
var source = snapshot.Placements[0];
var proposal = ready.ProposedOrder[0];
Assert.Equal(tabbed ? 39.8 : 40, proposal.Execution.Motions.Where(IsCut).Sum(m => m.Length), 8);
var subdivided = Subdivide(proposal.CopyProgram());
var replay = CuttingPlanService.ReplayPrograms(snapshot,
[source.Propose(subdivided, Read(subdivided), proposal.ContourChoices)], 0, default);
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
Assert.True(replay.IndependentlyReplayed);
// A different retained gap is not the selected tab, even though it remains open.
if (tabbed)
{
var counterfeit = proposal.CopyProgram();
counterfeit.Mode = Mode.Absolute;
var last = counterfeit.Codes.OfType<LinearMove>().Last(m => m.Layer == LayerType.Display);
last.EndPoint = new Vector(0, 4.9);
Assert.NotEqual(CuttingPlanStatus.Ready, CuttingPlanService.ReplayPrograms(snapshot,
[source.Propose(counterfeit, Read(counterfeit), proposal.ContourChoices)], 0, default).Status);
}
}
[Fact]
public void FixedUnknownTabMetadata_IsRefusedButCompatibilityRouteRemainsUnchanged()
{
var (part, settings) = Fixture();
Assert.True(part.RestoreLeadInProgram(Path(new Vector(0, 10)), true));
Assert.Equal(CuttingPlanStatus.Ready, CuttingPlanService.Plan(new CuttingPlanRequest([part], Start)).Status);
settings.TabsEnabled = true;
settings.TabConfig = new NormalTab { Size = 0.2 };
var prepared = PreparedContours.Capture(part.BaseDrawing.Program, settings);
var tabbed = prepared.Emit([prepared.ClosestEntry(0, Start)]);
Assert.True(part.RestoreLeadInProgram(tabbed, true));
Assert.NotEqual(CuttingPlanStatus.Ready, CuttingPlanService.Plan(new CuttingPlanRequest([part], Start, confirmedParameters: settings)).Status);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CompleteFixedProgram_RetainsAuthoredEntryAndDimensions(bool locked)
{
var (part, settings) = Fixture();
part.LeadInsLocked = locked;
((LineLeadIn)settings.ExternalLeadIn).Length = 0.7;
var before = ExplicitContourTests.Fingerprint(part.Program);
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], Start, confirmedParameters: settings,
eligibleParts: locked ? null : []));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.Equal(before, ExplicitContourTests.Fingerprint(result.ProposedOrder[0].CopyProgram()));
}
[Theory]
[InlineData("circles")]
[InlineData("mixed-circles")]
[InlineData("arcs")]
public void NativeCurves_SelectedRoundingClampingAndSubdivisionRemainValid(string shape)
{
var settings = ExplicitContourTests.Parameters();
settings.RoundLeadInAngles = true;
settings.LeadInAngleIncrement = 90;
settings.ArcCircleLeadIn = new LineLeadIn { Length = 10 };
var clean = PreparedContourTests.Holes();
if (shape == "mixed-circles") clean.Codes.OfType<ArcMove>().Last().Rotation = RotationType.CW;
if (shape == "arcs")
{
clean = ExplicitContourTests.Square(false);
clean.MoveTo(6, 3);
clean.ArcTo(4, 3, 5, 3, RotationType.CCW);
clean.LineTo(6, 3);
}
var part = new Part(new Drawing("native", clean));
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], Start, confirmedParameters: settings));
var ready = CuttingPlanService.Plan(snapshot);
Assert.True(ready.Status == CuttingPlanStatus.Ready, string.Join("; ", ready.Findings.Select(f => f.Message)));
var source = snapshot.Placements[0];
var selected = ready.ProposedOrder[0];
if (shape != "arcs")
{
// Search may legitimately select cardinal candidates. Force off-angle
// choices to exercise rounding, right hole before left for clear departure.
var choices = new[] { source.Prepared.ClosestEntry(1, Start),
source.Prepared.ClosestEntry(0, Start), source.Prepared.ClosestEntry(2, Start) };
var explicitProgram = source.Prepared.Emit(choices);
selected = source.Propose(explicitProgram, Read(explicitProgram), choices);
Assert.Equal(CuttingPlanStatus.Ready,
CuttingPlanService.ReplayPrograms(snapshot, [selected], 0, default).Status);
var cuts = selected.Execution.Motions.Where(IsCut).Take(2).ToArray();
Assert.Contains(Enumerable.Range(0, 2), i => cuts[i].Start!.Value.DistanceTo(selected.ContourChoices[i].Point) > 0.1);
Assert.All(selected.Execution.Motions.Where(m => m.Layer == LayerType.Leadin).Take(2), m => Assert.True(m.Length < 2));
}
var program = Subdivide(selected.CopyProgram());
Assert.Equal(CuttingPlanStatus.Ready, CuttingPlanService.ReplayPrograms(snapshot,
[source.Propose(program, Read(program), selected.ContourChoices)], 0, default).Status);
}
[Fact]
public void SelectedContract_IsFrozenBeforeReplayAndNeverTrustsCachedExecution()
{
var (part, settings) = Fixture();
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], Start, confirmedParameters: settings));
var source = snapshot.Placements[0];
var choices = new[] { source.Prepared.ClosestEntry(0, Start) };
var program = source.Prepared.Emit(choices);
var cachedExecution = Read(program);
// Supply stale complete execution alongside actual partial code. This must
// refuse even when proposal storage defensively copies its input program.
program.Mode = Mode.Absolute;
program.Codes.RemoveRange(3, program.Codes.Count - 3);
var proposal = source.Propose(program, cachedExecution, choices);
((LineLeadIn)settings.ExternalLeadIn).Length = 900;
part.BaseDrawing.Program.Codes.Clear();
var replay = CuttingPlanService.ReplayPrograms(snapshot, [proposal], 0, default);
Assert.NotEqual(CuttingPlanStatus.Ready, replay.Status);
Assert.Empty(replay.ProposedOrder);
}
[Fact]
public void FixedReindexedMergedNativeIntervals_PreserveCompleteCoverageAndPayload()
{
var settings = ExplicitContourTests.Parameters();
var clean = ExplicitContourTests.Square(false);
clean.Codes.Insert(1, new LinearMove(0, 7));
var part = new Part(new Drawing("subdivided clean", clean));
var program = new Program();
program.MoveTo(5, 10.3);
program.Codes.Add(new LinearMove(5, 10) { Layer = LayerType.Leadin });
program.LineTo(10, 10); program.LineTo(10, 0); program.LineTo(0, 0);
program.LineTo(0, 10); program.LineTo(5, 10);
Assert.True(part.RestoreLeadInProgram(program, true));
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], new Vector(5, 12), confirmedParameters: settings));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.Equal(ExplicitContourTests.Fingerprint(program), ExplicitContourTests.Fingerprint(result.ProposedOrder[0].CopyProgram()));
}
private static (Part, CuttingParameters) Fixture()
{
var settings = ExplicitContourTests.Parameters();
var clean = ExplicitContourTests.Square(false);
var prepared = PreparedContours.Capture(clean, settings);
var part = new Part(new Drawing("square", clean));
Assert.True(part.RestoreLeadInProgram(prepared.Emit([prepared.ClosestEntry(0, Start)]), false));
return (part, settings);
}
private static Program Path(params Vector[] points)
{
var program = new Program();
program.MoveTo(-0.3, 5);
program.Codes.Add(new LinearMove(0, 5) { Layer = LayerType.Leadin });
foreach (var point in points) program.LineTo(point);
return program;
}
private static OwnedExecution Read(Program program) => ExecutionMotionReader.Read(program, Vector.Zero, null, default);
private static bool IsCut(ExecutionMotion motion) => !motion.Rapid && motion.Layer is LayerType.Cut or LayerType.Display;
private static Program Subdivide(Program program)
{
// Flatten executed frames through the existing native reader, not tessellation.
var copy = new Program();
foreach (var motion in Read(program).Motions)
{
if (motion.Rapid) { copy.MoveTo(motion.End); continue; }
var midpoint = motion.Curve.Midpoint;
if (motion.Curve.ToEntity() is Arc arc)
{
copy.Codes.Add(new ArcMove(midpoint, arc.Center, arc.Rotation) { Layer = motion.Layer });
copy.Codes.Add(new ArcMove(motion.End, arc.Center, arc.Rotation) { Layer = motion.Layer });
}
else if (motion.Curve.ToEntity() is Circle circle)
{
var clockwise = PostVerificationGeometry.Curve.Create(motion.Start!.Value, motion.End, circle.Center, true);
var original = motion.Curve.SameDirection(clockwise) ? RotationType.CW : RotationType.CCW;
copy.Codes.Add(new ArcMove(midpoint, circle.Center, original) { Layer = motion.Layer });
copy.Codes.Add(new ArcMove(motion.End, circle.Center, original) { Layer = motion.Layer });
}
else
{
copy.Codes.Add(new LinearMove(midpoint) { Layer = motion.Layer });
copy.Codes.Add(new LinearMove(motion.End) { Layer = motion.Layer });
}
}
return copy;
}
}
@@ -37,7 +37,8 @@ public class ReplayPoseBindingTests
&& BitConverter.DoubleToInt64Bits(source.Rotation) == BitConverter.DoubleToInt64Bits(rotation);
Assert.Equal(fault == "matching-bits", sameBits);
var selected = new FixedProgramPlacement(source.SourcePart, source.SourceOrdinal, new Vector(x, y), rotation,
source.LeadInsLocked, proposal.Execution, proposal.CopyProgram(), source.Prepared, source.Material, proposal.ContourChoices);
source.LeadInsLocked, proposal.Execution, proposal.CopyProgram(), source.Prepared, source.Material, proposal.ContourChoices)
.Propose(proposal.CopyProgram(), proposal.Execution, proposal.ContourChoices);
var replay = CuttingPlanService.ReplayPrograms(snapshot, [selected], 0, default);
Assert.Equal(sameBits ? CuttingPlanStatus.Ready : CuttingPlanStatus.InvalidInput, replay.Status);
Assert.Equal(sameBits, replay.IndependentlyReplayed);