Files
OpenNest/OpenNest.Engine/CuttingPlanning/FixedProgramSearch.cs
T
aj 4bbd8454b5 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.
2026-10-05 00:01:29 -04:00

67 lines
3.0 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
namespace OpenNest.Engine.CuttingPlanning;
/// <summary>Bounded forward depth-first search, nearest feasible branch then source ordinal.</summary>
internal static class FixedProgramSearch
{
internal sealed record Outcome(CuttingPlanStatus Status, IReadOnlyList<int> Order,
IReadOnlyList<PostVerificationFinding> Findings, int Expansions);
internal static Outcome Run(CuttingPlanSnapshot snapshot, CancellationToken token)
{
var stack = new Stack<Frame>();
stack.Push(Create([], snapshot.StartPoint, new ReleasedContourState()));
var rejected = new HashSet<PostVerificationFinding>();
var expansions = 0;
while (stack.Count != 0)
{
token.ThrowIfCancellationRequested();
var frame = stack.Peek();
if (frame.Order.Length == snapshot.Placements.Count)
return new(CuttingPlanStatus.Ready, frame.Order, [], expansions);
if (frame.Next == frame.Candidates.Length)
{
stack.Pop();
continue;
}
if (expansions == snapshot.ExpansionBudget)
return new(CuttingPlanStatus.NoSolutionWithinBudget, [], rejected.ToArray(), expansions);
var candidate = frame.Candidates[frame.Next++];
expansions++;
var placement = snapshot.Placements[candidate];
var checker = frame.Checker.Copy();
var findings = checker.Check(placement.Execution, frame.Position, placement.SourceOrdinal + 1,
placement.IsCutOff, token);
if (findings.Count != 0)
{
rejected.UnionWith(findings);
continue;
}
stack.Push(Create([.. frame.Order, candidate], placement.Execution.DeparturePoint, checker));
}
return new(CuttingPlanStatus.ConstraintConflict, [], rejected.ToArray(), expansions);
Frame Create(int[] order, Vector position, ReleasedContourState checker) => new(order, position, checker,
Enumerable.Range(0, snapshot.Placements.Count).Where(index => !order.Contains(index)
&& 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());
}
private sealed class Frame(int[] order, Vector position, ReleasedContourState checker, int[] candidates)
{
internal int[] Order { get; } = order;
internal Vector Position { get; } = position;
internal ReleasedContourState Checker { get; } = checker;
internal int[] Candidates { get; } = candidates;
internal int Next { get; set; }
}
}