mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 15:34:14 -04:00
merge: integrate best-effort cutting on current master
This commit is contained in:
commit
81f3c006ab
10 files changed
+572
-49
No files matched your search
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// A deliberately uncertified fallback, not a relaxed safety checker. Reuses closed native
|
||||
/// contour preparation and emission, retaining source order except for proven prerequisites.
|
||||
/// Unknown program semantics, invalid motions and unrepresentable contours still refuse.
|
||||
/// </summary>
|
||||
internal static class BestEffortCuttingPlan
|
||||
{
|
||||
internal static CuttingPlanResult Plan(CuttingPlanSnapshot snapshot, CancellationToken token)
|
||||
{
|
||||
if (snapshot.Failure is { } failure)
|
||||
return new(failure, findings: snapshot.Findings);
|
||||
if (!snapshot.Regeneration || snapshot.OwnedParameters == null)
|
||||
return new(CuttingPlanStatus.InvalidInput);
|
||||
var findings = snapshot.Findings.ToList();
|
||||
findings.Add(new(null, null, null, null, PostVerificationKind.Incomplete,
|
||||
"Best-effort plan: lead clearance, rapid travel and material containment are not certified. "
|
||||
+ "Review the complete plan before cutting. Source contours have not been repaired."));
|
||||
var order = new List<FixedProgramPlacement>();
|
||||
var done = new HashSet<int>();
|
||||
var position = snapshot.StartPoint;
|
||||
var distance = 0.0;
|
||||
var checker = new ReleasedContourState();
|
||||
try
|
||||
{
|
||||
while (order.Count < snapshot.Placements.Count)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var source = snapshot.PreservePartOrder ? snapshot.Placements[order.Count]
|
||||
: snapshot.Placements.FirstOrDefault(p => !done.Contains(p.SourceOrdinal)
|
||||
&& snapshot.Dependencies.IsReady(p.SourceOrdinal, done));
|
||||
if (source == null || !snapshot.Dependencies.IsReady(source.SourceOrdinal, done))
|
||||
return new(CuttingPlanStatus.ConstraintConflict, findings: findings);
|
||||
var proposal = source;
|
||||
if (source.Prepared is { } prepared)
|
||||
{
|
||||
var choices = new List<ContourChoice>();
|
||||
var approach = position - source.Location;
|
||||
for (var contour = 0; contour < prepared.Count; contour++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
choices.Add(prepared.ClosestEntry(contour, approach));
|
||||
// Prefixes are standalone programs, not relative to the prior prefix.
|
||||
var prefix = ExecutionMotionReader.ReadSupported(prepared.EmitPrefix(choices),
|
||||
source.Location, position, token);
|
||||
approach = prefix.DeparturePoint - source.Location;
|
||||
}
|
||||
var program = prepared.Emit(choices);
|
||||
var emitted = ExecutionMotionReader.ReadSupported(program, source.Location, position, token);
|
||||
proposal = source.Propose(program, emitted, choices, token);
|
||||
}
|
||||
// Read the owned payload again; malformed/nonfinite output never reaches Apply.
|
||||
var execution = ExecutionMotionReader.ReadSupported(proposal.CopyProgram(), source.Location, position, token);
|
||||
if (!execution.HasCuttingContour)
|
||||
throw new ArgumentException("Best-effort output has no cutting contour.");
|
||||
findings.AddRange(checker.Check(execution, position, source.SourceOrdinal + 1, source.IsCutOff, token)
|
||||
.Select(f => new CuttingPlanFinding(f.PartNumber is int part ? part - 1 : null,
|
||||
f.PartNumber is int ordinal ? snapshot.Placements[ordinal - 1].SourcePart : null,
|
||||
f.OtherPartNumber is int other ? other - 1 : null,
|
||||
f.OtherPartNumber is int otherOrdinal ? snapshot.Placements[otherOrdinal - 1].SourcePart : null,
|
||||
f.Kind, f.Message)));
|
||||
distance += execution.RapidDistanceFrom(position);
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentException("Best-effort travel is not finite.");
|
||||
position = execution.DeparturePoint;
|
||||
done.Add(source.SourceOrdinal);
|
||||
order.Add(proposal);
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(CuttingPlanStatus.BestEffort, order, findings, rapidDistance: distance)
|
||||
{ Snapshot = snapshot };
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new(CuttingPlanStatus.Cancelled);
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or NotSupportedException
|
||||
or InvalidOperationException or ArithmeticException)
|
||||
{
|
||||
findings.Add(new(null, null, null, null, PostVerificationKind.Incomplete, exception.Message));
|
||||
return new(CuttingPlanStatus.UnsupportedGeometry, findings: findings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ internal sealed class CuttingDependencyGraph
|
||||
}
|
||||
|
||||
internal static CuttingDependencyGraph Build(IReadOnlyList<DependencyNode> nodes, Box plate,
|
||||
CancellationToken token)
|
||||
CancellationToken token, Action<CuttingDependencyException> uncertain = null)
|
||||
{
|
||||
var edges = nodes.Select(_ => new SortedSet<int>()).ToArray();
|
||||
for (var cut = 0; cut < nodes.Count; cut++)
|
||||
@@ -98,9 +98,24 @@ internal sealed class CuttingDependencyGraph
|
||||
token.ThrowIfCancellationRequested();
|
||||
// Bounds only select candidates; containment itself is proven on native material.
|
||||
if (inner != host && !nodes[inner].IsCutOff && !nodes[host].IsCutOff
|
||||
&& Within(nodes[inner].CleanBounds, nodes[host].HostBounds)
|
||||
&& InsideCutout(nodes, inner, host, token))
|
||||
edges[host].Add(inner);
|
||||
&& Within(nodes[inner].CleanBounds, nodes[host].HostBounds))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (InsideCutout(nodes, inner, host, token))
|
||||
edges[host].Add(inner);
|
||||
}
|
||||
catch (CuttingDependencyException exception) when (uncertain != null
|
||||
&& exception.Status == CuttingPlanStatus.UnsupportedGeometry)
|
||||
{
|
||||
uncertain(exception);
|
||||
}
|
||||
catch (Exception exception) when (uncertain != null
|
||||
&& exception is ArgumentException or NotSupportedException)
|
||||
{
|
||||
uncertain(Ambiguous(inner, host, exception.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
var graph = new CuttingDependencyGraph(edges.Select(e => e.ToArray()).ToArray());
|
||||
if (graph.FindCycle() is { } cyclic)
|
||||
|
||||
@@ -99,7 +99,11 @@ public sealed class CuttingPlanBatch
|
||||
var reorder = preservePartOrder ? null : CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
|
||||
expansionBudget: reorderBudget ?? budget, confirmedParameters: confirmedParameters), token);
|
||||
var overlap = PlateOverlapAnalyzer.Capture(plate.Parts.ToArray(), token);
|
||||
entries[index] = new(plate, plateNumbers?[index] ?? index + 1, reorder, keepOrder, overlap);
|
||||
var fallback = reorder ?? keepOrder;
|
||||
if (fallback.Failure == CuttingPlanStatus.UnsupportedGeometry)
|
||||
fallback = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
|
||||
confirmedParameters: confirmedParameters, preservePartOrder: preservePartOrder), token, bestEffort: true);
|
||||
entries[index] = new(plate, plateNumbers?[index] ?? index + 1, reorder, keepOrder, overlap, fallback);
|
||||
}
|
||||
return new(entries, owned);
|
||||
}
|
||||
@@ -112,7 +116,8 @@ public sealed class CuttingPlanBatch
|
||||
/// Checks and plans every plate from its captured snapshots. Safe on a worker: live plates are
|
||||
/// not read. Overlapping material blocks a plate whatever its route. A free search that ends
|
||||
/// without a complete plan within its budget is retried with the current order; the proposal
|
||||
/// reports that it kept the order.
|
||||
/// reports that it kept the order. Incomplete geometry also gets a separately labelled,
|
||||
/// unverified fallback when supported closed contours can still be emitted.
|
||||
/// </summary>
|
||||
public CuttingPlanProposal Plan(IProgress<CuttingPlanProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
@@ -140,19 +145,31 @@ public sealed class CuttingPlanBatch
|
||||
reorder = CuttingPlanService.Plan(entry.Reorder, token);
|
||||
if (reorder.Status != CuttingPlanStatus.NoSolutionWithinBudget)
|
||||
{
|
||||
plans[index] = new(entry.Plate, entry.Number, reorder, null, overlap);
|
||||
plans[index] = new(entry.Plate, entry.Number, WithFallback(entry, reorder, token), null, overlap);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
progress?.Report(new(index, entries.Length, entry.Number, CuttingPlanPhase.KeepingOrder));
|
||||
plans[index] = new(entry.Plate, entry.Number, CuttingPlanService.Plan(entry.KeepOrder, token), reorder,
|
||||
overlap);
|
||||
plans[index] = new(entry.Plate, entry.Number,
|
||||
WithFallback(entry, CuttingPlanService.Plan(entry.KeepOrder, token), token), reorder, overlap);
|
||||
}
|
||||
return new(plans, ownedParameters);
|
||||
}
|
||||
|
||||
private static CuttingPlanResult WithFallback(Entry entry, CuttingPlanResult strict, CancellationToken token)
|
||||
{
|
||||
if (strict.Status != CuttingPlanStatus.UnsupportedGeometry || entry.Fallback.Failure != null)
|
||||
return strict;
|
||||
var fallback = BestEffortCuttingPlan.Plan(entry.Fallback, token);
|
||||
if (fallback.Status != CuttingPlanStatus.BestEffort)
|
||||
return fallback.Status == CuttingPlanStatus.Cancelled ? fallback : strict;
|
||||
return new(CuttingPlanStatus.BestEffort, fallback.ProposedOrder,
|
||||
strict.Findings.Concat(fallback.Findings).Distinct(), rapidDistance: fallback.RapidDistance)
|
||||
{ Snapshot = entry.Fallback };
|
||||
}
|
||||
|
||||
private sealed record Entry(Plate Plate, int Number, CuttingPlanSnapshot Reorder, CuttingPlanSnapshot KeepOrder,
|
||||
PlateOverlapSnapshot Overlap);
|
||||
PlateOverlapSnapshot Overlap, CuttingPlanSnapshot Fallback);
|
||||
}
|
||||
|
||||
/// <summary>One plate's outcome inside a <see cref="CuttingPlanProposal"/>.</summary>
|
||||
@@ -188,6 +205,10 @@ public sealed class CuttingPlanPlateResult
|
||||
|
||||
public bool IsOverlapClear => Overlap is { IsComplete: true } && Overlap.Pairs.Count == 0;
|
||||
public bool IsReady => IsRouteReady && IsOverlapClear;
|
||||
|
||||
/// <summary>Owned readable output is available, but not all geometric checks passed.</summary>
|
||||
public bool CanApplyWithWarnings => Overlap != null && Overlap.Pairs.Count == 0
|
||||
&& (IsRouteReady || Result.Status == CuttingPlanStatus.BestEffort);
|
||||
public int PartCount => Result.ProposedOrder.Count;
|
||||
public int RegeneratedCount => Result.ProposedOrder.Count(p => p.IsRegenerated);
|
||||
|
||||
@@ -196,7 +217,7 @@ public sealed class CuttingPlanPlateResult
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The outcome of a batch. Apply is all-or-nothing and is offered only when every plate is ready.
|
||||
/// The outcome of a batch. Apply is all-or-nothing. Unverified output needs explicit acceptance.
|
||||
/// </summary>
|
||||
public sealed class CuttingPlanProposal
|
||||
{
|
||||
@@ -216,17 +237,26 @@ public sealed class CuttingPlanProposal
|
||||
public bool CanApply => Plates.Count > 0 && !IsCancelled && ownedParameters != null
|
||||
&& Plates.All(p => p.IsReady);
|
||||
|
||||
/// <summary>All plates have usable output, possibly requiring explicit warning acceptance.</summary>
|
||||
public bool CanApplyWithWarnings => Plates.Count > 0 && !IsCancelled && ownedParameters != null
|
||||
&& Plates.All(p => p.CanApplyWithWarnings);
|
||||
|
||||
public bool RequiresWarningAcceptance => CanApplyWithWarnings && !CanApply;
|
||||
|
||||
/// <summary>
|
||||
/// Installs every plate's replayed proposal through <see cref="CuttingPlanService.Apply"/>, on the
|
||||
/// thread that owns the plates. Only after every plate is applied does each one keep an owned copy
|
||||
/// of the confirmed parameters as its cutting settings; any other status changes nothing.
|
||||
/// </summary>
|
||||
public CuttingCommitResult Apply(CancellationToken token = default)
|
||||
public CuttingCommitResult Apply(CancellationToken token = default) => Apply(false, token);
|
||||
|
||||
/// <summary>Explicit per-proposal acceptance; never grants consent to post CNC output.</summary>
|
||||
public CuttingCommitResult Apply(bool acceptWarnings, CancellationToken token = default)
|
||||
{
|
||||
if (!CanApply)
|
||||
if (!(CanApply || acceptWarnings && CanApplyWithWarnings))
|
||||
return new(CuttingCommitStatus.InvalidInput,
|
||||
"Every plate must have a ready plan before anything is applied.");
|
||||
var commit = CuttingPlanService.Apply(Plates.Select(p => p.Result), token);
|
||||
var commit = CuttingPlanService.Apply(Plates.Select(p => p.Result), token, null, acceptWarnings);
|
||||
if (commit.Status == CuttingCommitStatus.Applied)
|
||||
foreach (var plate in Plates)
|
||||
plate.Plate.CuttingParameters = OwnedCuttingParameters.Copy(ownedParameters);
|
||||
@@ -234,18 +264,17 @@ public sealed class CuttingPlanProposal
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A detached copy of a ready plate for display, in the proposed order with the proposed
|
||||
/// programs, or null. A refused plate is not previewed: its program graphs may be unsafe to
|
||||
/// copy. Neither is a plate that changed after planning, which would draw replayed programs at
|
||||
/// poses that were never checked. The copy has quantity zero, so drawing quantities stay put.
|
||||
/// A detached copy of a ready or explicitly unverified proposal for display, or null.
|
||||
/// Refused graphs are never cloned. Changed plates are not previewed at uncaptured poses.
|
||||
/// The copy has quantity zero, so drawing quantities stay put.
|
||||
/// Call it on the thread that owns the plates.
|
||||
/// </summary>
|
||||
public Plate BuildPreview(int index)
|
||||
{
|
||||
var planned = Plates[index];
|
||||
if (!planned.IsReady || planned.Result.Snapshot?.PlateState?.IsCurrent() != true)
|
||||
if (!planned.CanApplyWithWarnings || planned.Result.Snapshot?.PlateState?.IsCurrent() != true)
|
||||
return null;
|
||||
// Unchanged since capture: live poses, plate and programs are the validated, replayed ones.
|
||||
// Unchanged since capture: graphs are supported and the preview uses the exact proposal.
|
||||
var source = planned.Plate;
|
||||
var preview = new Plate(source.Size)
|
||||
{
|
||||
@@ -274,6 +303,9 @@ public sealed class CuttingPlanProposal
|
||||
lines.Add($"Ready to apply to {Count(Plates.Count, "plate")}: "
|
||||
+ $"{Count(Plates.Count(p => p.OrderChanged), "plate")} with a new part order, "
|
||||
+ $"{Count(Plates.Sum(p => p.RegeneratedCount), "part program")} regenerated.");
|
||||
else if (RequiresWarningAcceptance)
|
||||
lines.Add("Best-effort plan available. Review the warnings and accept the unverified plan to apply. "
|
||||
+ "This is not approval to cut or post CNC output.");
|
||||
else
|
||||
lines.Add($"Apply is unavailable: {Plates.Count - ready} of {Count(Plates.Count, "plate")} could not "
|
||||
+ "be planned. No plate changes until every plate is ready.");
|
||||
@@ -307,6 +339,17 @@ public sealed class CuttingPlanProposal
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (plate.CanApplyWithWarnings)
|
||||
{
|
||||
yield return heading + $"best-effort, unverified. {Count(plate.PartCount, "part")}, "
|
||||
+ $"{plate.RegeneratedCount} regenerated; review lead-ins and cutting order.";
|
||||
foreach (var line in Limit(DescribeOverlap(plate.Overlap)))
|
||||
yield return line;
|
||||
foreach (var line in Limit(result.Findings.Select(DescribeFinding)))
|
||||
yield return line;
|
||||
yield break;
|
||||
}
|
||||
|
||||
var missingLead = !plate.IsRouteReady && result.Status != CuttingPlanStatus.Cancelled
|
||||
&& result.Findings.Any(f => f.Kind == PostVerificationKind.MissingLeadIn);
|
||||
yield return heading + (plate.IsRouteReady
|
||||
|
||||
@@ -60,8 +60,9 @@ public sealed class CuttingPlanSnapshot
|
||||
int expansionBudget, CuttingPlanStatus? failure = null, IEnumerable<CuttingPlanFinding> findings = null,
|
||||
bool regeneration = false, bool preservePartOrder = false, int maxEntries = 16,
|
||||
Action<int> expansionObserver = null, PlateCuttingState plateState = null,
|
||||
CuttingParameters ownedParameters = null, CuttingDependencyGraph dependencies = null)
|
||||
CuttingParameters ownedParameters = null, CuttingDependencyGraph dependencies = null, bool bestEffort = false)
|
||||
{
|
||||
BestEffort = bestEffort;
|
||||
PlateState = plateState;
|
||||
OwnedParameters = ownedParameters;
|
||||
Placements = Array.AsReadOnly(placements.ToArray());
|
||||
@@ -82,6 +83,7 @@ public sealed class CuttingPlanSnapshot
|
||||
internal PlateCuttingState PlateState { get; }
|
||||
/// <summary>Owned copy of the confirmed parameters taken at capture; never the caller's object.</summary>
|
||||
internal CuttingParameters OwnedParameters { get; }
|
||||
internal bool BestEffort { get; }
|
||||
internal bool Regeneration { get; }
|
||||
internal bool PreservePartOrder { get; }
|
||||
internal int MaxEntries { get; }
|
||||
@@ -159,7 +161,9 @@ public enum CuttingPlanStatus
|
||||
UnsupportedGeometry,
|
||||
InvalidInput,
|
||||
NoSolutionWithinBudget,
|
||||
Cancelled
|
||||
Cancelled,
|
||||
/// <summary>Usable owned programs, but geometric safety checks did not pass. Never Ready.</summary>
|
||||
BestEffort
|
||||
}
|
||||
|
||||
/// <summary>Ordinals are zero-based source positions, not proposed sequence positions.</summary>
|
||||
@@ -169,7 +173,8 @@ public sealed record CuttingPlanFinding(int? SourceOrdinal, Part SourcePart,
|
||||
/// <summary>
|
||||
/// A replayed direct-XY proposal, optionally with regenerated programs, that respects the
|
||||
/// captured cutoff/containment prerequisites. Not physical safety or posting consent. Apply
|
||||
/// installs it only for a plate-scoped request whose plate is unchanged. Failures contain no proposals.
|
||||
/// installs it only for a plate-scoped request whose plate is unchanged. BestEffort is explicitly
|
||||
/// unverified and requires batch warning acceptance; other failures contain no proposals.
|
||||
/// </summary>
|
||||
public sealed class CuttingPlanResult
|
||||
{
|
||||
|
||||
@@ -15,9 +15,13 @@ public static class CuttingPlanService
|
||||
/// 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)
|
||||
public static CuttingPlanSnapshot Capture(CuttingPlanRequest request, CancellationToken token = default) =>
|
||||
Capture(request, token, bestEffort: false);
|
||||
|
||||
internal static CuttingPlanSnapshot Capture(CuttingPlanRequest request, CancellationToken token, bool bestEffort)
|
||||
{
|
||||
var placements = new List<FixedProgramPlacement>();
|
||||
var warnings = new List<CuttingPlanFinding>();
|
||||
Part source = null;
|
||||
int? ordinal = null;
|
||||
try
|
||||
@@ -107,7 +111,11 @@ public static class CuttingPlanService
|
||||
{
|
||||
material = LeadMaterialSnapshot.Capture(ownedClean, location, token);
|
||||
if (!material.IsComplete)
|
||||
throw new NotSupportedException(material.Reason);
|
||||
{
|
||||
if (!bestEffort)
|
||||
throw new NotSupportedException(material.Reason);
|
||||
warnings.Add(new(index, source, null, null, PostVerificationKind.Incomplete, material.Reason));
|
||||
}
|
||||
if (!source.LeadInsLocked && (request.EligibleParts == null || eligible.Contains(source)))
|
||||
prepared = PreparedContours.Capture(ownedClean, request.ConfirmedParameters, token);
|
||||
}
|
||||
@@ -122,13 +130,16 @@ public static class CuttingPlanService
|
||||
placements[0].Execution.RapidDistanceFrom(request.StartPoint);
|
||||
source = null;
|
||||
ordinal = null;
|
||||
var dependencies = CuttingDependencyGraph.Build(nodes, request.Plate?.BoundingBox(includeParts: false), token);
|
||||
var dependencies = CuttingDependencyGraph.Build(nodes, request.Plate?.BoundingBox(includeParts: false), token,
|
||||
bestEffort ? exception => warnings.Add(new(exception.Ordinal, request.Parts[exception.Ordinal],
|
||||
exception.Other, exception.Other is int other ? request.Parts[other] : null,
|
||||
PostVerificationKind.Incomplete, exception.Message)) : null);
|
||||
return new(placements, request.StartPoint, request.ExpansionBudget, regeneration: request.ConfirmedParameters != null,
|
||||
preservePartOrder: request.PreservePartOrder, maxEntries: request.MaxEntries,
|
||||
expansionObserver: request.ExpansionObserver, plateState: plateState,
|
||||
ownedParameters: request.ConfirmedParameters == null ? null
|
||||
: OwnedCuttingParameters.Copy(request.ConfirmedParameters),
|
||||
dependencies: dependencies);
|
||||
dependencies: dependencies, findings: warnings, bestEffort: bestEffort);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -175,14 +186,15 @@ public static class CuttingPlanService
|
||||
|
||||
// beforeInstall is the commit's install-boundary test seam.
|
||||
internal static CuttingCommitResult Apply(IEnumerable<CuttingPlanResult> results, CancellationToken token,
|
||||
Action<Plate, Part> beforeInstall)
|
||||
Action<Plate, Part> beforeInstall, bool acceptUnverified = false)
|
||||
{
|
||||
var plans = new List<PlateCuttingPlan>();
|
||||
foreach (var result in results ?? [])
|
||||
{
|
||||
var snapshot = result?.Snapshot;
|
||||
if (result?.Status != CuttingPlanStatus.Ready || !result.IndependentlyReplayed
|
||||
|| snapshot?.PlateState == null || result.ProposedOrder.Count != snapshot.Placements.Count)
|
||||
var eligible = result != null && (result.Status == CuttingPlanStatus.Ready && result.IndependentlyReplayed
|
||||
|| acceptUnverified && result.Status == CuttingPlanStatus.BestEffort);
|
||||
if (!eligible || snapshot?.PlateState == null || result.ProposedOrder.Count != snapshot.Placements.Count)
|
||||
return new(CuttingCommitStatus.InvalidInput,
|
||||
"Only Ready, independently replayed plate-scoped proposals can be applied.");
|
||||
var programs = new List<PlannedPartProgram>();
|
||||
@@ -211,6 +223,8 @@ public static class CuttingPlanService
|
||||
return new(CuttingPlanStatus.InvalidInput);
|
||||
if (snapshot.Failure is { } failure)
|
||||
return new(failure, findings: snapshot.Findings);
|
||||
if (snapshot.BestEffort)
|
||||
return BestEffortCuttingPlan.Plan(snapshot, token);
|
||||
if (snapshot.Placements.Count == 0)
|
||||
return snapshot.PlateState == null ? new(CuttingPlanStatus.InvalidInput)
|
||||
: new(CuttingPlanStatus.Ready, independentlyReplayed: true); // Empty plate: unchanged no-op.
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class BestEffortCuttingPlanTests
|
||||
{
|
||||
[Fact]
|
||||
public void TouchingContours_OfferAPreviewWithoutChangingTheDrawing()
|
||||
{
|
||||
var part = TouchingContours();
|
||||
var plate = Plate(part);
|
||||
var original = OwnedProgramCopy.Copy(part.BaseDrawing.Program);
|
||||
var strict = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
|
||||
confirmedParameters: ExplicitContourTests.Parameters()));
|
||||
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, strict.Status);
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
|
||||
Assert.NotNull(proposal.BuildPreview(0));
|
||||
Assert.False(proposal.CanApply);
|
||||
Assert.False(part.HasManualLeadIns);
|
||||
Assert.True(ProgramContent.Equal(original, part.BaseDrawing.Program));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptWarnings_InstallsOwnedProgramsAndLeavesStrictApplyClosed()
|
||||
{
|
||||
var part = TouchingContours();
|
||||
var plate = Plate(part);
|
||||
var source = OwnedProgramCopy.Copy(part.BaseDrawing.Program);
|
||||
var pose = (part.Location, part.Rotation);
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
var result = Assert.Single(proposal.Plates).Result;
|
||||
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
|
||||
Assert.False(result.IndependentlyReplayed);
|
||||
Assert.True(proposal.RequiresWarningAcceptance);
|
||||
Assert.Contains(result.Findings, f => f.Message.Contains("Material boundaries"));
|
||||
Assert.Contains("unverified", string.Join("\n", proposal.Describe("in")));
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply().Status);
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, CuttingPlanService.Apply([result]).Status);
|
||||
var generated = Assert.Single(result.ProposedOrder);
|
||||
Assert.True(generated.IsRegenerated);
|
||||
Assert.Equal(3, generated.ContourChoices.Count);
|
||||
Assert.Equal(2, generated.ContourChoices[^1].ContourOrdinal);
|
||||
var originalCuts = ExecutionMotionReader.ReadSupported(source, part.Location, null).Motions
|
||||
.Where(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display).Sum(m => m.Length);
|
||||
var emittedCuts = ExecutionMotionReader.ReadSupported(generated.CopyProgram(), part.Location, null).Motions
|
||||
.Where(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display).Sum(m => m.Length);
|
||||
Assert.Equal(originalCuts, emittedCuts, 8);
|
||||
var copy = generated.CopyProgram();
|
||||
copy.Codes.Clear();
|
||||
|
||||
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: true).Status);
|
||||
|
||||
Assert.True(part.HasManualLeadIns);
|
||||
Assert.True(ProgramContent.Equal(generated.CopyProgram(), part.Program));
|
||||
Assert.True(ProgramContent.Equal(source, part.BaseDrawing.Program));
|
||||
Assert.Equal(pose, (part.Location, part.Rotation));
|
||||
Assert.Equal(CuttingCommitStatus.Stale, proposal.Apply(acceptWarnings: true).Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("null")]
|
||||
[InlineData("unknown")]
|
||||
[InlineData("cycle")]
|
||||
[InlineData("nonfinite")]
|
||||
[InlineData("open")]
|
||||
[InlineData("suppressed")]
|
||||
public void UnreadableOrUnrepresentableInput_IsNeverWaived(string failure)
|
||||
{
|
||||
var part = TouchingContours();
|
||||
var plate = Plate(part);
|
||||
var program = part.BaseDrawing.Program;
|
||||
switch (failure)
|
||||
{
|
||||
case "null": program.Codes = null; break;
|
||||
case "unknown": program.Codes.Add(new CustomMove()); break;
|
||||
case "cycle": program.Codes.Add(new SubProgramCall { Program = program }); break;
|
||||
case "nonfinite": part.Location = new Vector(double.NaN, 1); break;
|
||||
case "open": program.Codes.RemoveAt(program.Codes.Count - 1); break;
|
||||
case "suppressed": program.Codes.OfType<Motion>().First().Suppressed = true; break;
|
||||
}
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.False(proposal.CanApplyWithWarnings);
|
||||
Assert.Null(proposal.BuildPreview(0));
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply(acceptWarnings: true).Status);
|
||||
Assert.False(part.HasManualLeadIns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedBatch_StaleOrCancelledChangesNothing()
|
||||
{
|
||||
var imperfect = TouchingContours();
|
||||
var clean = new Part(new Drawing("clean", ExplicitContourTests.Square(false)), new Vector(1, 1));
|
||||
var plates = new[] { Plate(clean), Plate(imperfect) };
|
||||
var originals = new[] { clean.Program, imperfect.Program };
|
||||
var batch = CuttingPlanBatch.Capture(plates, ExplicitContourTests.Parameters(), false);
|
||||
var proposal = batch.Plan();
|
||||
Assert.True(proposal.RequiresWarningAcceptance);
|
||||
using var cancelled = new CancellationTokenSource();
|
||||
cancelled.Cancel();
|
||||
Assert.Equal(CuttingCommitStatus.Cancelled, proposal.Apply(true, cancelled.Token).Status);
|
||||
Assert.False(batch.Plan(token: cancelled.Token).CanApplyWithWarnings);
|
||||
imperfect.Offset(1, 0);
|
||||
Assert.Null(proposal.BuildPreview(1));
|
||||
Assert.Equal(CuttingCommitStatus.Stale, proposal.Apply(acceptWarnings: true).Status);
|
||||
Assert.Equal(originals, plates.SelectMany(p => p.Parts).Select(p => p.Program));
|
||||
Assert.All(plates, p => Assert.Null(p.CuttingParameters));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LockedPart_KeepsExactProgramWhileOtherPartsRegenerate()
|
||||
{
|
||||
var part = TouchingContours();
|
||||
part.LeadInsLocked = true;
|
||||
var program = part.Program;
|
||||
var other = new Part(new Drawing("clean", ExplicitContourTests.Square(false)), new Vector(20, 1));
|
||||
var proposal = CuttingPlanBatch.Capture([Plate(part, other)], ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.True(proposal.RequiresWarningAcceptance);
|
||||
Assert.False(proposal.Plates[0].Result.ProposedOrder[0].IsRegenerated);
|
||||
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(true).Status);
|
||||
Assert.Same(program, part.Program);
|
||||
Assert.True(part.LeadInsLocked);
|
||||
Assert.True(other.HasManualLeadIns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownPartOverlap_StillBlocksTheWholeBatch()
|
||||
{
|
||||
var first = new Part(new Drawing("first", ExplicitContourTests.Square(false)), new Vector(1, 1));
|
||||
var second = new Part(new Drawing("second", ExplicitContourTests.Square(false)), new Vector(6, 6));
|
||||
var proposal = CuttingPlanBatch.Capture([Plate(TouchingContours()), Plate(first, second)],
|
||||
ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.False(proposal.CanApplyWithWarnings);
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply(true).Status);
|
||||
Assert.False(first.HasManualLeadIns);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ProvenCutoffDependencies_AreNotWaived(bool keepOrder)
|
||||
{
|
||||
var part = TouchingContours();
|
||||
var cutProgram = new Program();
|
||||
cutProgram.MoveTo(20, 0); cutProgram.LineTo(20, 20);
|
||||
var cutoff = new Part(new Drawing("cutoff", cutProgram) { IsCutOff = true });
|
||||
var plate = Plate(part, cutoff); // An orphaned cutoff must precede all ordinary parts.
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), keepOrder).Plan();
|
||||
if (keepOrder)
|
||||
{
|
||||
Assert.False(proposal.CanApplyWithWarnings);
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply(true).Status);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(proposal.CanApplyWithWarnings);
|
||||
Assert.Same(cutoff, proposal.Plates[0].Result.ProposedOrder[0].SourcePart);
|
||||
Assert.False(proposal.Plates[0].Result.ProposedOrder[0].IsRegenerated);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfIntersectingClosedContour_StillGetsEveryCutAndALead()
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0); program.LineTo(8, 8); program.LineTo(0, 8);
|
||||
program.LineTo(8, 0); program.LineTo(0, 0);
|
||||
var part = new Part(new Drawing("crossed contour", program), new Vector(1, 1));
|
||||
var proposal = CuttingPlanBatch.Capture([Plate(part)], ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.True(proposal.RequiresWarningAcceptance);
|
||||
var generated = Assert.Single(proposal.Plates[0].Result.ProposedOrder);
|
||||
Assert.True(generated.IsRegenerated);
|
||||
Assert.Single(generated.ContourChoices);
|
||||
var moves = ExecutionMotionReader.ReadSupported(generated.CopyProgram(), part.Location, null).Motions;
|
||||
Assert.Contains(moves, m => m.Layer == LayerType.Leadin);
|
||||
var original = ExecutionMotionReader.ReadSupported(program, part.Location, null).Motions;
|
||||
Assert.Equal(original.Where(m => !m.Rapid).Sum(m => m.Length),
|
||||
moves.Where(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display).Sum(m => m.Length), 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProvenNestedPart_PrecedesItsHostEvenWithAnImperfectNeighbour()
|
||||
{
|
||||
var hostProgram = ExplicitContourTests.Square(false);
|
||||
hostProgram.MoveTo(2, 2); hostProgram.LineTo(8, 2); hostProgram.LineTo(8, 8);
|
||||
hostProgram.LineTo(2, 8); hostProgram.LineTo(2, 2);
|
||||
var host = new Part(new Drawing("host", hostProgram));
|
||||
var innerProgram = new Program();
|
||||
innerProgram.MoveTo(0, 0); innerProgram.LineTo(0, 1); innerProgram.LineTo(1, 1);
|
||||
innerProgram.LineTo(1, 0); innerProgram.LineTo(0, 0);
|
||||
var inner = new Part(new Drawing("insert", innerProgram), new Vector(4, 4));
|
||||
var imperfect = TouchingContours();
|
||||
imperfect.Offset(20, 0);
|
||||
var proposal = CuttingPlanBatch.Capture([Plate(host, inner, imperfect)],
|
||||
ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.True(proposal.RequiresWarningAcceptance, string.Join("\n", proposal.Describe("in")));
|
||||
var order = proposal.Plates[0].Result.ProposedOrder.Select(p => p.SourcePart).ToList();
|
||||
Assert.True(order.IndexOf(inner) < order.IndexOf(host));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BestEffortInstallFailure_RollsBackEveryPlate()
|
||||
{
|
||||
var parts = new[] { TouchingContours(), TouchingContours() };
|
||||
var plates = parts.Select(p => Plate(p)).ToArray();
|
||||
var originals = parts.Select(p => p.Program).ToArray();
|
||||
var proposal = CuttingPlanBatch.Capture(plates, ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.True(proposal.RequiresWarningAcceptance);
|
||||
var commit = CuttingPlanService.Apply(proposal.Plates.Select(p => p.Result), default,
|
||||
(plate, _) => { if (ReferenceEquals(plate, plates[1])) throw new InvalidOperationException("test install fault"); },
|
||||
acceptUnverified: true);
|
||||
Assert.Equal(CuttingCommitStatus.Failed, commit.Status);
|
||||
Assert.Equal(originals, parts.Select(p => p.Program));
|
||||
Assert.All(parts, p => Assert.False(p.HasManualLeadIns));
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData(0)]
|
||||
[InlineData(45)]
|
||||
public void RealDxf_PlansAndAppliesWithoutChangingTheDrawing(double degrees)
|
||||
{
|
||||
var path = TestConfig.GetExistingPath("BestEffortDxfPath");
|
||||
Skip.If(path == null, "BestEffortDxfPath not configured in test-config.json or file not found");
|
||||
var drawing = OpenNest.IO.CadImporter.ImportDrawing(path);
|
||||
var part = new Part(drawing, new Vector(1, 1));
|
||||
part.Rotate(degrees * System.Math.PI / 180);
|
||||
var plate = Plate(part);
|
||||
var source = OwnedProgramCopy.Copy(drawing.Program);
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.True(proposal.CanApplyWithWarnings, string.Join("\n", proposal.Describe("in")));
|
||||
Assert.NotNull(proposal.BuildPreview(0));
|
||||
Assert.True(Assert.Single(proposal.Plates[0].Result.ProposedOrder).IsRegenerated);
|
||||
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(true).Status);
|
||||
Assert.True(ProgramContent.Equal(source, drawing.Program));
|
||||
Assert.True(part.HasManualLeadIns);
|
||||
}
|
||||
|
||||
private sealed class CustomMove : LinearMove { }
|
||||
|
||||
internal static Part TouchingContours()
|
||||
{
|
||||
var program = ExplicitContourTests.Square(false);
|
||||
// Two ordinary holes share an edge: closed executable contours, ambiguous material.
|
||||
program.MoveTo(2, 2); program.LineTo(4, 2); program.LineTo(4, 4);
|
||||
program.LineTo(2, 4); program.LineTo(2, 2);
|
||||
program.MoveTo(4, 2); program.LineTo(6, 2); program.LineTo(6, 4);
|
||||
program.LineTo(4, 4); program.LineTo(4, 2);
|
||||
return new Part(new Drawing("touching contours", program), new Vector(1, 1));
|
||||
}
|
||||
|
||||
internal static Plate Plate(params Part[] parts)
|
||||
{
|
||||
var plate = new Plate(new Size(100, 100));
|
||||
foreach (var part in parts)
|
||||
plate.Parts.Add(part);
|
||||
return plate;
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,41 @@ public class CuttingPlanFormTests
|
||||
Assert.DoesNotContain("Plate 2", text);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void UnverifiedPlan_RequiresAcceptanceAndReplanClearsIt() => RunSta(() =>
|
||||
{
|
||||
var program = SquareProgram();
|
||||
program.MoveTo(2, 2); program.LineTo(4, 2); program.LineTo(4, 4);
|
||||
program.LineTo(2, 4); program.LineTo(2, 2);
|
||||
program.MoveTo(4, 2); program.LineTo(6, 2); program.LineTo(6, 4);
|
||||
program.LineTo(4, 4); program.LineTo(4, 2);
|
||||
var part = new Part(new Drawing("touching contours", program), new Vector(1, 1));
|
||||
var (nest, view) = CreateView(part);
|
||||
using var editor = view;
|
||||
using var form = new CuttingPlanForm(view, nest, allPlates: false, Parameters());
|
||||
form.Show();
|
||||
WaitForPlan(form);
|
||||
var accept = Control<CheckBox>(form, "acceptWarningsCheckBox");
|
||||
Assert.True(accept.Visible);
|
||||
Assert.False(accept.Checked);
|
||||
Assert.False(Control<Button>(form, "applyButton").Enabled);
|
||||
Assert.True(Field<PlateView>(form, "preview").Visible);
|
||||
Assert.Contains("UNVERIFIED", Control<Label>(form, "previewLabel").Text);
|
||||
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
|
||||
Assert.Null(form.CommitResult);
|
||||
Assert.False(part.HasManualLeadIns);
|
||||
accept.Checked = true;
|
||||
Assert.True(Control<Button>(form, "applyButton").Enabled);
|
||||
Invoke(form, "PlanButton_Click", null, EventArgs.Empty);
|
||||
WaitForPlan(form);
|
||||
Assert.False(accept.Checked);
|
||||
Assert.False(Control<Button>(form, "applyButton").Enabled);
|
||||
accept.Checked = true;
|
||||
Invoke(form, "ApplyButton_Click", null, EventArgs.Empty);
|
||||
Assert.Equal(CuttingCommitStatus.Applied, form.CommitResult!.Status);
|
||||
Assert.True(part.HasManualLeadIns);
|
||||
});
|
||||
|
||||
private static (Nest Nest, PlateView View) CreateView(params Part[] parts)
|
||||
{
|
||||
var nest = new Nest();
|
||||
|
||||
+17
-3
@@ -25,6 +25,7 @@ partial class CuttingPlanForm
|
||||
settingsButton = new Button();
|
||||
optionsPanel = new FlowLayoutPanel();
|
||||
keepOrderCheckBox = new CheckBox();
|
||||
acceptWarningsCheckBox = new CheckBox();
|
||||
planButton = new Button();
|
||||
split = new SplitContainer();
|
||||
previewLayout = new TableLayoutPanel();
|
||||
@@ -54,15 +55,17 @@ partial class CuttingPlanForm
|
||||
layout.Controls.Add(optionsPanel, 0, 1);
|
||||
layout.Controls.Add(split, 0, 2);
|
||||
layout.Controls.Add(statusLabel, 0, 3);
|
||||
layout.Controls.Add(buttonsPanel, 0, 4);
|
||||
layout.Controls.Add(acceptWarningsCheckBox, 0, 4);
|
||||
layout.Controls.Add(buttonsPanel, 0, 5);
|
||||
layout.Dock = DockStyle.Fill;
|
||||
layout.Name = "layout";
|
||||
layout.RowCount = 5;
|
||||
layout.RowCount = 6;
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.TabIndex = 0;
|
||||
//
|
||||
// settingsPanel
|
||||
@@ -190,6 +193,16 @@ partial class CuttingPlanForm
|
||||
statusLabel.Name = "statusLabel";
|
||||
statusLabel.TabIndex = 3;
|
||||
//
|
||||
// acceptWarningsCheckBox
|
||||
//
|
||||
acceptWarningsCheckBox.AutoSize = true;
|
||||
acceptWarningsCheckBox.Dock = DockStyle.Fill;
|
||||
acceptWarningsCheckBox.Name = "acceptWarningsCheckBox";
|
||||
acceptWarningsCheckBox.Text = "I reviewed the warnings. Apply this unverified plan.";
|
||||
acceptWarningsCheckBox.Visible = false;
|
||||
acceptWarningsCheckBox.TabIndex = 4;
|
||||
acceptWarningsCheckBox.CheckedChanged += AcceptWarningsCheckBox_CheckedChanged;
|
||||
//
|
||||
// buttonsPanel
|
||||
//
|
||||
buttonsPanel.AutoSize = true;
|
||||
@@ -199,7 +212,7 @@ partial class CuttingPlanForm
|
||||
buttonsPanel.FlowDirection = FlowDirection.RightToLeft;
|
||||
buttonsPanel.Margin = new Padding(0, 8, 0, 0);
|
||||
buttonsPanel.Name = "buttonsPanel";
|
||||
buttonsPanel.TabIndex = 4;
|
||||
buttonsPanel.TabIndex = 5;
|
||||
buttonsPanel.WrapContents = false;
|
||||
//
|
||||
// applyButton
|
||||
@@ -264,6 +277,7 @@ partial class CuttingPlanForm
|
||||
private Button settingsButton;
|
||||
private FlowLayoutPanel optionsPanel;
|
||||
private CheckBox keepOrderCheckBox;
|
||||
private CheckBox acceptWarningsCheckBox;
|
||||
private Button planButton;
|
||||
private SplitContainer split;
|
||||
private TableLayoutPanel previewLayout;
|
||||
|
||||
@@ -241,7 +241,11 @@ public partial class CuttingPlanForm : Form
|
||||
{
|
||||
proposal = result;
|
||||
summaryBox.Text = string.Join(Environment.NewLine, result.Describe(unit));
|
||||
statusLabel.Text = result.CanApply ? "Review the plan, then apply it." : "Nothing can be applied.";
|
||||
acceptWarningsCheckBox.Checked = false;
|
||||
acceptWarningsCheckBox.Visible = result.RequiresWarningAcceptance;
|
||||
statusLabel.Text = result.CanApply ? "Review the plan, then apply it."
|
||||
: result.RequiresWarningAcceptance ? "Unverified plan: review and accept the warnings to apply."
|
||||
: "Nothing can be applied.";
|
||||
applyButton.Enabled = result.CanApply;
|
||||
var index = Array.IndexOf(plates, activePlate);
|
||||
var plate = index < 0 ? null : result.BuildPreview(index);
|
||||
@@ -252,7 +256,7 @@ public partial class CuttingPlanForm : Form
|
||||
+ "match the editor.";
|
||||
return;
|
||||
}
|
||||
previewLabel.Text = previewText;
|
||||
previewLabel.Text = result.Plates[index].IsReady ? previewText : "UNVERIFIED — " + previewText;
|
||||
preview.Plate = plate;
|
||||
preview.Visible = true;
|
||||
preview.ZoomToFit();
|
||||
@@ -261,6 +265,8 @@ public partial class CuttingPlanForm : Form
|
||||
private void ClearProposal()
|
||||
{
|
||||
proposal = null;
|
||||
acceptWarningsCheckBox.Checked = false;
|
||||
acceptWarningsCheckBox.Visible = false;
|
||||
applyButton.Enabled = false;
|
||||
preview.Visible = false;
|
||||
previewLabel.Text = previewText;
|
||||
@@ -277,6 +283,7 @@ public partial class CuttingPlanForm : Form
|
||||
{
|
||||
settingsButton.Enabled = !running;
|
||||
keepOrderCheckBox.Enabled = !running;
|
||||
acceptWarningsCheckBox.Enabled = !running;
|
||||
planButton.Enabled = !running;
|
||||
if (running)
|
||||
applyButton.Enabled = false;
|
||||
@@ -334,9 +341,15 @@ public partial class CuttingPlanForm : Form
|
||||
public void Report(CuttingPlanProgress value) => context.Post(_ => handler(value), null);
|
||||
}
|
||||
|
||||
private bool CanApplyProposal => proposal?.CanApply == true
|
||||
|| proposal?.CanApplyWithWarnings == true && acceptWarningsCheckBox.Checked;
|
||||
|
||||
private void AcceptWarningsCheckBox_CheckedChanged(object sender, EventArgs e) =>
|
||||
applyButton.Enabled = planning == null && CanApplyProposal;
|
||||
|
||||
private void ApplyButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (planning != null || proposal?.CanApply != true)
|
||||
if (planning != null || !CanApplyProposal)
|
||||
return;
|
||||
if (!TryCheckCanChange(out var reason))
|
||||
{
|
||||
@@ -344,7 +357,7 @@ public partial class CuttingPlanForm : Form
|
||||
return;
|
||||
}
|
||||
|
||||
var commit = proposal.Apply();
|
||||
var commit = proposal.Apply(acceptWarningsCheckBox.Checked);
|
||||
CommitResult = commit;
|
||||
switch (commit.Status)
|
||||
{
|
||||
|
||||
+42
-12
@@ -219,7 +219,9 @@ against owned clean material. Neither mode certifies final NC, production cuttin
|
||||
readiness or physical machine safety.
|
||||
|
||||
Findings and source ordinals use the original zero-based source list, not proposed
|
||||
sequence positions. A non-ready result contains no proposed order or unsafe fallback.
|
||||
sequence positions. Strict service refusals contain no proposed order. The desktop batch may
|
||||
produce a separate `BestEffort` proposal as described below; it never labels it `Ready` or
|
||||
sets `IndependentlyReplayed`.
|
||||
|
||||
- `ConstraintConflict`: fixed programs or explored fixed routing violate the
|
||||
modeled constraints. Locked internal crossings cannot be repaired by regeneration.
|
||||
@@ -293,8 +295,8 @@ plans every plate that has parts. Both open one dialog built on
|
||||
- The dialog starts from the plate's cutting settings (or the last-used settings) and plans
|
||||
at once. `Cutting Settings...` edits them and `Keep the current part order` fixes the
|
||||
whole-part order; either change replans. The settings are confirmed parameters: every
|
||||
unlocked part's lead-ins are regenerated, and locked parts keep programs that must
|
||||
already pass the checks.
|
||||
unlocked part's lead-ins are regenerated. Locked parts keep their exact programs;
|
||||
the strict route checks them, while any best-effort warnings require explicit review below.
|
||||
- A missing or zero-length lead-in is reported directly, rather than as a search-limit
|
||||
failure. Open `Cutting Settings...`, choose a lead-in other than `None` with a nonzero
|
||||
length on the affected `External`, `Internal`, or `Arc / Circle` tab, then replan.
|
||||
@@ -302,11 +304,12 @@ plans every plate that has parts. Both open one dialog built on
|
||||
When a lead hits another part, the finding suggests more spacing or a shorter lead;
|
||||
when no tested entry fits, it suggests reducing lead-in length and, if neighbours
|
||||
obstruct it, spacing the parts farther apart. These are suggestions, not guaranteed
|
||||
fixes: replanning runs the same checks, and Apply stays blocked until every plate is ready.
|
||||
fixes: replanning runs the same strict checks; an unverified fallback requires
|
||||
separate warning acceptance and is not approval to post or cut.
|
||||
- Every plate is captured on the UI thread and checked and planned on a worker. Clean part
|
||||
material is checked for overlaps with the pre-post overlap analyzer; overlapping parts or
|
||||
an incomplete check (see [pre-post verification](post-verification.md)) block that plate
|
||||
whatever its route. A free-order search that ends
|
||||
material is checked for overlaps with the pre-post overlap analyzer; known overlapping parts
|
||||
still block that plate whatever its route. Incomplete checks remain visible warnings, never
|
||||
a clear result (see [pre-post verification](post-verification.md)). A free-order search that ends
|
||||
`NoSolutionWithinBudget` is retried once with the current part order, and the summary
|
||||
says the order was kept. Both are allowed 400 expansions per part (at least the
|
||||
default 20000), because both still plan contour order and entries for every part.
|
||||
@@ -314,11 +317,13 @@ plans every plate that has parts. Both open one dialog built on
|
||||
with their status and findings. Finding part numbers are the plate's current order, as the
|
||||
editor numbers them. The preview shows the active plate detached from the nest (quantity
|
||||
zero, so drawing quantities do not change) in the proposed order with the proposed
|
||||
programs. It is shown only for a ready plate that still matches what was planned: a
|
||||
refused plate may hold program graphs that are unsafe to copy, and a changed one would
|
||||
draw replayed programs at poses that were never checked.
|
||||
- Apply is enabled only when every plate is ready, and it is all or nothing through
|
||||
`CuttingPlanService.Apply`. After it applies, each plate keeps its own copy of the
|
||||
programs. Ready and best-effort proposals can be previewed only while the plate still
|
||||
matches capture. Best-effort previews are labelled `UNVERIFIED`. Refused program graphs
|
||||
are never copied, and changed plates require replanning.
|
||||
- Apply requires usable output for every plate and remains all or nothing. A ready batch
|
||||
can apply immediately; an unverified batch requires the unchecked, per-proposal
|
||||
`I reviewed the warnings. Apply this unverified plan.` checkbox. Replanning clears it.
|
||||
Both use the same owned-program, freshness and rollback boundary. After it applies, each plate keeps its own copy of the
|
||||
confirmed settings, which also become the saved defaults. `Stale` keeps the dialog open
|
||||
and asks for a replan; nothing changes.
|
||||
- Closing or cancelling while planning cancels the worker and keeps the dialog open until
|
||||
@@ -332,6 +337,31 @@ plans every plate that has parts. Both open one dialog built on
|
||||
therefore follow moves and clones just like incremental (G91) programs; displaying a
|
||||
part does not rewrite its program or coordinate mode.
|
||||
|
||||
### Best-effort fallback for imperfect geometry
|
||||
|
||||
When strict planning reports unsupported or incomplete geometry, the desktop batch
|
||||
automatically attempts a bounded, deterministic best-effort proposal. Touching, intersecting
|
||||
or numerically uncertain material boundaries need not prevent lead-in generation when their
|
||||
closed executable contours are readable. The fallback reuses the existing contour emitter:
|
||||
internal contours first, the largest bounding perimeter last, nearest entry from the preceding
|
||||
departure, with the confirmed lead styles. It retains source part order except to satisfy
|
||||
proven cutoff and nested-part prerequisites. `Keep the current part order` still refuses an
|
||||
order that contradicts a proven prerequisite. Uncertain containment is named as a warning.
|
||||
|
||||
No source contours are repaired, removed, simplified, or silently closed. Locked programs
|
||||
and cutoffs remain exact. Unknown/null/recursive instruction graphs, suppressed or nonfinite
|
||||
motions and contours the emitter cannot represent still refuse. This fallback is for
|
||||
incomplete geometry checks, not every constraint conflict or exhausted search. Known
|
||||
inter-part material overlap still blocks the entire batch.
|
||||
|
||||
The result retains strict refusal findings and available rapid-check findings, and is
|
||||
explicitly not a certificate of lead clearance, rapid travel or material containment.
|
||||
Inspect the preview and warnings before accepting it. A route with only an incomplete
|
||||
overlap check may likewise be accepted with warnings. `CanApply` and parameterless batch
|
||||
`Apply()` remain strict; `CanApplyWithWarnings` and `Apply(acceptWarnings: true)` are the
|
||||
explicit review path. `CuttingPlanService.Apply` still rejects best-effort results directly.
|
||||
Acceptance never skips the separate pre-post checks or grants posting consent.
|
||||
|
||||
## Remaining integration boundaries
|
||||
|
||||
The service does not establish clean-material non-overlap, scrap release by open
|
||||
|
||||
Reference in new issue
Block a user