mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 13:32:13 -04:00
feat(cutting): order cutoffs and nested parts before their hosts
The cutting planner now accepts cutoffs on plate-scoped requests and plans whole-part prerequisites captured from owned values: - A cutoff precedes every part its nominal span crosses, using the same rule and drawing-reference matching as automatic sequencing; a cutoff without a definition precedes every part. Cutoffs stay fixed programs, need no lead-in and never become rapid obstacles; rapids into and out of them are still checked. - A part proven, on native clean material, to lie inside a cutout of another part precedes that host. Touching or crossing boundaries are ambiguous and refuse; a part in a concave pocket has no dependency. - Both searches only expand ready parts, a preserved order that breaks a prerequisite is a constraint conflict, and final replay rechecks the captured prerequisites instead of trusting the search.
This commit is contained in:
1 parent
f3bca26fb9
commit
4bbd8454b5
8 files changed
+589
-34
No files matched your search
@@ -21,7 +21,14 @@ public sealed class ReleasedContourState
|
||||
|
||||
/// <summary>Consumes one whole owned program; part numbers are caller identity keys.</summary>
|
||||
public IReadOnlyList<PostVerificationFinding> Check(OwnedExecution execution, Vector? arrival,
|
||||
int partNumber, CancellationToken token = default)
|
||||
int partNumber, CancellationToken token = default) => Check(execution, arrival, partNumber, false, token);
|
||||
|
||||
/// <summary>
|
||||
/// As the analyzer does for cutoffs: rapids are checked, but cutoff cuts need no lead-in and,
|
||||
/// being open, never become obstacles.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PostVerificationFinding> Check(OwnedExecution execution, Vector? arrival,
|
||||
int partNumber, bool cutoff, CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(execution);
|
||||
if (arrival is { } point)
|
||||
@@ -29,7 +36,7 @@ public sealed class ReleasedContourState
|
||||
var moves = execution.Motions.ToArray();
|
||||
moves[0] = moves[0].WithStart(arrival);
|
||||
var findings = new List<PostVerificationFinding>();
|
||||
AnalyzeMoves(moves, false, findings, 1, partNumber, token);
|
||||
AnalyzeMoves(moves, cutoff, findings, 1, partNumber, token);
|
||||
return findings.AsReadOnly();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>Whole-part cutting prerequisites shared by automatic sequencing and cutting planning.</summary>
|
||||
internal static class CuttingDependencyGraph
|
||||
/// <summary>
|
||||
/// Whole-part cutting prerequisites for one captured plate. A cutoff precedes every part its
|
||||
/// nominal span crosses (an orphaned cutoff, whose definition is missing, precedes every part),
|
||||
/// and a part proven to lie inside a cutout of another part precedes that host. Built on the
|
||||
/// caller thread from owned values; ambiguous containment refuses instead of guessing from bounds.
|
||||
/// </summary>
|
||||
internal sealed class CuttingDependencyGraph
|
||||
{
|
||||
private const int ContactBudget = 1000000;
|
||||
private readonly int[][] prerequisites;
|
||||
|
||||
private CuttingDependencyGraph(int[][] prerequisites) => this.prerequisites = prerequisites;
|
||||
|
||||
internal static CuttingDependencyGraph Empty(int count) =>
|
||||
new(Enumerable.Range(0, count).Select(_ => Array.Empty<int>()).ToArray());
|
||||
|
||||
/// <summary>Exact prerequisites, for tests and callers that already proved them.</summary>
|
||||
internal static CuttingDependencyGraph FromPrerequisites(IReadOnlyList<int[]> prerequisites)
|
||||
{
|
||||
var copy = prerequisites.Select(p => p.Distinct().Order().ToArray()).ToArray();
|
||||
if (copy.SelectMany(p => p).Any(i => i < 0 || i >= copy.Length))
|
||||
throw new ArgumentException("Prerequisite ordinal out of range.");
|
||||
return new(copy);
|
||||
}
|
||||
|
||||
internal int Count => prerequisites.Length;
|
||||
|
||||
internal IReadOnlyList<int> PrerequisitesOf(int ordinal) => prerequisites[ordinal];
|
||||
|
||||
internal bool IsReady(int ordinal, IEnumerable<int> done)
|
||||
{
|
||||
if (prerequisites[ordinal].Length == 0)
|
||||
return true;
|
||||
var finished = done as ICollection<int> ?? done.ToHashSet();
|
||||
return prerequisites[ordinal].All(finished.Contains);
|
||||
}
|
||||
|
||||
/// <summary>First (part, missing prerequisite) in a proposed complete order, or null.</summary>
|
||||
internal (int Part, int Prerequisite)? FirstViolation(IReadOnlyList<int> order)
|
||||
{
|
||||
var position = Enumerable.Repeat(-1, prerequisites.Length).ToArray();
|
||||
for (var i = 0; i < order.Count; i++)
|
||||
position[order[i]] = i;
|
||||
foreach (var part in order)
|
||||
foreach (var prerequisite in prerequisites[part])
|
||||
if (position[prerequisite] < 0 || position[prerequisite] > position[part])
|
||||
return (part, prerequisite);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>An ordinal on a prerequisite cycle, or null when the graph is acyclic.</summary>
|
||||
internal int? FindCycle()
|
||||
{
|
||||
var remaining = prerequisites.Select(p => p.Length).ToArray();
|
||||
var dependents = Enumerable.Range(0, prerequisites.Length).Select(_ => new List<int>()).ToArray();
|
||||
for (var part = 0; part < prerequisites.Length; part++)
|
||||
foreach (var prerequisite in prerequisites[part])
|
||||
dependents[prerequisite].Add(part);
|
||||
var ready = new Queue<int>(Enumerable.Range(0, prerequisites.Length).Where(i => remaining[i] == 0));
|
||||
while (ready.Count != 0)
|
||||
foreach (var dependent in dependents[ready.Dequeue()])
|
||||
if (--remaining[dependent] == 0)
|
||||
ready.Enqueue(dependent);
|
||||
var cyclic = Array.FindIndex(remaining, r => r != 0);
|
||||
return cyclic < 0 ? null : cyclic;
|
||||
}
|
||||
|
||||
internal static CuttingDependencyGraph Build(IReadOnlyList<DependencyNode> nodes, Box plate,
|
||||
CancellationToken token)
|
||||
{
|
||||
var edges = nodes.Select(_ => new SortedSet<int>()).ToArray();
|
||||
for (var cut = 0; cut < nodes.Count; cut++)
|
||||
{
|
||||
if (!nodes[cut].IsCutOff)
|
||||
continue;
|
||||
var definition = nodes[cut].CutOff;
|
||||
for (var part = 0; part < nodes.Count; part++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (!nodes[part].IsCutOff && (definition == null || CutOffCrosses(definition.Axis,
|
||||
definition.Position, definition.StartLimit, definition.EndLimit, nodes[part].PlacedBounds, plate)))
|
||||
edges[part].Add(cut);
|
||||
}
|
||||
}
|
||||
for (var inner = 0; inner < nodes.Count; inner++)
|
||||
for (var host = 0; host < nodes.Count; host++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
var graph = new CuttingDependencyGraph(edges.Select(e => e.ToArray()).ToArray());
|
||||
if (graph.FindCycle() is { } cyclic)
|
||||
throw new CuttingDependencyException(CuttingPlanStatus.ConstraintConflict, cyclic, null,
|
||||
$"Part {cyclic + 1} is on a cutting dependency cycle.");
|
||||
return graph;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a cutoff's nominal line crosses a part's bounds within the cutoff's span. Uses the
|
||||
/// nominal line, not its trimmed cutting segments (which deliberately skip the parts). Bounds
|
||||
@@ -28,4 +131,82 @@ internal static class CuttingDependencyGraph
|
||||
|| System.Math.Max(start, end) < alongMin - Tolerance.Epsilon
|
||||
|| System.Math.Min(start, end) > alongMax + Tolerance.Epsilon);
|
||||
}
|
||||
|
||||
// True when the inner part's perimeter lies strictly inside one cutout of the host. Any
|
||||
// boundary contact or crossing between them, or incomplete material, is ambiguous.
|
||||
private static bool InsideCutout(IReadOnlyList<DependencyNode> nodes, int inner, int host,
|
||||
CancellationToken token)
|
||||
{
|
||||
var part = nodes[inner].Material;
|
||||
var enclosing = nodes[host].Material;
|
||||
if (!part.IsComplete || !enclosing.IsComplete)
|
||||
throw Ambiguous(inner, host, part.Reason ?? enclosing.Reason);
|
||||
var budget = ContactBudget;
|
||||
foreach (var curve in part.Rings[0])
|
||||
foreach (var ring in enclosing.Rings)
|
||||
foreach (var boundary in ring)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (--budget < 0)
|
||||
throw Ambiguous(inner, host, "the containment check exceeds the native query limit");
|
||||
if (curve.Contacts(boundary, out var overlap).Count != 0 || overlap)
|
||||
throw Ambiguous(inner, host, "their boundaries touch or cross");
|
||||
}
|
||||
var probe = part.Rings[0][0].Start;
|
||||
return enclosing.Rings.Skip(1).Any(hole => LeadMaterialSnapshot.Inside(probe, hole, token));
|
||||
}
|
||||
|
||||
private static CuttingDependencyException Ambiguous(int inner, int host, string reason) =>
|
||||
new(CuttingPlanStatus.UnsupportedGeometry, inner, host,
|
||||
$"Containment of part {inner + 1} relative to part {host + 1} cannot be established: {reason}.");
|
||||
|
||||
private static bool Within(Box inner, Box outer) =>
|
||||
inner.Left >= outer.Left - Tolerance.Epsilon && inner.Right <= outer.Right + Tolerance.Epsilon
|
||||
&& inner.Bottom >= outer.Bottom - Tolerance.Epsilon && inner.Top <= outer.Top + Tolerance.Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>Owned copy of a cutoff definition's nominal line.</summary>
|
||||
internal sealed record CutOffDefinition(CutOffAxis Axis, Vector Position, double? StartLimit, double? EndLimit);
|
||||
|
||||
/// <summary>Owned per-placement dependency input; material is captured only when a pair needs it.</summary>
|
||||
internal sealed class DependencyNode
|
||||
{
|
||||
private readonly Func<LeadMaterialSnapshot> materialFactory;
|
||||
private LeadMaterialSnapshot material;
|
||||
|
||||
internal DependencyNode(bool isCutOff, CutOffDefinition cutOff, Box placedBounds, Box cleanBounds,
|
||||
Func<LeadMaterialSnapshot> materialFactory)
|
||||
{
|
||||
IsCutOff = isCutOff;
|
||||
CutOff = cutOff;
|
||||
PlacedBounds = Copy(placedBounds);
|
||||
CleanBounds = cleanBounds == null ? null : Copy(cleanBounds);
|
||||
HostBounds = cleanBounds == null ? PlacedBounds : Union(PlacedBounds, CleanBounds);
|
||||
this.materialFactory = materialFactory;
|
||||
}
|
||||
|
||||
internal bool IsCutOff { get; }
|
||||
internal CutOffDefinition CutOff { get; }
|
||||
internal Box PlacedBounds { get; }
|
||||
internal Box CleanBounds { get; }
|
||||
internal Box HostBounds { get; }
|
||||
internal LeadMaterialSnapshot Material => material ??= materialFactory();
|
||||
|
||||
private static Box Copy(Box box) => new(box.X, box.Y, box.Length, box.Width);
|
||||
|
||||
private static Box Union(Box a, Box b)
|
||||
{
|
||||
var left = System.Math.Min(a.Left, b.Left);
|
||||
var bottom = System.Math.Min(a.Bottom, b.Bottom);
|
||||
return new(left, bottom, System.Math.Max(a.Right, b.Right) - left, System.Math.Max(a.Top, b.Top) - bottom);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A dependency refusal naming the part and, when relevant, the other part.</summary>
|
||||
internal sealed class CuttingDependencyException(CuttingPlanStatus status, int ordinal, int? other, string message)
|
||||
: Exception(message)
|
||||
{
|
||||
internal CuttingPlanStatus Status { get; } = status;
|
||||
internal int Ordinal { get; } = ordinal;
|
||||
internal int? Other { get; } = other;
|
||||
}
|
||||
@@ -60,11 +60,12 @@ 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)
|
||||
CuttingParameters ownedParameters = null, CuttingDependencyGraph dependencies = null)
|
||||
{
|
||||
PlateState = plateState;
|
||||
OwnedParameters = ownedParameters;
|
||||
Placements = Array.AsReadOnly(placements.ToArray());
|
||||
Dependencies = dependencies ?? CuttingDependencyGraph.Empty(Placements.Count);
|
||||
StartPoint = startPoint;
|
||||
ExpansionBudget = expansionBudget;
|
||||
Failure = failure;
|
||||
@@ -75,6 +76,8 @@ public sealed class CuttingPlanSnapshot
|
||||
ExpansionObserver = expansionObserver;
|
||||
}
|
||||
|
||||
/// <summary>Whole-part prerequisites by source ordinal (cutoffs, inner parts before hosts).</summary>
|
||||
internal CuttingDependencyGraph Dependencies { get; }
|
||||
/// <summary>Exact captured plate state for plate-scoped requests; null for detached part lists.</summary>
|
||||
internal PlateCuttingState PlateState { get; }
|
||||
/// <summary>Owned copy of the confirmed parameters taken at capture; never the caller's object.</summary>
|
||||
@@ -135,9 +138,12 @@ public sealed class FixedProgramPlacement
|
||||
}
|
||||
return new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution,
|
||||
OwnedProgramCopy.Copy(proposed, token), Prepared, Material, choices)
|
||||
{ SelectedProgram = selected };
|
||||
{ SelectedProgram = selected, IsCutOff = IsCutOff };
|
||||
}
|
||||
|
||||
/// <summary>A cutoff: always a fixed open-cut program, never material or regenerated.</summary>
|
||||
public bool IsCutOff { get; internal init; }
|
||||
|
||||
public Part SourcePart { get; }
|
||||
public int SourceOrdinal { get; }
|
||||
public Vector Location { get; }
|
||||
|
||||
@@ -44,6 +44,12 @@ public static class CuttingPlanService
|
||||
if (part == null || !request.Parts.Any(p => ReferenceEquals(p, part)) || !eligible.Add(part))
|
||||
throw new ArgumentException("Foreign or duplicate eligible placement.");
|
||||
}
|
||||
// Cutoff definitions by drawing reference, as automatic sequencing matches them.
|
||||
var definitions = new Dictionary<Drawing, CutOff>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var cutOff in request.Plate?.CutOffs ?? [])
|
||||
if (cutOff?.Drawing != null)
|
||||
definitions[cutOff.Drawing] = cutOff;
|
||||
var nodes = new List<DependencyNode>();
|
||||
// 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++)
|
||||
@@ -54,12 +60,28 @@ public static class CuttingPlanService
|
||||
if (source?.BaseDrawing == null || source.Program == null || !double.IsFinite(source.Rotation)
|
||||
|| !identities.Add(source))
|
||||
throw new ArgumentException("Missing/duplicate source placement or invalid pose.");
|
||||
if (source.BaseDrawing.IsCutOff)
|
||||
throw new NotSupportedException("Cutoff dependency ordering is outside the fixed-program route slice.");
|
||||
// Validate both original graphs before Clone or any virtual transform can
|
||||
// erase unsupported runtime semantics, including fixed/ineligible targets.
|
||||
OwnedProgramCopy.Validate(source.BaseDrawing.Program, token);
|
||||
OwnedProgramCopy.Validate(source.Program, token);
|
||||
if (source.BaseDrawing.IsCutOff)
|
||||
{
|
||||
// A cutoff is a fixed open cut: never material, never regenerated.
|
||||
if (request.Plate == null)
|
||||
throw new NotSupportedException("Cutoff dependencies require a plate-scoped request.");
|
||||
if (eligible.Contains(source))
|
||||
throw new ArgumentException("A cutoff cannot be eligible for regeneration.");
|
||||
var cut = ExecutionMotionReader.ReadSupported(source.Program, source.Location, request.StartPoint, token);
|
||||
if (!cut.HasCuttingContour)
|
||||
throw new ArgumentException("Placed cutoff program has no nonzero cutting motions.");
|
||||
nodes.Add(new(true, definitions.TryGetValue(source.BaseDrawing, out var definition)
|
||||
? new(definition.Axis, definition.Position, definition.StartLimit, definition.EndLimit) : null,
|
||||
source.BoundingBox, null, null));
|
||||
placements.Add(new(source, index, source.Location, source.Rotation, source.LeadInsLocked,
|
||||
cut, OwnedProgramCopy.Copy(source.Program, token))
|
||||
{ IsCutOff = true });
|
||||
continue;
|
||||
}
|
||||
var clean = ExecutionMotionReader.ReadSupported(source.BaseDrawing.Program, Vector.Zero, null, token);
|
||||
if (!clean.HasCuttingContour)
|
||||
throw new NotSupportedException("Scribe-only or noncutting source drawings are outside this route slice.");
|
||||
@@ -69,35 +91,52 @@ public static class CuttingPlanService
|
||||
var ownedProgram = OwnedProgramCopy.Copy(source.Program, token);
|
||||
PreparedContours prepared = null;
|
||||
LeadMaterialSnapshot material = null;
|
||||
// Geometry-only transform: original graphs were strictly validated and
|
||||
// clone expansion bounded above. Legacy Rotate visits per parent, so
|
||||
// retain Clone's per-parent sharing here instead of restoring diamonds.
|
||||
// Exact placed/proposed payloads still use lossless OwnedProgramCopy.
|
||||
var ownedClean = (Program)source.BaseDrawing.Program.Clone();
|
||||
ownedClean.Rotate(source.Rotation - source.BaseDrawing.Program.Rotation);
|
||||
var cleanBounds = ownedClean.BoundingBox();
|
||||
cleanBounds.Offset(source.Location);
|
||||
var location = source.Location;
|
||||
if (request.ConfirmedParameters != null)
|
||||
{
|
||||
// Geometry-only transform: original graphs were strictly validated and
|
||||
// clone expansion bounded above. Legacy Rotate visits per parent, so
|
||||
// retain Clone's per-parent sharing here instead of restoring diamonds.
|
||||
// Exact placed/proposed payloads still use lossless OwnedProgramCopy.
|
||||
var ownedClean = (Program)source.BaseDrawing.Program.Clone();
|
||||
ownedClean.Rotate(source.Rotation - source.BaseDrawing.Program.Rotation);
|
||||
material = LeadMaterialSnapshot.Capture(ownedClean, source.Location, token);
|
||||
material = LeadMaterialSnapshot.Capture(ownedClean, 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);
|
||||
}
|
||||
var captured = material;
|
||||
// Fixed-route material is captured only for a pair whose bounds admit containment.
|
||||
nodes.Add(new(false, null, source.BoundingBox, cleanBounds,
|
||||
() => captured ?? LeadMaterialSnapshot.Capture(ownedClean, location, 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);
|
||||
source = null;
|
||||
ordinal = null;
|
||||
var dependencies = CuttingDependencyGraph.Build(nodes, request.Plate?.BoundingBox(includeParts: false), token);
|
||||
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));
|
||||
: OwnedCuttingParameters.Copy(request.ConfirmedParameters),
|
||||
dependencies: dependencies);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return Failure(CuttingPlanStatus.Cancelled, "Capture cancelled.");
|
||||
}
|
||||
catch (CuttingDependencyException exception)
|
||||
{
|
||||
var other = exception.Other is { } index ? request.Parts[index] : null;
|
||||
return new(placements, request.StartPoint, request.ExpansionBudget, exception.Status,
|
||||
[new(exception.Ordinal, request.Parts[exception.Ordinal], exception.Other, other, null, exception.Message)]);
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
return Failure(CuttingPlanStatus.UnsupportedGeometry, exception.Message);
|
||||
@@ -172,6 +211,10 @@ public static class CuttingPlanService
|
||||
if (snapshot.Placements.Count == 0)
|
||||
return snapshot.PlateState == null ? new(CuttingPlanStatus.InvalidInput)
|
||||
: new(CuttingPlanStatus.Ready, independentlyReplayed: true); // Empty plate: unchanged no-op.
|
||||
if (snapshot.PreservePartOrder
|
||||
&& snapshot.Dependencies.FirstViolation(Enumerable.Range(0, snapshot.Placements.Count).ToArray()) is { } manual)
|
||||
return new(CuttingPlanStatus.ConstraintConflict,
|
||||
findings: [DependencyFinding(snapshot, manual, "The preserved part order")]);
|
||||
try
|
||||
{
|
||||
var fixedFindings = new List<CuttingPlanFinding>();
|
||||
@@ -182,7 +225,7 @@ public static class CuttingPlanService
|
||||
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);
|
||||
placement.SourceOrdinal + 1, placement.IsCutOff, token);
|
||||
fixedFindings.AddRange(Map(snapshot, findings));
|
||||
}
|
||||
if (fixedFindings.Count != 0)
|
||||
@@ -215,6 +258,10 @@ public static class CuttingPlanService
|
||||
if (order.Count != snapshot.Placements.Count || order.Distinct().Count() != order.Count
|
||||
|| order.Any(index => index < 0 || index >= snapshot.Placements.Count))
|
||||
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
|
||||
// Dependencies are rechecked from the captured graph, not trusted from the search.
|
||||
if (snapshot.Dependencies.FirstViolation(order) is { } violation)
|
||||
return new(CuttingPlanStatus.InvalidInput,
|
||||
findings: [DependencyFinding(snapshot, violation, "The proposed order")], expansions: expansions);
|
||||
var checker = new ReleasedContourState();
|
||||
var position = snapshot.StartPoint;
|
||||
var distance = 0.0;
|
||||
@@ -223,7 +270,8 @@ public static class CuttingPlanService
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var placement = snapshot.Placements[index];
|
||||
findings.AddRange(checker.Check(placement.Execution, position, placement.SourceOrdinal + 1, token));
|
||||
findings.AddRange(checker.Check(placement.Execution, position, placement.SourceOrdinal + 1,
|
||||
placement.IsCutOff, token));
|
||||
distance += placement.Execution.RapidDistanceFrom(position);
|
||||
position = placement.Execution.DeparturePoint;
|
||||
}
|
||||
@@ -245,11 +293,14 @@ public static class CuttingPlanService
|
||||
|| 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);
|
||||
if (snapshot.Dependencies.FirstViolation(order.Select(p => p.SourceOrdinal).ToArray()) is { } violation)
|
||||
return new(CuttingPlanStatus.InvalidInput,
|
||||
findings: [DependencyFinding(snapshot, violation, "The proposed order")], 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();
|
||||
var materials = snapshot.Placements.Where(p => !p.IsCutOff).Select(p => p.Material).ToArray();
|
||||
foreach (var proposal in order)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -258,6 +309,7 @@ public static class CuttingPlanService
|
||||
|| !SameBits(source.Location.X, proposal.Location.X) || !SameBits(source.Location.Y, proposal.Location.Y)
|
||||
|| !SameBits(source.Rotation, proposal.Rotation) || source.LeadInsLocked != proposal.LeadInsLocked
|
||||
|| !ReferenceEquals(source.Prepared, proposal.Prepared) || !ReferenceEquals(source.Material, proposal.Material)
|
||||
|| source.IsCutOff != proposal.IsCutOff
|
||||
|| snapshot.PreservePartOrder && proposal.SourceOrdinal != order.TakeWhile(p => !ReferenceEquals(p, proposal)).Count())
|
||||
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
|
||||
if (source.Prepared == null)
|
||||
@@ -273,7 +325,9 @@ public static class CuttingPlanService
|
||||
try
|
||||
{
|
||||
execution = ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, position, token);
|
||||
complete = ContourProgramVerifier.Verify(execution, source.Material, proposal.SelectedProgram, token);
|
||||
// A cutoff is an open cut with no material; it is retained exactly (checked above).
|
||||
complete = source.IsCutOff
|
||||
|| ContourProgramVerifier.Verify(execution, source.Material, proposal.SelectedProgram, token);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
|
||||
{
|
||||
@@ -281,8 +335,9 @@ public static class CuttingPlanService
|
||||
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);
|
||||
checker.Check(execution, position, source.SourceOrdinal + 1, source.IsCutOff, token)));
|
||||
var lead = source.IsCutOff ? new LeadPathValidationResult(true, true, null)
|
||||
: LeadPathValidator.Check(execution, source.Material, materials, token);
|
||||
if (!lead.IsComplete || !lead.IsClear)
|
||||
findings.Add(JointCuttingPlanSearch.Finding(source,
|
||||
lead.IsComplete ? null : PostVerificationKind.Incomplete, lead.Reason));
|
||||
@@ -303,6 +358,16 @@ public static class CuttingPlanService
|
||||
rapidDistance: distance, independentlyReplayed: true);
|
||||
}
|
||||
|
||||
private static CuttingPlanFinding DependencyFinding(CuttingPlanSnapshot snapshot,
|
||||
(int Part, int Prerequisite) violation, string subject)
|
||||
{
|
||||
var part = snapshot.Placements[violation.Part];
|
||||
var prerequisite = snapshot.Placements[violation.Prerequisite];
|
||||
var reason = prerequisite.IsCutOff ? "the cutoff crossing it" : "the part nested in its cutout";
|
||||
return new(part.SourceOrdinal, part.SourcePart, prerequisite.SourceOrdinal, prerequisite.SourcePart, null,
|
||||
$"{subject} cuts part {part.SourceOrdinal + 1} before {reason} (part {prerequisite.SourceOrdinal + 1}).");
|
||||
}
|
||||
|
||||
private static bool SameBits(double source, double proposed) =>
|
||||
BitConverter.DoubleToInt64Bits(source) == BitConverter.DoubleToInt64Bits(proposed);
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ internal static class FixedProgramSearch
|
||||
expansions++;
|
||||
var placement = snapshot.Placements[candidate];
|
||||
var checker = frame.Checker.Copy();
|
||||
var findings = checker.Check(placement.Execution, frame.Position, placement.SourceOrdinal + 1, token);
|
||||
var findings = checker.Check(placement.Execution, frame.Position, placement.SourceOrdinal + 1,
|
||||
placement.IsCutOff, token);
|
||||
if (findings.Count != 0)
|
||||
{
|
||||
rejected.UnionWith(findings);
|
||||
@@ -48,6 +49,7 @@ internal static class FixedProgramSearch
|
||||
|
||||
Frame Create(int[] order, Vector position, ReleasedContourState checker) => new(order, position, checker,
|
||||
Enumerable.Range(0, snapshot.Placements.Count).Where(index => !order.Contains(index)
|
||||
&& snapshot.Dependencies.IsReady(index, order)
|
||||
&& (!snapshot.PreservePartOrder || index == order.Length))
|
||||
.OrderBy(index => snapshot.Placements[index].Execution.RapidDistanceFrom(position))
|
||||
.ThenBy(index => snapshot.Placements[index].SourceOrdinal).ToArray());
|
||||
|
||||
@@ -19,7 +19,7 @@ internal static class JointCuttingPlanSearch
|
||||
{
|
||||
var expansions = 0;
|
||||
var rejected = new List<CuttingPlanFinding>();
|
||||
var materials = snapshot.Placements.Select(p => p.Material).ToArray();
|
||||
var materials = snapshot.Placements.Where(p => !p.IsCutOff).Select(p => p.Material).ToArray();
|
||||
var stack = new Stack<Frame>();
|
||||
stack.Push(new(new([], snapshot.StartPoint, new ReleasedContourState(), null)));
|
||||
try
|
||||
@@ -60,6 +60,7 @@ internal static class JointCuttingPlanSearch
|
||||
{
|
||||
var sources = node.Active is { } active ? new[] { active.Source }
|
||||
: snapshot.Placements.Where(p => !node.Order.Any(o => o.SourceOrdinal == p.SourceOrdinal)
|
||||
&& snapshot.Dependencies.IsReady(p.SourceOrdinal, node.Order.Select(o => o.SourceOrdinal).ToArray())
|
||||
&& (!snapshot.PreservePartOrder || p.SourceOrdinal == node.Order.Length));
|
||||
foreach (var source in sources)
|
||||
{
|
||||
@@ -129,9 +130,11 @@ internal static class JointCuttingPlanSearch
|
||||
|
||||
bool Check(FixedProgramPlacement source, OwnedExecution execution, Vector arrival, ReleasedContourState checker)
|
||||
{
|
||||
var findings = checker.Check(execution, arrival, source.SourceOrdinal + 1, token);
|
||||
var findings = checker.Check(execution, arrival, source.SourceOrdinal + 1, source.IsCutOff, token);
|
||||
rejected.AddRange(Map(snapshot, findings));
|
||||
var lead = LeadPathValidator.Check(execution, source.Material, materials, token);
|
||||
// A fixed cutoff has no material or leads to certify; its rapids are still checked.
|
||||
var lead = source.IsCutOff ? new LeadPathValidationResult(true, true, null)
|
||||
: 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;
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class CuttingDependencyTests
|
||||
{
|
||||
// Started just above P, nearest-first cuts P before the far cutoff start. P leaves upward
|
||||
// and the cutoff's trimmed cut stops short of P, so P, cutoff, Q crosses nothing: only the
|
||||
// nominal-span dependency puts the cutoff first.
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Plan_CutOffWhoseNominalSpanCrossesAPart_IsCutFirstAndAppliedThatWay(bool regenerate)
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var (_, plate, p, q, cut) = CutOffPlate(parameters, orphan: false);
|
||||
var request = new CuttingPlanRequest(plate, new Vector(11, 12.5),
|
||||
confirmedParameters: regenerate ? parameters : null);
|
||||
var snapshot = CuttingPlanService.Capture(request);
|
||||
Assert.Equal(new[] { 2 }, snapshot.Dependencies.PrerequisitesOf(0));
|
||||
Assert.Empty(snapshot.Dependencies.PrerequisitesOf(1));
|
||||
Assert.Empty(snapshot.Dependencies.PrerequisitesOf(2));
|
||||
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
var order = result.ProposedOrder.Select(o => o.SourcePart).ToList();
|
||||
Assert.True(order.IndexOf(cut) < order.IndexOf(p), string.Join(",", result.ProposedOrder.Select(o => o.SourceOrdinal)));
|
||||
Assert.Equal(regenerate, result.ProposedOrder.Single(o => ReferenceEquals(o.SourcePart, p)).IsRegenerated);
|
||||
Assert.False(result.ProposedOrder.Single(o => ReferenceEquals(o.SourcePart, cut)).IsRegenerated);
|
||||
|
||||
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
|
||||
Assert.Equal(order, plate.Parts);
|
||||
Assert.Contains(q, plate.Parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_OrphanedCutOff_PrecedesEveryPart()
|
||||
{
|
||||
var (_, plate, p, q, cut) = CutOffPlate(ExplicitContourTests.Parameters(), orphan: true);
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(plate, new Vector(11, 12.5)));
|
||||
Assert.Equal(new[] { 2 }, snapshot.Dependencies.PrerequisitesOf(0));
|
||||
Assert.Equal(new[] { 2 }, snapshot.Dependencies.PrerequisitesOf(1));
|
||||
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.Same(cut, result.ProposedOrder[0].SourcePart);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_PreservedOrderCuttingAPartBeforeItsCutOff_IsAConstraintConflict()
|
||||
{
|
||||
var (_, plate, p, _, cut) = CutOffPlate(ExplicitContourTests.Parameters(), orphan: false);
|
||||
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest(plate, new Vector(11, 12.5), preservePartOrder: true));
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.ConstraintConflict, result.Status);
|
||||
Assert.Empty(result.ProposedOrder);
|
||||
var finding = Assert.Single(result.Findings);
|
||||
Assert.Same(p, finding.SourcePart);
|
||||
Assert.Same(cut, finding.OtherSourcePart);
|
||||
Assert.Contains("cutoff", finding.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Replay_RechecksDependenciesInsteadOfTrustingTheSearch(bool regenerate)
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var (_, plate, _, _, _) = CutOffPlate(parameters, orphan: false);
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(plate, new Vector(11, 12.5),
|
||||
confirmedParameters: regenerate ? parameters : null));
|
||||
var ready = CuttingPlanService.Plan(snapshot);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, ready.Status);
|
||||
// Same verified placements, counterfeit order: crossed part before its cutoff.
|
||||
var counterfeit = ready.ProposedOrder.OrderBy(o => o.SourceOrdinal).ToArray();
|
||||
|
||||
var replayed = regenerate
|
||||
? CuttingPlanService.ReplayPrograms(snapshot, counterfeit, 0, default)
|
||||
: CuttingPlanService.Replay(snapshot, counterfeit.Select(o => o.SourceOrdinal).ToArray(), 0, default);
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.InvalidInput, replayed.Status);
|
||||
Assert.Empty(replayed.ProposedOrder);
|
||||
Assert.Contains(replayed.Findings, f => f.SourceOrdinal == 0 && f.OtherSourceOrdinal == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_DetachedListWithACutOff_IsUnsupported()
|
||||
{
|
||||
var (_, plate, _, _, _) = CutOffPlate(ExplicitContourTests.Parameters(), orphan: false);
|
||||
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest(plate.Parts.ToArray()));
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
|
||||
Assert.Contains("plate-scoped", Assert.Single(result.Findings).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_PartInsideAHostCutout_IsCutBeforeTheHost()
|
||||
{
|
||||
var (plate, host, inner) = NestedPlate(Rectangle(2.7, 2.7, 0.6, 0.6));
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(plate));
|
||||
Assert.Equal(new[] { 1 }, snapshot.Dependencies.PrerequisitesOf(0));
|
||||
Assert.Empty(snapshot.Dependencies.PrerequisitesOf(1));
|
||||
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.Equal(new[] { inner, host }, result.ProposedOrder.Select(o => o.SourcePart));
|
||||
|
||||
var preserved = CuttingPlanService.Plan(new CuttingPlanRequest(plate, preservePartOrder: true));
|
||||
Assert.Equal(CuttingPlanStatus.ConstraintConflict, preserved.Status);
|
||||
var finding = Assert.Single(preserved.Findings);
|
||||
Assert.Same(host, finding.SourcePart);
|
||||
Assert.Same(inner, finding.OtherSourcePart);
|
||||
Assert.Contains("nested in its cutout", finding.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_PartStraddlingAHostCutoutEdge_IsAmbiguousContainment()
|
||||
{
|
||||
var (plate, host, inner) = NestedPlate(Rectangle(3.6, 2.8, 0.8, 0.4));
|
||||
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest(plate));
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
|
||||
var finding = Assert.Single(result.Findings);
|
||||
Assert.Same(inner, finding.SourcePart);
|
||||
Assert.Same(host, finding.OtherSourcePart);
|
||||
Assert.Contains("Containment", finding.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_PartInAConcavePocketOutsideTheHost_HasNoDependency()
|
||||
{
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
var host = Pocketed();
|
||||
var inner = Rectangle(4.5, 5, 1, 1);
|
||||
plate.Parts.Add(host);
|
||||
plate.Parts.Add(inner);
|
||||
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(plate));
|
||||
|
||||
Assert.Null(snapshot.Findings.FirstOrDefault()?.Message);
|
||||
Assert.Empty(snapshot.Dependencies.PrerequisitesOf(0));
|
||||
Assert.Empty(snapshot.Dependencies.PrerequisitesOf(1));
|
||||
Assert.Equal(CuttingPlanStatus.Ready, CuttingPlanService.Plan(snapshot).Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DependencyGraph_ReportsCyclesAndFirstViolation()
|
||||
{
|
||||
Assert.NotNull(CuttingDependencyGraph.FromPrerequisites([[1], [0], []]).FindCycle());
|
||||
var chain = CuttingDependencyGraph.FromPrerequisites([[], [0], [1]]);
|
||||
Assert.Null(chain.FindCycle());
|
||||
Assert.Null(chain.FirstViolation([0, 1, 2]));
|
||||
Assert.Equal((2, 1), chain.FirstViolation([0, 2, 1]));
|
||||
Assert.True(chain.IsReady(1, [0]));
|
||||
Assert.False(chain.IsReady(2, [0]));
|
||||
}
|
||||
|
||||
private static (Nest, Plate, Part P, Part Q, Part Cut) CutOffPlate(CuttingParameters parameters, bool orphan)
|
||||
{
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
var p = Emitted(parameters, new Vector(10, 10));
|
||||
var q = Emitted(parameters, new Vector(40, 20));
|
||||
var cutOff = new CutOff(new Vector(11, 0), CutOffAxis.Vertical) { StartLimit = 0, EndLimit = 30 };
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(11, 30));
|
||||
program.Codes.Add(new LinearMove(11, 12.5) { Layer = LayerType.Cut });
|
||||
cutOff.Drawing.Program = program;
|
||||
if (!orphan)
|
||||
plate.CutOffs.Add(cutOff);
|
||||
var cut = new Part(cutOff.Drawing);
|
||||
plate.Parts.Add(p);
|
||||
plate.Parts.Add(q);
|
||||
plate.Parts.Add(cut);
|
||||
return (nest, plate, p, q, cut);
|
||||
}
|
||||
|
||||
// A regenerable 2x2 square with its emitted lead at the top-edge midpoint.
|
||||
private static Part Emitted(CuttingParameters parameters, Vector location)
|
||||
{
|
||||
var clean = LeadPathValidationTests.Rectangle(0, 0, 2, 2);
|
||||
var prepared = PreparedContours.Capture(clean, parameters);
|
||||
var part = new Part(new Drawing("same", clean), location);
|
||||
Assert.True(part.RestoreLeadInProgram(prepared.Emit([prepared.ClosestEntry(0, new Vector(1, 3))]), false));
|
||||
part.CuttingParameters = parameters;
|
||||
return part;
|
||||
}
|
||||
|
||||
// Host: 10x10 square with circular cutouts of radius 1 at (3,3) and (7,3), cut hole, hole,
|
||||
// perimeter from the right so its own program has no completed-contour crossing.
|
||||
private static (Plate, Part Host, Part Inner) NestedPlate(Part inner)
|
||||
{
|
||||
var clean = PreparedContourTests.Holes();
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var prepared = PreparedContours.Capture(clean, parameters);
|
||||
var program = prepared.Emit(
|
||||
[
|
||||
prepared.Entry(0, 0, new Vector(4, 3)),
|
||||
prepared.Entry(1, 0, new Vector(8, 3)),
|
||||
prepared.ClosestEntry(2, new Vector(12, 3))
|
||||
]);
|
||||
var host = new Part(new Drawing("same", clean));
|
||||
Assert.True(host.RestoreLeadInProgram(program, false));
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Parts.Add(host);
|
||||
plate.Parts.Add(inner);
|
||||
return (plate, host, inner);
|
||||
}
|
||||
|
||||
// A U-shaped part open at the top between x 3 and 7, lead on its right edge.
|
||||
private static Part Pocketed()
|
||||
{
|
||||
var outline = new[] { (10.0, 0.0), (0.0, 0.0), (0.0, 10.0), (3.0, 10.0), (3.0, 3.0), (7.0, 3.0),
|
||||
(7.0, 10.0), (10.0, 10.0), (10.0, 5.0) };
|
||||
var clean = new Program();
|
||||
clean.MoveTo(10, 5);
|
||||
foreach (var (x, y) in outline)
|
||||
clean.LineTo(x, y);
|
||||
var placed = new Program();
|
||||
placed.MoveTo(10.25, 5);
|
||||
placed.Codes.Add(new LinearMove(10, 5) { Layer = LayerType.Leadin });
|
||||
foreach (var (x, y) in outline)
|
||||
placed.LineTo(x, y);
|
||||
placed.Codes.Add(new LinearMove(10.25, 5) { Layer = LayerType.Leadout });
|
||||
var part = new Part(new Drawing("same", clean));
|
||||
Assert.True(part.RestoreLeadInProgram(placed, false));
|
||||
return part;
|
||||
}
|
||||
|
||||
// Fixed-program rectangle at (x, y), contour starting at its right-edge midpoint with a
|
||||
// 0.25 straight lead-in and lead-out to the right.
|
||||
private static Part Rectangle(double x, double y, double width, double height)
|
||||
{
|
||||
var clean = new Program();
|
||||
clean.MoveTo(width, height / 2);
|
||||
Contour(clean, width, height);
|
||||
var part = new Part(new Drawing("same", clean), new Vector(x, y));
|
||||
var placed = new Program();
|
||||
placed.MoveTo(width + 0.25, height / 2);
|
||||
placed.Codes.Add(new LinearMove(width, height / 2) { Layer = LayerType.Leadin });
|
||||
Contour(placed, width, height);
|
||||
placed.Codes.Add(new LinearMove(width + 0.25, height / 2) { Layer = LayerType.Leadout });
|
||||
Assert.True(part.RestoreLeadInProgram(placed, false));
|
||||
return part;
|
||||
}
|
||||
|
||||
private static void Contour(Program program, double width, double height)
|
||||
{
|
||||
program.LineTo(width, 0);
|
||||
program.LineTo(0, 0);
|
||||
program.LineTo(0, height);
|
||||
program.LineTo(width, height);
|
||||
program.LineTo(width, height / 2);
|
||||
}
|
||||
|
||||
private static string Describe(CuttingPlanResult r) =>
|
||||
$"{r.Status}, expanded {r.Expansions}: " + string.Join("; ", r.Findings.Select(f => f.Message));
|
||||
}
|
||||
+28
-8
@@ -50,6 +50,27 @@ flags, symbolic bindings and shared subprogram identity; unsupported graphs are
|
||||
refused. The geometry-only clean transform uses per-parent copies so legacy rotation
|
||||
does not visit a globally shared descendant twice; it never changes the fixed payload.
|
||||
|
||||
## Cutting dependencies
|
||||
|
||||
Capture builds whole-part prerequisites from owned values, and both the search and
|
||||
the final replay enforce them:
|
||||
|
||||
- A cutoff precedes every part its nominal line crosses within its span, using the
|
||||
same rule as automatic sequencing: nominal position and limits against the part's
|
||||
placed bounds, matched by drawing reference, never by name or trimmed segments. A
|
||||
cutoff whose definition is missing precedes every part. Cutoffs need a
|
||||
plate-scoped request; they are always fixed programs, need no lead-in, are not
|
||||
material for lead validation and, being open cuts, never become rapid obstacles.
|
||||
Rapids into and out of them are still checked.
|
||||
- A part whose perimeter lies strictly inside a cutout of another part precedes
|
||||
that host. Bounds only select candidate pairs; containment is proven on native
|
||||
clean material. Touching or crossing boundaries, or material that cannot be
|
||||
captured, refuse as `UnsupportedGeometry` naming both parts. A part in a concave
|
||||
pocket outside the host's material has no dependency.
|
||||
- A preserved manual order that violates a prerequisite, or a cycle, is a
|
||||
`ConstraintConflict`. Replay rechecks the captured prerequisites and refuses a
|
||||
violating order rather than trusting the search.
|
||||
|
||||
## Search and exact output
|
||||
|
||||
With regeneration, the bounded deterministic search considers whole-part order,
|
||||
@@ -107,8 +128,8 @@ sequence positions. A non-ready result contains no proposed order or unsafe fall
|
||||
- `ConstraintConflict`: fixed programs or explored fixed routing violate the
|
||||
modeled constraints. Locked internal crossings cannot be repaired by regeneration.
|
||||
- `UnsupportedGeometry`: unsupported motion/material semantics or an incomplete
|
||||
check. Open nominal outlines, ambiguous release states, cutoff dependencies and
|
||||
scribe-only source drawings are not silently accepted.
|
||||
check. Open nominal outlines, ambiguous release states or containment, cutoffs
|
||||
in a detached part list and scribe-only source drawings are not silently accepted.
|
||||
- `InvalidInput`: malformed/missing/duplicate placements or settings, invalid
|
||||
geometry, empty input, invalid eligibility or nonpositive bounds.
|
||||
- `NoSolutionWithinBudget`: the bounded/capped search found no complete proposal;
|
||||
@@ -154,13 +175,12 @@ on an `Applied` result, not as a rollback.
|
||||
|
||||
## Remaining integration boundaries
|
||||
|
||||
The service does not establish clean-material non-overlap, inner-part-before-host
|
||||
release dependencies, cutoff order or physical retention strength. It does not
|
||||
write CNC or set posting consent. A `Ready` proposal can still be unsuitable for
|
||||
cutting.
|
||||
The service does not establish clean-material non-overlap, scrap release by open
|
||||
cutoff cuts or sheet edges, or physical retention strength. It does not write CNC
|
||||
or set posting consent. A `Ready` proposal can still be unsuitable for cutting.
|
||||
|
||||
Later slices add containment/cutoff dependencies, then desktop integration
|
||||
(including `PartsReordered` refresh hooks) and legacy automatic-path retirement. Windows interaction,
|
||||
Later slices add desktop integration (including `PartsReordered` refresh hooks)
|
||||
and legacy automatic-path retirement. Windows interaction,
|
||||
supplied-job coverage and actual posted order remain separate acceptance gates.
|
||||
Fresh [pre-post verification](post-verification.md) is still required; it is not a
|
||||
physical safety qualification.
|
||||
|
||||
Reference in new issue
Block a user