mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 13:32:13 -04:00
feat(cutting): jointly plan contour order and entry points
This commit is contained in:
1 parent
018e4e3dd4
commit
70d886e4cf
5 files changed
+816
-13
No files matched your search
@@ -1,25 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// Caller-side input for a fixed-program, direct-XY route only. Keep all sources stable
|
||||
/// during Capture. Both locked and unlocked programs are fixed; this is not an Apply request.
|
||||
/// Caller-side direct-XY input. Regeneration requires explicit confirmed parameters.
|
||||
/// Keep sources/settings stable during Capture. This is not an Apply request.
|
||||
/// </summary>
|
||||
public sealed class CuttingPlanRequest
|
||||
{
|
||||
public CuttingPlanRequest(IEnumerable<Part> parts, Vector startPoint = default, int expansionBudget = 20000)
|
||||
public CuttingPlanRequest(IEnumerable<Part> parts, Vector startPoint = default, int expansionBudget = 20000,
|
||||
CuttingParameters confirmedParameters = null, IEnumerable<Part> eligibleParts = null,
|
||||
bool preservePartOrder = false, int maxEntries = 16)
|
||||
{
|
||||
Parts = parts == null ? null : Array.AsReadOnly(parts.ToArray());
|
||||
StartPoint = startPoint;
|
||||
ExpansionBudget = expansionBudget;
|
||||
ConfirmedParameters = confirmedParameters;
|
||||
EligibleParts = eligibleParts == null ? null : Array.AsReadOnly(eligibleParts.ToArray());
|
||||
PreservePartOrder = preservePartOrder;
|
||||
MaxEntries = maxEntries;
|
||||
}
|
||||
|
||||
public CuttingParameters ConfirmedParameters { get; }
|
||||
public IReadOnlyList<Part> EligibleParts { get; }
|
||||
public bool PreservePartOrder { get; }
|
||||
public int MaxEntries { get; }
|
||||
internal Action<int> ExpansionObserver { get; init; }
|
||||
public IReadOnlyList<Part> Parts { get; }
|
||||
public Vector StartPoint { get; }
|
||||
public int ExpansionBudget { get; }
|
||||
@@ -29,15 +42,25 @@ public sealed class CuttingPlanRequest
|
||||
public sealed class CuttingPlanSnapshot
|
||||
{
|
||||
internal CuttingPlanSnapshot(IEnumerable<FixedProgramPlacement> placements, Vector startPoint,
|
||||
int expansionBudget, CuttingPlanStatus? failure = null, IEnumerable<CuttingPlanFinding> findings = null)
|
||||
int expansionBudget, CuttingPlanStatus? failure = null, IEnumerable<CuttingPlanFinding> findings = null,
|
||||
bool regeneration = false, bool preservePartOrder = false, int maxEntries = 16,
|
||||
Action<int> expansionObserver = null)
|
||||
{
|
||||
Placements = Array.AsReadOnly(placements.ToArray());
|
||||
StartPoint = startPoint;
|
||||
ExpansionBudget = expansionBudget;
|
||||
Failure = failure;
|
||||
Findings = Array.AsReadOnly((findings ?? []).ToArray());
|
||||
Regeneration = regeneration;
|
||||
PreservePartOrder = preservePartOrder;
|
||||
MaxEntries = maxEntries;
|
||||
ExpansionObserver = expansionObserver;
|
||||
}
|
||||
|
||||
internal bool Regeneration { get; }
|
||||
internal bool PreservePartOrder { get; }
|
||||
internal int MaxEntries { get; }
|
||||
internal Action<int> ExpansionObserver { get; }
|
||||
public IReadOnlyList<FixedProgramPlacement> Placements { get; }
|
||||
public Vector StartPoint { get; }
|
||||
public int ExpansionBudget { get; }
|
||||
@@ -48,7 +71,9 @@ public sealed class CuttingPlanSnapshot
|
||||
public sealed class FixedProgramPlacement
|
||||
{
|
||||
internal FixedProgramPlacement(Part sourcePart, int sourceOrdinal, Vector location,
|
||||
double rotation, bool leadInsLocked, OwnedExecution execution)
|
||||
double rotation, bool leadInsLocked, OwnedExecution execution, Program program = null,
|
||||
PreparedContours prepared = null, LeadMaterialSnapshot material = null,
|
||||
IReadOnlyList<ContourChoice> choices = null)
|
||||
{
|
||||
SourcePart = sourcePart;
|
||||
SourceOrdinal = sourceOrdinal;
|
||||
@@ -56,8 +81,22 @@ public sealed class FixedProgramPlacement
|
||||
Rotation = rotation;
|
||||
LeadInsLocked = leadInsLocked;
|
||||
Execution = execution;
|
||||
this.program = program;
|
||||
Prepared = prepared;
|
||||
Material = material;
|
||||
ContourChoices = Array.AsReadOnly((choices ?? []).ToArray());
|
||||
}
|
||||
|
||||
private readonly Program program;
|
||||
internal PreparedContours Prepared { get; }
|
||||
internal LeadMaterialSnapshot Material { get; }
|
||||
public IReadOnlyList<ContourChoice> ContourChoices { get; }
|
||||
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 : (Program)program.Clone();
|
||||
internal FixedProgramPlacement Propose(Program proposed, OwnedExecution execution, IReadOnlyList<ContourChoice> choices) =>
|
||||
new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution, proposed, Prepared, Material, choices);
|
||||
|
||||
public Part SourcePart { get; }
|
||||
public int SourceOrdinal { get; }
|
||||
public Vector Location { get; }
|
||||
@@ -81,8 +120,8 @@ public sealed record CuttingPlanFinding(int? SourceOrdinal, Part SourcePart,
|
||||
int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message);
|
||||
|
||||
/// <summary>
|
||||
/// A checked fixed-program route, not full cutting-plan readiness, posting consent,
|
||||
/// physical safety, or an atomic Apply payload. Failures contain no proposed order.
|
||||
/// A replayed direct-XY proposal, optionally with regenerated programs. Not physical
|
||||
/// safety, posting consent, dependency readiness or an atomic Apply payload. Failures contain no proposals.
|
||||
/// </summary>
|
||||
public sealed class CuttingPlanResult
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
@@ -11,8 +12,8 @@ namespace OpenNest.Engine.CuttingPlanning;
|
||||
public static class CuttingPlanService
|
||||
{
|
||||
/// <summary>
|
||||
/// Read stable caller-owned sources once, before dispatching worker work. No Program.Clone,
|
||||
/// private Plates, settings aliases, quantity updates, or subcall rebinding are involved.
|
||||
/// Read stable caller-owned sources once into privately owned programs and geometry.
|
||||
/// No private Plates, settings aliases, quantity updates or source subcall rebinding.
|
||||
/// </summary>
|
||||
public static CuttingPlanSnapshot Capture(CuttingPlanRequest request, CancellationToken token = default)
|
||||
{
|
||||
@@ -22,8 +23,17 @@ public static class CuttingPlanService
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (request?.Parts == null || request.Parts.Count == 0 || request.ExpansionBudget <= 0)
|
||||
if (request?.Parts == null || request.Parts.Count == 0 || request.ExpansionBudget <= 0 || request.MaxEntries <= 0)
|
||||
throw new ArgumentException("A nonempty source list and positive expansion budget are required.");
|
||||
var eligible = new HashSet<Part>(ReferenceEqualityComparer.Instance);
|
||||
if (request.EligibleParts != null)
|
||||
{
|
||||
if (request.ConfirmedParameters == null)
|
||||
throw new ArgumentException("Eligibility requires confirmed cutting parameters.");
|
||||
foreach (var part in request.EligibleParts)
|
||||
if (part == null || !request.Parts.Any(p => ReferenceEquals(p, part)) || !eligible.Add(part))
|
||||
throw new ArgumentException("Foreign or duplicate eligible placement.");
|
||||
}
|
||||
// Reuse the reader's coordinate validation without publishing its native kernel.
|
||||
var identities = new HashSet<Part>(ReferenceEqualityComparer.Instance);
|
||||
for (var index = 0; index < request.Parts.Count; index++)
|
||||
@@ -42,11 +52,29 @@ public static class CuttingPlanService
|
||||
var execution = ExecutionMotionReader.Read(source.Program, source.Location, request.StartPoint, token);
|
||||
if (!execution.HasCuttingContour)
|
||||
throw new ArgumentException("Placed program has no nonzero cutting contour motions.");
|
||||
placements.Add(new(source, index, source.Location, source.Rotation, source.LeadInsLocked, execution));
|
||||
ValidateCloneGraph(source.Program, token);
|
||||
var ownedProgram = (Program)source.Program.Clone();
|
||||
PreparedContours prepared = null;
|
||||
LeadMaterialSnapshot material = null;
|
||||
if (request.ConfirmedParameters != null)
|
||||
{
|
||||
ValidateCloneGraph(source.BaseDrawing.Program, token);
|
||||
var ownedClean = (Program)source.BaseDrawing.Program.Clone();
|
||||
ownedClean.Rotate(source.Rotation - source.BaseDrawing.Program.Rotation);
|
||||
material = LeadMaterialSnapshot.Capture(ownedClean, source.Location, token);
|
||||
if (!material.IsComplete)
|
||||
throw new NotSupportedException(material.Reason);
|
||||
if (!source.LeadInsLocked && (request.EligibleParts == null || eligible.Contains(source)))
|
||||
prepared = PreparedContours.Capture(ownedClean, request.ConfirmedParameters, token);
|
||||
}
|
||||
placements.Add(new(source, index, source.Location, source.Rotation, source.LeadInsLocked,
|
||||
execution, ownedProgram, prepared, material));
|
||||
}
|
||||
// The start can be invalid even when the first rapid has no cutting geometry.
|
||||
placements[0].Execution.RapidDistanceFrom(request.StartPoint);
|
||||
return new(placements, request.StartPoint, request.ExpansionBudget);
|
||||
return new(placements, request.StartPoint, request.ExpansionBudget, regeneration: request.ConfirmedParameters != null,
|
||||
preservePartOrder: request.PreservePartOrder, maxEntries: request.MaxEntries,
|
||||
expansionObserver: request.ExpansionObserver);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -84,6 +112,8 @@ public static class CuttingPlanService
|
||||
foreach (var placement in snapshot.Placements)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (placement.Prepared != null)
|
||||
continue; // An eligible old crossing is precisely what regeneration may repair.
|
||||
// Ignore only the unknown incoming rapid. Every fixed internal motion is checked.
|
||||
var findings = new ReleasedContourState().Check(placement.Execution, null,
|
||||
placement.SourceOrdinal + 1, token);
|
||||
@@ -94,6 +124,13 @@ public static class CuttingPlanService
|
||||
? CuttingPlanStatus.UnsupportedGeometry : CuttingPlanStatus.ConstraintConflict,
|
||||
findings: fixedFindings);
|
||||
|
||||
if (snapshot.Regeneration)
|
||||
{
|
||||
var joint = JointCuttingPlanSearch.Run(snapshot, token);
|
||||
if (joint.Status != CuttingPlanStatus.Ready)
|
||||
return new(joint.Status, findings: joint.Findings, expansions: joint.Expansions);
|
||||
return ReplayPrograms(snapshot, joint.Order, joint.Expansions, token);
|
||||
}
|
||||
var search = FixedProgramSearch.Run(snapshot, token);
|
||||
if (search.Status != CuttingPlanStatus.Ready)
|
||||
return new(search.Status, findings: Map(snapshot, search.Findings), expansions: search.Expansions);
|
||||
@@ -133,6 +170,129 @@ public static class CuttingPlanService
|
||||
expansions: expansions, rapidDistance: distance, independentlyReplayed: true);
|
||||
}
|
||||
|
||||
// Re-read EXACT selected programs with a fresh checker and native lead validation.
|
||||
// No emission/regeneration or cached branch verdict is used here.
|
||||
internal static CuttingPlanResult ReplayPrograms(CuttingPlanSnapshot snapshot,
|
||||
IReadOnlyList<FixedProgramPlacement> order, int expansions, CancellationToken token)
|
||||
{
|
||||
if (order == null || order.Count != snapshot.Placements.Count
|
||||
|| order.Any(p => p == null || p.SourceOrdinal < 0 || p.SourceOrdinal >= snapshot.Placements.Count)
|
||||
|| order.Select(p => p.SourceOrdinal).Distinct().Count() != order.Count)
|
||||
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
|
||||
var checker = new ReleasedContourState();
|
||||
var position = snapshot.StartPoint;
|
||||
var distance = 0.0;
|
||||
var findings = new List<CuttingPlanFinding>();
|
||||
var materials = snapshot.Placements.Select(p => p.Material).ToArray();
|
||||
foreach (var proposal in order)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var source = snapshot.Placements[proposal.SourceOrdinal];
|
||||
if (!ReferenceEquals(source.SourcePart, proposal.SourcePart) || source.Location != proposal.Location
|
||||
|| source.Rotation != proposal.Rotation || source.LeadInsLocked != proposal.LeadInsLocked
|
||||
|| !ReferenceEquals(source.Prepared, proposal.Prepared) || !ReferenceEquals(source.Material, proposal.Material)
|
||||
|| snapshot.PreservePartOrder && proposal.SourceOrdinal != order.TakeWhile(p => !ReferenceEquals(p, proposal)).Count())
|
||||
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
|
||||
if (source.Prepared == null)
|
||||
{
|
||||
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)
|
||||
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
|
||||
OwnedExecution execution;
|
||||
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);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
|
||||
{
|
||||
return new(CuttingPlanStatus.UnsupportedGeometry,
|
||||
findings: [JointCuttingPlanSearch.Finding(source, PostVerificationKind.Incomplete, ex.Message)], expansions: expansions);
|
||||
}
|
||||
findings.AddRange(JointCuttingPlanSearch.Map(snapshot,
|
||||
checker.Check(execution, position, source.SourceOrdinal + 1, token)));
|
||||
var lead = LeadPathValidator.Check(execution, source.Material, materials, token);
|
||||
if (!lead.IsComplete || !lead.IsClear)
|
||||
findings.Add(JointCuttingPlanSearch.Finding(source,
|
||||
lead.IsComplete ? null : PostVerificationKind.Incomplete, lead.Reason));
|
||||
distance += execution.RapidDistanceFrom(position);
|
||||
position = execution.DeparturePoint;
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (findings.Count != 0)
|
||||
return new(findings.Any(f => f.Kind == PostVerificationKind.Incomplete)
|
||||
? CuttingPlanStatus.UnsupportedGeometry : CuttingPlanStatus.ConstraintConflict,
|
||||
findings: findings, expansions: expansions);
|
||||
return new(CuttingPlanStatus.Ready, order, expansions: expansions,
|
||||
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 void ValidateCloneGraph(Program program, CancellationToken token)
|
||||
{
|
||||
var active = new HashSet<Program>(ReferenceEqualityComparer.Instance);
|
||||
var done = new HashSet<Program>(ReferenceEqualityComparer.Instance);
|
||||
Visit(program);
|
||||
void Visit(Program current)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (current == null || active.Contains(current) || active.Count >= 64)
|
||||
throw new ArgumentException("Missing, recursive or excessively nested clone graph.");
|
||||
if (done.Contains(current)) return;
|
||||
// The reader already validated the executed graph. Inactive registered
|
||||
// subprograms may be motionless; preserve them exactly, but guard Clone traversal.
|
||||
if (current.Codes == null || current.Codes.Any(c => c == null))
|
||||
throw new ArgumentException("Missing clone graph instructions.");
|
||||
active.Add(current);
|
||||
foreach (var child in current.SubPrograms.Values.Concat(current.Codes.OfType<SubProgramCall>().Select(c => c.Program)))
|
||||
Visit(child);
|
||||
active.Remove(current);
|
||||
done.Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CuttingPlanFinding> Map(CuttingPlanSnapshot snapshot,
|
||||
IEnumerable<PostVerificationFinding> findings) => findings.Select(finding =>
|
||||
{
|
||||
|
||||
@@ -47,7 +47,8 @@ internal static class FixedProgramSearch
|
||||
return new(CuttingPlanStatus.ConstraintConflict, [], rejected.ToArray(), expansions);
|
||||
|
||||
Frame Create(int[] order, Vector position, ReleasedContourState checker) => new(order, position, checker,
|
||||
Enumerable.Range(0, snapshot.Placements.Count).Where(index => !order.Contains(index))
|
||||
Enumerable.Range(0, snapshot.Placements.Count).Where(index => !order.Contains(index)
|
||||
&& (!snapshot.PreservePartOrder || index == order.Length))
|
||||
.OrderBy(index => snapshot.Placements[index].Execution.RapidDistanceFrom(position))
|
||||
.ThenBy(index => snapshot.Placements[index].SourceOrdinal).ToArray());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>One forward DFS over whole-part selection and emitted contour prefixes.</summary>
|
||||
internal static class JointCuttingPlanSearch
|
||||
{
|
||||
internal sealed record Outcome(CuttingPlanStatus Status, IReadOnlyList<FixedProgramPlacement> Order,
|
||||
IReadOnlyList<CuttingPlanFinding> Findings, int Expansions);
|
||||
|
||||
internal static Outcome Run(CuttingPlanSnapshot snapshot, CancellationToken token)
|
||||
{
|
||||
var expansions = 0;
|
||||
var rejected = new List<CuttingPlanFinding>();
|
||||
var materials = snapshot.Placements.Select(p => p.Material).ToArray();
|
||||
var stack = new Stack<Frame>();
|
||||
stack.Push(new(new([], snapshot.StartPoint, new ReleasedContourState(), null)));
|
||||
try
|
||||
{
|
||||
while (stack.Count != 0)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var frame = stack.Peek();
|
||||
var node = frame.Node;
|
||||
if (node.Order.Length == snapshot.Placements.Count)
|
||||
return new(CuttingPlanStatus.Ready, node.Order, [], expansions);
|
||||
frame.Children ??= Expand(node).OrderBy(c => c.Distance)
|
||||
.ThenBy(c => c.Ordinal).ThenBy(c => c.Contour).ThenBy(c => c.Entry).ToArray();
|
||||
if (frame.Next == frame.Children.Length)
|
||||
{
|
||||
stack.Pop();
|
||||
continue;
|
||||
}
|
||||
stack.Push(new(frame.Children[frame.Next++].Node));
|
||||
}
|
||||
// Entries are capped; exhaustion is not a proof over all possible entries.
|
||||
var status = snapshot.Placements.Any(p => p.Prepared != null)
|
||||
? CuttingPlanStatus.NoSolutionWithinBudget
|
||||
: rejected.Any(f => f.Kind == PostVerificationKind.Incomplete)
|
||||
? CuttingPlanStatus.UnsupportedGeometry : CuttingPlanStatus.ConstraintConflict;
|
||||
return new(status, [], rejected.Distinct().ToArray(), expansions);
|
||||
}
|
||||
catch (BudgetExceededException)
|
||||
{
|
||||
return new(CuttingPlanStatus.NoSolutionWithinBudget, [], rejected.Distinct().ToArray(), expansions);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new(CuttingPlanStatus.Cancelled, [], [], expansions);
|
||||
}
|
||||
|
||||
IEnumerable<Edge> Expand(Node node)
|
||||
{
|
||||
var sources = node.Active is { } active ? new[] { active.Source }
|
||||
: snapshot.Placements.Where(p => !node.Order.Any(o => o.SourceOrdinal == p.SourceOrdinal)
|
||||
&& (!snapshot.PreservePartOrder || p.SourceOrdinal == node.Order.Length));
|
||||
foreach (var source in sources)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (source.Prepared == null)
|
||||
{
|
||||
CountExpansion(source);
|
||||
var checker = node.Checker.Copy();
|
||||
if (Check(source, source.Execution, node.Position, checker))
|
||||
yield return new(new([.. node.Order, source], source.Execution.DeparturePoint, checker, null),
|
||||
source.Execution.RapidDistanceFrom(node.Position), source.SourceOrdinal, -1, -1);
|
||||
continue;
|
||||
}
|
||||
var prepared = source.Prepared;
|
||||
var choices = node.Active?.Choices ?? [];
|
||||
var arrival = node.Active?.Arrival ?? node.Position;
|
||||
var before = node.Active?.Before ?? node.Checker;
|
||||
var contours = choices.Length == prepared.Count - 1 ? new[] { prepared.PerimeterOrdinal }
|
||||
: Enumerable.Range(0, prepared.PerimeterOrdinal).Where(c => !choices.Any(e => e.ContourOrdinal == c));
|
||||
foreach (var contour in contours)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var entries = prepared.Entries(contour, node.Position - source.Location, snapshot.MaxEntries, token);
|
||||
for (var entry = 0; entry < entries.Count; entry++)
|
||||
{
|
||||
CountExpansion(source); // Before emission/native queries, including rejected candidates.
|
||||
var prefix = choices.Append(entries[entry]).ToArray();
|
||||
Program program;
|
||||
OwnedExecution execution;
|
||||
try
|
||||
{
|
||||
program = prefix.Length == prepared.Count ? prepared.Emit(prefix) : prepared.EmitPrefix(prefix);
|
||||
execution = ExecutionMotionReader.Read(program, source.Location, arrival, token);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or ArithmeticException or NotSupportedException)
|
||||
{
|
||||
rejected.Add(Finding(source, PostVerificationKind.Incomplete,
|
||||
$"Contour {contour}, entry {entry} emission refused: {ex.Message}"));
|
||||
continue; // Retain emitter semantics, never repair/shorten invalid lead styles.
|
||||
}
|
||||
// Prefix includes all earlier cuts and scribes. Replay from BEFORE the whole part.
|
||||
var checker = before.Copy();
|
||||
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, execution.DeparturePoint, checker,
|
||||
new(source, prefix, arrival, before, distance));
|
||||
yield return new(next, distance - (node.Active?.Distance ?? 0), source.SourceOrdinal, contour, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CountExpansion(FixedProgramPlacement source)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (expansions == snapshot.ExpansionBudget)
|
||||
{
|
||||
rejected.Add(Finding(source, null, $"Expansion budget {snapshot.ExpansionBudget} reached before the next candidate."));
|
||||
throw new BudgetExceededException();
|
||||
}
|
||||
expansions++;
|
||||
snapshot.ExpansionObserver?.Invoke(expansions);
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
bool Check(FixedProgramPlacement source, OwnedExecution execution, Vector arrival, ReleasedContourState checker)
|
||||
{
|
||||
var findings = checker.Check(execution, arrival, source.SourceOrdinal + 1, token);
|
||||
rejected.AddRange(Map(snapshot, findings));
|
||||
var lead = LeadPathValidator.Check(execution, source.Material, materials, token);
|
||||
if (!lead.IsComplete || !lead.IsClear)
|
||||
rejected.Add(Finding(source, lead.IsComplete ? null : PostVerificationKind.Incomplete, lead.Reason));
|
||||
return findings.Count == 0 && lead.IsComplete && lead.IsClear;
|
||||
}
|
||||
}
|
||||
|
||||
internal static CuttingPlanFinding Finding(FixedProgramPlacement source, PostVerificationKind? kind, string message) =>
|
||||
new(source.SourceOrdinal, source.SourcePart, null, null, kind, message);
|
||||
|
||||
internal static IEnumerable<CuttingPlanFinding> Map(CuttingPlanSnapshot snapshot, IEnumerable<PostVerificationFinding> findings) =>
|
||||
findings.Select(f =>
|
||||
{
|
||||
var source = f.PartNumber is { } p ? snapshot.Placements[p - 1] : null;
|
||||
var other = f.OtherPartNumber is { } o ? snapshot.Placements[o - 1] : null;
|
||||
return new CuttingPlanFinding(source?.SourceOrdinal, source?.SourcePart,
|
||||
other?.SourceOrdinal, other?.SourcePart, f.Kind, f.Message);
|
||||
});
|
||||
|
||||
private sealed class BudgetExceededException : Exception;
|
||||
private sealed record ActivePart(FixedProgramPlacement Source, ContourChoice[] Choices, Vector Arrival,
|
||||
ReleasedContourState Before, double Distance);
|
||||
private sealed record Node(FixedProgramPlacement[] Order, Vector Position, ReleasedContourState Checker, ActivePart Active);
|
||||
private sealed record Edge(Node Node, double Distance, int Ordinal, int Contour, int Entry);
|
||||
private sealed class Frame(Node node)
|
||||
{
|
||||
internal Node Node { get; } = node;
|
||||
internal Edge[] Children { get; set; }
|
||||
internal int Next { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
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 JointCuttingPlanTests
|
||||
{
|
||||
[Fact]
|
||||
public void JointSearch_RepairsActualCompletedHoleCrossing_WithExactReplayAndNoMutation()
|
||||
{
|
||||
var (part, parameters) = Crossing();
|
||||
var unchanged = Unchanged(part, parameters);
|
||||
Assert.Contains(new ReleasedContourState().Check(Read(part.Program, part.Location), Vector.Zero, 1),
|
||||
f => f.Kind == PostVerificationKind.RapidCrossing);
|
||||
Assert.Equal(CuttingPlanStatus.ConstraintConflict,
|
||||
CuttingPlanService.Plan(new CuttingPlanRequest([part])).Status);
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], confirmedParameters: parameters));
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
var proposal = Assert.Single(result.ProposedOrder);
|
||||
Assert.True(proposal.IsRegenerated);
|
||||
Assert.Equal(new[] { 0, 1, 2 }, proposal.ContourChoices.Select(c => c.ContourOrdinal).Order());
|
||||
Assert.Equal(2, proposal.ContourChoices[^1].ContourOrdinal);
|
||||
Assert.NotEqual(ExplicitContourTests.Fingerprint(part.Program), ExplicitContourTests.Fingerprint(proposal.CopyProgram()));
|
||||
var actual = Read(proposal.CopyProgram(), proposal.Location);
|
||||
Assert.Empty(new ReleasedContourState().Check(actual, Vector.Zero, 1));
|
||||
var material = LeadMaterialSnapshot.Capture(part.BaseDrawing.Program, part.Location);
|
||||
var leads = LeadPathValidator.Check(actual, material, []);
|
||||
Assert.True(leads.IsComplete && leads.IsClear, leads.Reason);
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
var copy = new Part(new Drawing("replay", (Program)part.BaseDrawing.Program.Clone()), proposal.Location);
|
||||
Assert.True(copy.RestoreLeadInProgram(proposal.CopyProgram(), false));
|
||||
plate.Parts.Add(copy);
|
||||
var report = PostVerificationAnalyzer.Analyze(nest, Vector.Zero);
|
||||
Assert.Empty(report.Findings);
|
||||
var detached = proposal.CopyProgram(); detached.Codes.Clear(); detached.SubPrograms.Clear();
|
||||
Assert.NotEmpty(proposal.CopyProgram().Codes);
|
||||
unchanged();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("locked")]
|
||||
[InlineData("ineligible")]
|
||||
[InlineData("no-valid-entry")]
|
||||
[InlineData("unsupported")]
|
||||
public void JointSearch_RefusesWithoutInstallingFallbackOrMutating(string fault)
|
||||
{
|
||||
var (part, parameters) = Crossing();
|
||||
Part[]? eligible = null;
|
||||
var expected = CuttingPlanStatus.ConstraintConflict;
|
||||
if (fault == "locked") part.LeadInsLocked = true;
|
||||
if (fault == "ineligible") eligible = [];
|
||||
if (fault == "no-valid-entry")
|
||||
{
|
||||
parameters.ArcCircleLeadIn = new NoLeadIn();
|
||||
parameters.InternalLeadIn = parameters.ArcCircleLeadIn;
|
||||
expected = CuttingPlanStatus.NoSolutionWithinBudget;
|
||||
}
|
||||
if (fault == "unsupported")
|
||||
{
|
||||
part.BaseDrawing.Program.Codes.AddRange(LeadPathValidationTests.Rectangle(30, 30, 40, 40).Codes);
|
||||
expected = CuttingPlanStatus.UnsupportedGeometry;
|
||||
}
|
||||
var unchanged = Unchanged(part, parameters);
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], confirmedParameters: parameters, eligibleParts: eligible));
|
||||
Assert.Equal(expected, result.Status);
|
||||
Assert.Empty(result.ProposedOrder);
|
||||
Assert.False(result.IndependentlyReplayed);
|
||||
unchanged();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JointSearch_BacktracksInternalEntryWhenLaterWholePartFails()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var first = SimplePart(Vector.Zero, parameters);
|
||||
var second = SimplePart(new Vector(20, 0), parameters);
|
||||
second.LeadInsLocked = true;
|
||||
var start = new Vector(-2, 5);
|
||||
var solo = CuttingPlanService.Plan(new CuttingPlanRequest([first], start, confirmedParameters: parameters));
|
||||
Assert.Equal(CuttingPlanStatus.Ready, solo.Status);
|
||||
var fixedLater = CuttingPlanService.Capture(new CuttingPlanRequest([second])).Placements[0];
|
||||
var state = new ReleasedContourState();
|
||||
Assert.Empty(state.Check(solo.ProposedOrder[0].Execution, start, 1));
|
||||
Assert.Contains(state.Check(fixedLater.Execution, solo.ProposedOrder[0].Execution.DeparturePoint, 2),
|
||||
f => f.Kind == PostVerificationKind.RapidCrossing);
|
||||
var joint = CuttingPlanService.Plan(new CuttingPlanRequest([first, second], start,
|
||||
confirmedParameters: parameters, preservePartOrder: true));
|
||||
Assert.True(joint.Status == CuttingPlanStatus.Ready, Describe(joint));
|
||||
Assert.Equal(new[] { first, second }, joint.ProposedOrder.Select(p => p.SourcePart));
|
||||
Assert.NotEqual(solo.ProposedOrder[0].ContourChoices[0].Point, joint.ProposedOrder[0].ContourChoices[0].Point);
|
||||
Assert.True(joint.IndependentlyReplayed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_CallerMutationAndDuplicateNames_DoNotChangeDeterministicOwnedProposals()
|
||||
{
|
||||
var (first, parameters) = Crossing();
|
||||
var (second, _) = Crossing();
|
||||
second.Location = new Vector(30, 0);
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([first, second], confirmedParameters: parameters));
|
||||
var baseline = CuttingPlanService.Plan(snapshot);
|
||||
Assert.True(baseline.Status == CuttingPlanStatus.Ready, Describe(baseline));
|
||||
var fingerprints = baseline.ProposedOrder.Select(p => ExplicitContourTests.Fingerprint(p.CopyProgram())).ToArray();
|
||||
foreach (var part in new[] { first, second })
|
||||
{
|
||||
foreach (var call in part.Program.Codes.OfType<SubProgramCall>()) call.Program.Codes.Clear();
|
||||
part.Program.Codes.Clear(); part.BaseDrawing.Program.Codes.Clear();
|
||||
part.Location = new Vector(999, 999); part.LeadInsLocked = true;
|
||||
part.CuttingParameters = new CuttingParameters();
|
||||
}
|
||||
((LineLeadIn)parameters.ExternalLeadIn).Length = 900;
|
||||
parameters.TabsEnabled = true;
|
||||
for (var repeat = 0; repeat < 3; repeat++)
|
||||
{
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
||||
Assert.Equal(baseline.Expansions, result.Expansions);
|
||||
Assert.Equal(baseline.RapidDistance, result.RapidDistance);
|
||||
Assert.Equal(baseline.ProposedOrder.Select(p => p.SourcePart), result.ProposedOrder.Select(p => p.SourcePart));
|
||||
Assert.Equal(fingerprints, result.ProposedOrder.Select(p => ExplicitContourTests.Fingerprint(p.CopyProgram())));
|
||||
}
|
||||
Assert.Equal(new[] { first, second }, baseline.ProposedOrder.Select(p => p.SourcePart));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BudgetOneAndCancellationDuringExpansion_HaveExactCountsNoFallbackNoMutation()
|
||||
{
|
||||
var (part, parameters) = Crossing();
|
||||
var unchanged = Unchanged(part, parameters);
|
||||
var observed = 0;
|
||||
var budget = CuttingPlanService.Plan(new CuttingPlanRequest([part], expansionBudget: 1,
|
||||
confirmedParameters: parameters)
|
||||
{ ExpansionObserver = n => observed = n });
|
||||
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, budget.Status);
|
||||
Assert.Equal(1, budget.Expansions); Assert.Equal(1, observed); Assert.Empty(budget.ProposedOrder);
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
var cancelled = CuttingPlanService.Plan(new CuttingPlanRequest([part], confirmedParameters: parameters)
|
||||
{
|
||||
ExpansionObserver = n => { if (n == 3) cancellation.Cancel(); }
|
||||
}, cancellation.Token);
|
||||
Assert.Equal(CuttingPlanStatus.Cancelled, cancelled.Status);
|
||||
Assert.Equal(3, cancelled.Expansions); Assert.Empty(cancelled.ProposedOrder);
|
||||
unchanged();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("foreign")]
|
||||
[InlineData("duplicate")]
|
||||
[InlineData("without-settings")]
|
||||
[InlineData("zero-cap")]
|
||||
public void EligibilityAndEntryBounds_InvalidInputIsNotIgnored(string fault)
|
||||
{
|
||||
var (part, parameters) = Crossing();
|
||||
var other = SimplePart(new Vector(30, 0), parameters);
|
||||
var request = new CuttingPlanRequest([part], confirmedParameters: fault == "without-settings" ? null : parameters,
|
||||
eligibleParts: fault == "foreign" ? [other] : fault == "duplicate" ? [part, part] : [part],
|
||||
maxEntries: fault == "zero-cap" ? 0 : 16);
|
||||
Assert.Equal(CuttingPlanStatus.InvalidInput, CuttingPlanService.Plan(request).Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.0, 1.5707963267948966)]
|
||||
[InlineData(0.37, 0.91)]
|
||||
public void RotatedCapture_UsesDeltaFromAlreadyRotatedBase_AndLocationOnce(double baseAngle, double delta)
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var clean = PreparedContourTests.Holes(); clean.Rotate(baseAngle);
|
||||
var part = new Part(new Drawing("rotated", clean)); part.Rotate(delta);
|
||||
part.Location = new Vector(30, 40);
|
||||
var expected = (Program)clean.Clone(); expected.Rotate(delta);
|
||||
var target = LeadMaterialSnapshot.Capture(expected, part.Location);
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], new Vector(20, 30), confirmedParameters: parameters));
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
var proposal = Assert.Single(result.ProposedOrder);
|
||||
Assert.Equal(part.Rotation, proposal.Rotation);
|
||||
Assert.Equal(part.Location, proposal.Location);
|
||||
var lead = LeadPathValidator.Check(Read(proposal.CopyProgram(), part.Location), target, []);
|
||||
Assert.True(lead.IsComplete && lead.IsClear, lead.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratedLeadoutDeparture_IsUsedForArrivalAndReplayDistance()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
parameters.ExternalLeadOut = new LineLeadOut { Length = 0.2, ApproachAngle = 45 };
|
||||
var parts = new[] { SimplePart(Vector.Zero, parameters), SimplePart(new Vector(20, 0), parameters) };
|
||||
var start = new Vector(-2, 5);
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest(parts, start,
|
||||
confirmedParameters: parameters, preservePartOrder: true));
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
var distance = 0.0; var arrival = start;
|
||||
foreach (var p in result.ProposedOrder)
|
||||
{
|
||||
var actual = Read(p.CopyProgram(), p.Location);
|
||||
Assert.Equal(LayerType.Leadout, actual.Motions[^1].Layer);
|
||||
Assert.NotEqual(actual.Motions[^1].Start, actual.DeparturePoint);
|
||||
Assert.Equal(actual.DeparturePoint, p.Execution.DeparturePoint);
|
||||
distance += actual.RapidDistanceFrom(arrival); arrival = actual.DeparturePoint;
|
||||
}
|
||||
Assert.Equal(distance, result.RapidDistance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("unsafe")]
|
||||
[InlineData("duplicate-choice")]
|
||||
[InlineData("omitted-contour")]
|
||||
[InlineData("duplicate-placement")]
|
||||
[InlineData("omitted-placement")]
|
||||
[InlineData("wrong-pose")]
|
||||
public void ExactReplay_RejectsUnsafeOrIncompleteSelectedPrograms(string fault)
|
||||
{
|
||||
var (part, parameters) = Crossing();
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], confirmedParameters: parameters));
|
||||
var ready = CuttingPlanService.Plan(snapshot);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, ready.Status);
|
||||
var proposal = ready.ProposedOrder[0];
|
||||
var source = snapshot.Placements[0];
|
||||
IReadOnlyList<FixedProgramPlacement> order = [proposal];
|
||||
if (fault == "unsafe") order = [source.Propose((Program)part.Program.Clone(), Read(part.Program, part.Location), proposal.ContourChoices)];
|
||||
if (fault == "duplicate-choice") order = [source.Propose(proposal.CopyProgram(), proposal.Execution,
|
||||
[proposal.ContourChoices[0], proposal.ContourChoices[0], proposal.ContourChoices[2]])];
|
||||
if (fault == "omitted-contour")
|
||||
{
|
||||
var prefix = source.Prepared.EmitPrefix(proposal.ContourChoices.Take(2).ToArray());
|
||||
order = [source.Propose(prefix, Read(prefix, source.Location), proposal.ContourChoices)];
|
||||
}
|
||||
if (fault == "duplicate-placement") order = [proposal, proposal];
|
||||
if (fault == "omitted-placement") order = [];
|
||||
if (fault == "wrong-pose") order = [new FixedProgramPlacement(part, 0, new Vector(99, 99), source.Rotation,
|
||||
false, proposal.Execution, proposal.CopyProgram(), source.Prepared, source.Material, proposal.ContourChoices)];
|
||||
var replay = CuttingPlanService.ReplayPrograms(snapshot, order, 0, default);
|
||||
Assert.Equal(fault == "unsafe" ? CuttingPlanStatus.ConstraintConflict : CuttingPlanStatus.InvalidInput, replay.Status);
|
||||
Assert.Empty(replay.ProposedOrder); Assert.False(replay.IndependentlyReplayed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixedLeads_SearchAndReplayRejectNativeTargetMaterialCrossing()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var clean = LeadPathValidationTests.Rectangle(0, 0, 10, 10);
|
||||
var part = new Part(new Drawing("fixed", clean));
|
||||
var unsafeProgram = new Program(); unsafeProgram.MoveTo(5, 5);
|
||||
unsafeProgram.Codes.Add(new LinearMove(0, 5) { Layer = LayerType.Leadin });
|
||||
unsafeProgram.LineTo(0, 10); unsafeProgram.LineTo(10, 10); unsafeProgram.LineTo(10, 0);
|
||||
unsafeProgram.LineTo(0, 0); unsafeProgram.LineTo(0, 5);
|
||||
Assert.True(part.RestoreLeadInProgram(unsafeProgram, true));
|
||||
Assert.Empty(new ReleasedContourState().Check(Read(unsafeProgram, Vector.Zero), Vector.Zero, 1));
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part], confirmedParameters: parameters));
|
||||
var search = JointCuttingPlanSearch.Run(snapshot, default);
|
||||
Assert.Equal(CuttingPlanStatus.ConstraintConflict, search.Status);
|
||||
Assert.Empty(search.Order);
|
||||
var replay = CuttingPlanService.ReplayPrograms(snapshot, snapshot.Placements, 0, default);
|
||||
Assert.Equal(CuttingPlanStatus.ConstraintConflict, replay.Status);
|
||||
Assert.Contains(replay.Findings, f => f.Message.Contains("target material"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTabbedArcCandidate_IsRefusedWithoutRepairOrException()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
parameters.TabsEnabled = true; parameters.TabConfig = new NormalTab { Size = 0.2 };
|
||||
parameters.ExternalLeadOut = new ArcLeadOut { Radius = 0.2 };
|
||||
var part = new Part(new Drawing("tabbed", LeadPathValidationTests.Rectangle(0, 0, 10, 10)));
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], new Vector(-2, 5), confirmedParameters: parameters));
|
||||
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, result.Status);
|
||||
Assert.Empty(result.ProposedOrder); Assert.True(result.Expansions > 0);
|
||||
Assert.Contains(result.Findings, f => f.Kind == PostVerificationKind.Incomplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixedLead_SearchChecksOtherUncutHoleAwareMaterial_NotOnlyReleasedContours()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var first = SimplePart(Vector.Zero, parameters); first.LeadInsLocked = true;
|
||||
var clean = LeadPathValidationTests.Rectangle(-0.3, 4.5, -0.1, 5.5);
|
||||
var obstacle = new Part(new Drawing("uncut", clean));
|
||||
var prepared = PreparedContours.Capture(clean, parameters);
|
||||
Assert.True(obstacle.RestoreLeadInProgram(prepared.Emit([prepared.ClosestEntry(0, new Vector(-1, 5))]), true));
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([first, obstacle],
|
||||
new Vector(-2, 5), confirmedParameters: parameters, preservePartOrder: true));
|
||||
var direct = LeadPathValidator.Check(snapshot.Placements[0].Execution, snapshot.Placements[0].Material,
|
||||
snapshot.Placements.Select(p => p.Material).ToArray());
|
||||
Assert.True(direct.IsComplete); Assert.False(direct.IsClear);
|
||||
Assert.Contains("another placed material", direct.Reason);
|
||||
var search = JointCuttingPlanSearch.Run(snapshot, default);
|
||||
Assert.Equal(CuttingPlanStatus.ConstraintConflict, search.Status);
|
||||
Assert.Contains(search.Findings, f => f.Message.Contains("another placed material"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnsupportedMaterialOfLockedIneligiblePlacement_BlocksCompleteRegeneration()
|
||||
{
|
||||
var (part, parameters) = Crossing();
|
||||
var locked = SimplePart(new Vector(30, 0), parameters); locked.LeadInsLocked = true;
|
||||
locked.BaseDrawing.Program.Codes.RemoveAt(locked.BaseDrawing.Program.Codes.Count - 1);
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part, locked], confirmedParameters: parameters,
|
||||
eligibleParts: [part]));
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
|
||||
Assert.Empty(result.ProposedOrder);
|
||||
Assert.Contains(result.Findings, f => ReferenceEquals(f.SourcePart, locked) && f.SourceOrdinal == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JointSearch_SelectsWholePartOrderAndCanPreserveCallerOrder()
|
||||
{
|
||||
var (first, parameters) = Crossing(); first.Location = new Vector(30, 0);
|
||||
var (second, _) = Crossing();
|
||||
var free = CuttingPlanService.Plan(new CuttingPlanRequest([first, second], confirmedParameters: parameters));
|
||||
var preserved = CuttingPlanService.Plan(new CuttingPlanRequest([first, second], confirmedParameters: parameters,
|
||||
preservePartOrder: true));
|
||||
Assert.True(free.Status == CuttingPlanStatus.Ready, Describe(free));
|
||||
Assert.True(preserved.Status == CuttingPlanStatus.Ready, Describe(preserved));
|
||||
Assert.Equal(new[] { second, first }, free.ProposedOrder.Select(p => p.SourcePart));
|
||||
Assert.Equal(new[] { first, second }, preserved.ProposedOrder.Select(p => p.SourcePart));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixedProgramCopy_PreservesSignedSubcallsCommentsAndVariables_WithoutMutableAliases()
|
||||
{
|
||||
var (part, _) = Crossing();
|
||||
part.Program.Codes.Insert(0, new Comment("fixed text"));
|
||||
part.Program.Variables["value"] = new VariableDefinition("value", "2+3", 5);
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([part]));
|
||||
var placement = snapshot.Placements[0];
|
||||
var copy = placement.CopyProgram();
|
||||
Assert.Equal(part.Program.ToString(), copy.ToString());
|
||||
Assert.Equal(part.Program.Variables["value"].Expression, copy.Variables["value"].Expression);
|
||||
Assert.Equal(part.Program.Codes.OfType<SubProgramCall>().Select(c => (c.Id, c.Offset, c.Rotation)),
|
||||
copy.Codes.OfType<SubProgramCall>().Select(c => (c.Id, c.Offset, c.Rotation)));
|
||||
Assert.Equal(part.Program.Codes.OfType<Comment>().Select(c => c.ToString()), copy.Codes.OfType<Comment>().Select(c => c.ToString()));
|
||||
foreach (var call in copy.Codes.OfType<SubProgramCall>()) call.Program.Codes.Clear();
|
||||
copy.Codes.Clear(); copy.Variables.Clear();
|
||||
Assert.NotEmpty(placement.CopyProgram().Codes);
|
||||
Assert.NotEmpty(part.Program.Codes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Regeneration_CompletesOriginallyOmittedHoles_AndPrefixesKeepScribesOnce()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var clean = PreparedContourTests.Holes(); clean.MoveTo(-2, -2);
|
||||
clean.Codes.Add(new LinearMove(-1, -1) { Layer = LayerType.Scribe });
|
||||
var part = new Part(new Drawing("incomplete old program", clean));
|
||||
var original = SimplePart(Vector.Zero, parameters).Program;
|
||||
Assert.True(part.RestoreLeadInProgram((Program)original.Clone(), false));
|
||||
part.CuttingParameters = parameters;
|
||||
var unchanged = Unchanged(part, parameters);
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part], confirmedParameters: parameters));
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
var proposal = Assert.Single(result.ProposedOrder);
|
||||
Assert.Equal(new[] { 0, 1, 2 }, proposal.ContourChoices.Select(c => c.ContourOrdinal).Order());
|
||||
var actual = Read(proposal.CopyProgram(), Vector.Zero);
|
||||
Assert.Single(actual.Motions.Where(m => m.Layer == LayerType.Scribe));
|
||||
Assert.Empty(new ReleasedContourState().Check(actual, Vector.Zero, 1));
|
||||
unchanged();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixedProgramCapture_PreservesInactiveMotionlessRegisteredSubprogram()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var part = SimplePart(Vector.Zero, parameters);
|
||||
part.Program.SubPrograms[-27] = new Program();
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part]));
|
||||
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
||||
Assert.Empty(result.ProposedOrder[0].CopyProgram().SubPrograms[-27].Codes);
|
||||
}
|
||||
|
||||
private static Part SimplePart(Vector location, CuttingParameters parameters)
|
||||
{
|
||||
var clean = LeadPathValidationTests.Rectangle(0, 0, 10, 10);
|
||||
var part = new Part(new Drawing("same", clean), location);
|
||||
var prepared = PreparedContours.Capture(clean, parameters);
|
||||
var emitted = prepared.Emit([prepared.ClosestEntry(0, new Vector(-2, 5))]);
|
||||
Assert.True(part.RestoreLeadInProgram(emitted, false)); part.CuttingParameters = parameters;
|
||||
return part;
|
||||
}
|
||||
|
||||
private static (Part, CuttingParameters) Crossing()
|
||||
{
|
||||
var clean = PreparedContourTests.Holes();
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var prepared = PreparedContours.Capture(clean, parameters);
|
||||
var choices = new[]
|
||||
{
|
||||
prepared.Entry(0, 0, new Vector(2, 3)),
|
||||
prepared.Entry(1, 0, new Vector(8, 3)),
|
||||
prepared.ClosestEntry(2, new Vector(-2, 3))
|
||||
};
|
||||
var original = prepared.Emit(choices);
|
||||
var part = new Part(new Drawing("same", clean));
|
||||
Assert.True(part.RestoreLeadInProgram(original, false));
|
||||
part.CuttingParameters = parameters;
|
||||
return (part, parameters);
|
||||
}
|
||||
|
||||
private static Action Unchanged(Part part, CuttingParameters parameters)
|
||||
{
|
||||
var program = part.Program;
|
||||
var clean = part.BaseDrawing.Program;
|
||||
var fingerprint = ExplicitContourTests.Fingerprint(program);
|
||||
var cleanText = clean.ToString();
|
||||
var codes = program.Codes.ToArray();
|
||||
var subs = program.Codes.OfType<SubProgramCall>()
|
||||
.Select(c => (c, c.Program, text: c.Program.ToString(), c.Id, c.Offset, c.Rotation)).ToArray();
|
||||
var location = part.Location;
|
||||
var rotation = part.Rotation;
|
||||
var locked = part.LeadInsLocked;
|
||||
var manual = part.HasManualLeadIns;
|
||||
var bounds = part.BoundingBox;
|
||||
var quantity = part.BaseDrawing.Quantity.Nested;
|
||||
var length = ((LineLeadIn)parameters.ExternalLeadIn).Length;
|
||||
return () =>
|
||||
{
|
||||
Assert.Same(program, part.Program); Assert.Same(clean, part.BaseDrawing.Program);
|
||||
Assert.Equal(fingerprint, ExplicitContourTests.Fingerprint(program)); Assert.Equal(cleanText, clean.ToString());
|
||||
Assert.Equal(codes, program.Codes); Assert.Equal(location, part.Location); Assert.Equal(rotation, part.Rotation);
|
||||
Assert.Equal(locked, part.LeadInsLocked); Assert.Equal(manual, part.HasManualLeadIns);
|
||||
Assert.Same(bounds, part.BoundingBox); Assert.Same(parameters, part.CuttingParameters);
|
||||
Assert.Equal(quantity, part.BaseDrawing.Quantity.Nested);
|
||||
Assert.Equal(length, ((LineLeadIn)parameters.ExternalLeadIn).Length);
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
Assert.Same(sub.Program, sub.c.Program); Assert.Equal(sub.text, sub.c.Program.ToString());
|
||||
Assert.Equal(sub.Id, sub.c.Id); Assert.Equal(sub.Offset, sub.c.Offset); Assert.Equal(sub.Rotation, sub.c.Rotation);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static OwnedExecution Read(Program p, Vector location) => ExecutionMotionReader.Read(p, location, null, default);
|
||||
private static string Describe(CuttingPlanResult r) => $"{r.Status}, expanded {r.Expansions}: " + string.Join("; ", r.Findings.Select(f => f.Message));
|
||||
}
|
||||
Reference in new issue
Block a user