mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-12 07:44:21 -04:00
fix(cutting): recover checked routes through interlocking holed parts
This commit is contained in:
1 parent
09dd19ca66
commit
97a468ee2c
4 files changed
+322
-17
No files matched your search
@@ -24,6 +24,7 @@ internal static class JointCuttingPlanSearch
|
||||
/// along its order before it gives up and learns from the part that blocked it.
|
||||
/// </summary>
|
||||
internal const int StallExpansionsPerEntry = 8;
|
||||
private const int MaximumRecoveryHoles = 16;
|
||||
|
||||
/// <summary>LeadPrechecks counts S07 adapter evaluations — bounded work tracked separately from expansions.</summary>
|
||||
internal sealed record Outcome(CuttingPlanStatus Status, IReadOnlyList<FixedProgramPlacement> Order,
|
||||
@@ -46,6 +47,9 @@ internal static class JointCuttingPlanSearch
|
||||
var preferred = walk.Follow(keptSequence, stall, null, preferredOnly: true);
|
||||
if (preferred.Order != null)
|
||||
return walk.Ready(preferred.Order);
|
||||
var recovered = walk.Follow(keptSequence, stall, null, contourRecovery: true);
|
||||
if (recovered.Order != null)
|
||||
return walk.Ready(recovered.Order);
|
||||
}
|
||||
var kept = walk.Follow(keptSequence, null, null);
|
||||
return kept.Order != null ? walk.Ready(kept.Order) : walk.Exhausted();
|
||||
@@ -65,6 +69,9 @@ internal static class JointCuttingPlanSearch
|
||||
var preferred = walk.Follow(sequence, stall, resume, preferredOnly: true);
|
||||
if (preferred.Order != null)
|
||||
return walk.Ready(preferred.Order);
|
||||
var recovered = walk.Follow(sequence, stall, null, contourRecovery: true);
|
||||
if (recovered.Order != null)
|
||||
return walk.Ready(recovered.Order);
|
||||
}
|
||||
var attempt = walk.Follow(sequence, stall, resume);
|
||||
if (attempt.Order != null)
|
||||
@@ -73,6 +80,12 @@ internal static class JointCuttingPlanSearch
|
||||
{
|
||||
// "Cut before" rules are a heuristic: a part blocked straight after another can
|
||||
// still be reachable via a third. Search every order with what budget remains.
|
||||
if (preferEndpoints)
|
||||
{
|
||||
var recovery = walk.Follow(null, stall, null, contourRecovery: true);
|
||||
if (recovery.Order != null)
|
||||
return walk.Ready(recovery.Order);
|
||||
}
|
||||
var full = walk.Follow(null, null, null);
|
||||
return full.Order != null ? walk.Ready(full.Order) : walk.Exhausted();
|
||||
}
|
||||
@@ -189,7 +202,7 @@ internal static class JointCuttingPlanSearch
|
||||
/// With a stall limit the attempt ends once that many expansions pass without getting
|
||||
/// further along the order; it never backtracks behind its starting node.
|
||||
/// </summary>
|
||||
internal Attempt Follow(int[] sequence, int? stall, Node resume, bool preferredOnly = false)
|
||||
internal Attempt Follow(int[] sequence, int? stall, Node resume, bool preferredOnly = false, bool contourRecovery = false)
|
||||
{
|
||||
var root = resume ?? new Node([], snapshot.StartPoint, new ReleasedContourState(reportMissingLeadIns: false), null, null);
|
||||
var attempt = new Attempt(sequence);
|
||||
@@ -211,8 +224,18 @@ internal static class JointCuttingPlanSearch
|
||||
if (attempt.Advance(node))
|
||||
progressExpansions = Expansions;
|
||||
else if (stall is int limit && Expansions - progressExpansions > limit)
|
||||
return attempt;
|
||||
frame.Children ??= OrderedChildren(node, sequence, attempt, preferredOnly).GetEnumerator();
|
||||
{
|
||||
if (sequence != null || !contourRecovery)
|
||||
return attempt;
|
||||
// Diversify the first whole part rather than spending the remainder
|
||||
// on endpoints of a stalled root's already completed prefix.
|
||||
while (stack.Count > 1)
|
||||
stack.Pop().Children?.Dispose();
|
||||
attempt.Restart();
|
||||
progressExpansions = Expansions;
|
||||
continue;
|
||||
}
|
||||
frame.Children ??= OrderedChildren(node, sequence, attempt, preferredOnly, contourRecovery).GetEnumerator();
|
||||
if (!frame.Children.MoveNext())
|
||||
{
|
||||
stack.Pop().Children.Dispose();
|
||||
@@ -269,15 +292,16 @@ internal static class JointCuttingPlanSearch
|
||||
/// one part and contour stage the automatic rank leads — OrderBy(Distance) alone
|
||||
/// would undo the look-ahead facing. Legacy (unranked) children keep distance order.
|
||||
/// </summary>
|
||||
private IEnumerable<Edge> OrderedChildren(Node node, int[] sequence, Attempt attempt, bool preferredOnly)
|
||||
private IEnumerable<Edge> OrderedChildren(Node node, int[] sequence, Attempt attempt, bool preferredOnly, bool contourRecovery)
|
||||
{
|
||||
// A prescribed next part (or an active part's next contour) has a single
|
||||
// source. Expand already yields its contour/entry rank order, so emit and
|
||||
// validate a sibling only when DFS reaches it. Free whole-part selection
|
||||
// still needs every source's distances before it can rank them.
|
||||
if (sequence != null || node.Active != null)
|
||||
return Expand(node, sequence, attempt, preferredOnly);
|
||||
var edges = Expand(node, sequence, attempt, preferredOnly).ToList();
|
||||
// still needs every source's distances before it can rank them. Recovery
|
||||
// instead visits dependency-ready sources lazily by their material centres.
|
||||
if (sequence != null || node.Active != null || contourRecovery)
|
||||
return Expand(node, sequence, attempt, preferredOnly, contourRecovery);
|
||||
var edges = Expand(node, sequence, attempt, preferredOnly, contourRecovery).ToList();
|
||||
if (edges.Count <= 1)
|
||||
return edges.ToArray();
|
||||
// Stable source order: the minimum incremental rapid per source, ties ordinal.
|
||||
@@ -295,7 +319,7 @@ internal static class JointCuttingPlanSearch
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<Edge> Expand(Node node, int[] sequence, Attempt attempt, bool preferredOnly)
|
||||
private IEnumerable<Edge> Expand(Node node, int[] sequence, Attempt attempt, bool preferredOnly, bool contourRecovery)
|
||||
{
|
||||
IEnumerable<FixedProgramPlacement> sources;
|
||||
if (node.Active is { } active)
|
||||
@@ -309,6 +333,8 @@ internal static class JointCuttingPlanSearch
|
||||
: snapshot.Dependencies.IsReady(sequence[node.Order.Length], finished)
|
||||
? [snapshot.Placements[sequence[node.Order.Length]]] : [];
|
||||
}
|
||||
if (contourRecovery)
|
||||
sources = sources.OrderBy(p => Centre(p).DistanceTo(node.Position)).ThenBy(p => p.SourceOrdinal);
|
||||
foreach (var source in sources)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -322,6 +348,12 @@ internal static class JointCuttingPlanSearch
|
||||
continue;
|
||||
}
|
||||
var prepared = source.Prepared;
|
||||
if (contourRecovery && prepared.Count > 1 && prepared.PerimeterOrdinal <= MaximumRecoveryHoles && node.Active == null)
|
||||
{
|
||||
foreach (var edge in RecoverPart(node, sequence, source, attempt))
|
||||
yield return edge;
|
||||
continue;
|
||||
}
|
||||
if (prepared.Count > 1 && node.Active == null)
|
||||
{
|
||||
// Selecting an endpoint does not cut it. Each endpoint owns a separate
|
||||
@@ -420,6 +452,92 @@ internal static class JointCuttingPlanSearch
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery only: solve directed contour transitions rather than assuming
|
||||
// that a centre-to-centre hole tour can cross the already released material.
|
||||
// One complete checked chain per outside endpoint; original DFS remains fallback.
|
||||
private IEnumerable<Edge> RecoverPart(Node node, int[] sequence,
|
||||
FixedProgramPlacement source, Attempt attempt)
|
||||
{
|
||||
var prepared = source.Prepared;
|
||||
var target = LookAheadCentre(node, sequence, source) - source.Location;
|
||||
// Placement origins are authored program coordinates, not a downstream cut.
|
||||
// Use the same material-centre look-ahead as the retained entry search.
|
||||
var perimeters = prepared.AutomaticEntryCandidatesWithFallbacks(prepared.PerimeterOrdinal, token: token)
|
||||
.OrderBy(c => c.Choice.Point.DistanceTo(target ?? node.Position - source.Location))
|
||||
.Where(c => Evaluate(source, c).Status == ContourFeasibilityStatus.Clear).Select(c => c.Choice).ToArray();
|
||||
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal)
|
||||
.Select(c => prepared.AutomaticEntryCandidatesWithFallbacks(c, token: token)
|
||||
.Where(e => Evaluate(source, e).Status == ContourFeasibilityStatus.Clear).Select(e => e.Choice).ToArray()).ToArray();
|
||||
var motions = new Dictionary<ContourChoice, OwnedExecution>();
|
||||
var stop = Expansions + (long)StallExpansionsPerEntry * snapshot.MaxEntries * prepared.Count;
|
||||
OwnedExecution Motion(ContourChoice choice)
|
||||
{
|
||||
if (motions.TryGetValue(choice, out var known)) return known;
|
||||
CountExpansion(source);
|
||||
return motions[choice] = ExecutionMotionReader.Read(
|
||||
prepared.EmitCandidateForValidation(choice), source.Location, null, token);
|
||||
}
|
||||
for (var endpoint = 0; endpoint < perimeters.Length; endpoint++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (Expansions >= stop) yield break;
|
||||
var outside = perimeters[endpoint];
|
||||
var chain = new List<ContourChoice>();
|
||||
// This dominance heuristic is confined to recovery: emitted
|
||||
// seams can differ for the same released subset. Never prune full DFS.
|
||||
var failed = new HashSet<(int Mask, long X, long Y)>();
|
||||
if (!Solve(0, node.Position, node.Checker)) continue;
|
||||
var program = prepared.Emit(chain);
|
||||
var execution = ExecutionMotionReader.Read(program, source.Location, node.Position, token);
|
||||
var checker = node.Checker.Copy();
|
||||
CountExpansion(source);
|
||||
if (!Check(source, execution, node.Position, checker, attempt)) continue;
|
||||
var proposal = source.Propose(program, execution, chain, token);
|
||||
yield return new(new([.. node.Order, proposal], execution.DeparturePoint, checker, null, node),
|
||||
execution.RapidDistanceFrom(node.Position), source.SourceOrdinal, -1, endpoint, endpoint);
|
||||
|
||||
bool Solve(int mask, Vector position, ReleasedContourState before)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (Expansions >= stop) return false;
|
||||
var key = (mask, BitConverter.DoubleToInt64Bits(position.X),
|
||||
BitConverter.DoubleToInt64Bits(position.Y));
|
||||
if (failed.Contains(key)) return false;
|
||||
if (mask == (1 << prepared.PerimeterOrdinal) - 1)
|
||||
{
|
||||
CountExpansion(source);
|
||||
var findings = before.Copy().Check(Motion(outside), position, source.SourceOrdinal + 1, token);
|
||||
rejected.AddRange(Map(snapshot, findings));
|
||||
attempt.NoteCrossings(source.SourceOrdinal, findings);
|
||||
if (findings.Count != 0) { failed.Add(key); return false; }
|
||||
chain.Add(outside);
|
||||
return true;
|
||||
}
|
||||
var candidates = Enumerable.Range(0, holes.Length).Where(c => (mask & (1 << c)) == 0)
|
||||
.SelectMany(c => holes[c])
|
||||
.OrderBy(c => Motion(c).Motions[0].End.DistanceTo(position));
|
||||
foreach (var choice in candidates)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (Expansions >= stop) return false;
|
||||
CountExpansion(source);
|
||||
var execution = Motion(choice);
|
||||
var after = before.Copy();
|
||||
var findings = after.Check(execution, position, source.SourceOrdinal + 1, token);
|
||||
rejected.AddRange(Map(snapshot, findings));
|
||||
attempt.NoteCrossings(source.SourceOrdinal, findings);
|
||||
if (findings.Count != 0) continue;
|
||||
chain.Add(choice);
|
||||
if (Solve(mask | (1 << choice.ContourOrdinal), execution.DeparturePoint, after)) return true;
|
||||
chain.RemoveAt(chain.Count - 1);
|
||||
}
|
||||
failed.Add(key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The S03-S08 pipeline for one outside contour, in prepared LOCAL coordinates
|
||||
/// converted exactly once: rank the (fallback-complemented) catalogue toward the
|
||||
@@ -576,6 +694,13 @@ internal static class JointCuttingPlanSearch
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void Restart()
|
||||
{
|
||||
progress = -1;
|
||||
Deepest = null;
|
||||
Crossed.Clear();
|
||||
}
|
||||
|
||||
internal void NoteCrossings(int ordinal, IEnumerable<PostVerificationFinding> findings)
|
||||
{
|
||||
if (Blocked != ordinal)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Security.Cryptography;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class ContourRecoveryFileTests
|
||||
{
|
||||
// Optional private input: never embed customer geometry or names in the repository.
|
||||
[SkippableTheory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void InterlockedPlateFinishesThroughDesktopBatchWithoutChangingInput(bool lineLeads)
|
||||
{
|
||||
var path = TestConfig.GetExistingPath("InterlockedCuttingNestPath");
|
||||
Skip.If(path == null, "Configure InterlockedCuttingNestPath in ignored test-config.json.");
|
||||
var bytes = SHA256.HashData(File.ReadAllBytes(path!));
|
||||
var nest = new NestReader(path).Read();
|
||||
var plates = nest.Plates.Where(p => p.Parts.Count > 0).ToArray();
|
||||
Assert.NotEmpty(plates);
|
||||
var parts = plates.SelectMany(p => p.Parts).ToArray();
|
||||
var programs = parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)).ToArray();
|
||||
var poses = parts.Select(p => (p.Location, p.Rotation, p.LeadInsLocked)).ToArray();
|
||||
var parameters = Parameters(lineLeads);
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture(plates, parameters, preservePartOrder: false).Plan();
|
||||
|
||||
Assert.True(proposal.CanApply, string.Join(Environment.NewLine, proposal.Describe("in")));
|
||||
Assert.False(proposal.RequiresWarningAcceptance);
|
||||
Assert.Equal(plates.Length, proposal.Plates.Count);
|
||||
foreach (var planned in proposal.Plates)
|
||||
{
|
||||
Assert.True(planned.IsOverlapClear);
|
||||
Assert.True(planned.Result.IndependentlyReplayed);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, planned.Result.Status);
|
||||
Assert.InRange(planned.Result.Expansions, 1, CuttingPlanBatch.PlateBudget(planned.Plate.Parts.Count));
|
||||
Assert.Equal(planned.Plate.Parts.Count, planned.PartCount);
|
||||
var snapshot = planned.Result.Snapshot;
|
||||
foreach (var part in planned.Result.ProposedOrder)
|
||||
{
|
||||
Assert.Equal(snapshot.Placements[part.SourceOrdinal].Prepared.Count, part.ContourChoices.Count);
|
||||
Assert.Equal(part.ContourChoices.Count, part.ContourChoices.Select(c => c.ContourOrdinal).Distinct().Count());
|
||||
}
|
||||
var replay = CuttingPlanService.ReplayPrograms(snapshot, planned.Result.ProposedOrder, 0, default);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
|
||||
Assert.True(replay.IndependentlyReplayed);
|
||||
Assert.Empty(replay.Findings);
|
||||
|
||||
var bounded = CuttingPlanService.Plan(snapshot.WithSearchLimits(1, 16));
|
||||
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, bounded.Status);
|
||||
Assert.Equal(1, bounded.Expansions);
|
||||
Assert.Empty(bounded.ProposedOrder);
|
||||
Assert.False(bounded.IndependentlyReplayed);
|
||||
}
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
var cancelled = CuttingPlanBatch.Capture(plates, parameters, false).Plan(token: cancellation.Token);
|
||||
Assert.True(cancelled.IsCancelled);
|
||||
Assert.False(cancelled.CanApplyWithWarnings);
|
||||
Assert.Equal(parts, plates.SelectMany(p => p.Parts));
|
||||
Assert.Equal(programs, parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
|
||||
Assert.Equal(poses, parts.Select(p => (p.Location, p.Rotation, p.LeadInsLocked)));
|
||||
Assert.Equal(bytes, SHA256.HashData(File.ReadAllBytes(path)));
|
||||
}
|
||||
|
||||
private static CuttingParameters Parameters(bool lineLeads)
|
||||
{
|
||||
var parameters = new CuttingParameters();
|
||||
if (lineLeads)
|
||||
{
|
||||
parameters.ExternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
|
||||
parameters.InternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
|
||||
parameters.ArcCircleLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class InterlockingHoleRouteTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void InterlockingHoledPartsPlanWithinDefaultBudget(bool leads, bool keep)
|
||||
{
|
||||
var a = Polygon([new(2.5, 2), new(2.5, 4), new(30, 4), new(30, 10),
|
||||
new(2.5, 10), new(2.5, 12), new(32, 12), new(32, 2)]);
|
||||
var b = Polygon([new(0, 4.5), new(0, 14.5), new(20, 14.5), new(20, 12.5),
|
||||
new(2, 12.5), new(2, 6.5), new(20, 6.5), new(20, 4.5)]);
|
||||
foreach (var x in new[] { 3.5, 12, 21, 31 })
|
||||
foreach (var y in new[] { 3.0, 11.0 }) Hole(a, x, y);
|
||||
foreach (var x in new[] { 1.0, 7, 13, 19 })
|
||||
foreach (var y in new[] { 5.5, 13.5 }) Hole(b, x, y);
|
||||
var parts = new[] { new Part(new Drawing("left-open bracket", a)), new Part(new Drawing("right-open bracket", b)) };
|
||||
var parameters = new CuttingParameters();
|
||||
if (leads)
|
||||
{
|
||||
parameters.ExternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
|
||||
parameters.InternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
|
||||
parameters.ArcCircleLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
|
||||
}
|
||||
var before = parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)).ToArray();
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(parts,
|
||||
confirmedParameters: parameters, preservePartOrder: keep));
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready,
|
||||
$"{result.Status}: {result.Expansions} " + string.Join(";", result.Findings.Select(f => f.Message)));
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
Assert.Equal(2, result.ProposedOrder.Count);
|
||||
Assert.All(result.ProposedOrder, p => Assert.Equal(9, p.ContourChoices.Count));
|
||||
var replay = CuttingPlanService.ReplayPrograms(snapshot, result.ProposedOrder, 0, default);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
|
||||
Assert.True(replay.IndependentlyReplayed);
|
||||
Assert.Equal(before, parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
|
||||
Assert.InRange(result.Expansions, 1, 20000);
|
||||
if (keep)
|
||||
Assert.Equal(new[] { 0, 1 }, result.ProposedOrder.Select(p => p.SourceOrdinal));
|
||||
var repeated = CuttingPlanService.Plan(snapshot);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, repeated.Status);
|
||||
Assert.Equal(result.Expansions, repeated.Expansions);
|
||||
Assert.Equal(result.ProposedOrder.Select(p => ExplicitContourTests.Fingerprint(p.CopyProgram())),
|
||||
repeated.ProposedOrder.Select(p => ExplicitContourTests.Fingerprint(p.CopyProgram())));
|
||||
|
||||
var bounded = CuttingPlanService.Plan(snapshot.WithSearchLimits(1, 16));
|
||||
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, bounded.Status);
|
||||
Assert.Equal(1, bounded.Expansions);
|
||||
Assert.Empty(bounded.ProposedOrder);
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
var cancelAt = result.Expansions - 1;
|
||||
var cancelled = CuttingPlanService.Plan(new CuttingPlanRequest(parts,
|
||||
confirmedParameters: parameters, preservePartOrder: keep)
|
||||
{
|
||||
ExpansionObserver = n => { if (n == cancelAt) cancellation.Cancel(); }
|
||||
}, cancellation.Token);
|
||||
Assert.Equal(CuttingPlanStatus.Cancelled, cancelled.Status);
|
||||
Assert.Equal(cancelAt, cancelled.Expansions);
|
||||
Assert.Empty(cancelled.ProposedOrder);
|
||||
Assert.False(cancelled.IndependentlyReplayed);
|
||||
Assert.Equal(before, parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
|
||||
}
|
||||
|
||||
private static OpenNest.CNC.Program Polygon(Vector[] vertices)
|
||||
{
|
||||
var program = new OpenNest.CNC.Program();
|
||||
program.MoveTo(vertices[0].X, vertices[0].Y);
|
||||
foreach (var point in vertices.Skip(1).Append(vertices[0])) program.LineTo(point.X, point.Y);
|
||||
return program;
|
||||
}
|
||||
|
||||
private static void Hole(OpenNest.CNC.Program program, double x, double y)
|
||||
{
|
||||
program.MoveTo(x + 0.2, y);
|
||||
program.ArcTo(x + 0.2, y, x, y, RotationType.CCW);
|
||||
}
|
||||
}
|
||||
+24
-8
@@ -99,8 +99,15 @@ each outside endpoint. This lets a later blocked approach change the previous
|
||||
part's departure without first exhausting combinations of its earlier holes.
|
||||
This preferred pass uses the same emitted-motion checks, shared expansion budget
|
||||
and stall limit as the retained search; on failure the full entry/hole-order
|
||||
backtracking pass remains available. Single-part and no-hole requests keep their
|
||||
existing search order.
|
||||
backtracking pass remains available. Before that full pass, a failed preferred tour
|
||||
also gets a directed-contour recovery pass for parts with at most 16 holes. It
|
||||
tries native outside entries beyond the ranked shortlist, then builds a hole route
|
||||
from actual emitted pierces, checking each transition against already released
|
||||
contours. This reaches inner edges of interlocking concave parts which can be
|
||||
missing from the facing-side shortlist. A failed released-hole-subset/departure
|
||||
state is memoized only in recovery; it never prunes the retained full search.
|
||||
Recovered whole programs are checked again and independently replayed. Single-part
|
||||
and no-hole requests keep their existing search order.
|
||||
|
||||
A preserved order is followed as given. Otherwise the order is an open
|
||||
travelling-salesman path over part centres from the start point: nearest neighbour,
|
||||
@@ -114,11 +121,18 @@ getting further, so it learns instead of retrying every entry combination of the
|
||||
parts before it. A rule that would contradict an order already required is skipped.
|
||||
"Cut before" rules are a heuristic (a part blocked straight after another may be
|
||||
reachable via a third), so once nothing new can be learned the remaining budget
|
||||
goes to a full search that tries every ready part, nearest first; only when that
|
||||
also fails is the result a refusal.
|
||||
first tries sequence-free recovery, visiting dependency-ready parts lazily by
|
||||
material-centre distance. When a branch stalls, it disposes the deeper search
|
||||
frames and resumes alternatives at the root instead of spending the remainder
|
||||
on that branch's completed prefix. Any budget left then goes to the retained full
|
||||
search. Recovery shares the original total expansion budget; exhausting it still
|
||||
returns a refusal, never a best-effort upgrade.
|
||||
|
||||
Candidates use native closest points, vertices, midpoints and circle angles in
|
||||
stable order, capped by `maxEntries`. Circle rounding, clamping, corner resolution
|
||||
stable order. `maxEntries` caps the normal ranked shortlist, not the recovery
|
||||
catalogue: recovery may inspect the remaining finite native entries, with every
|
||||
precheck and visited transition charged to the same expansion budget. Circle
|
||||
rounding, clamping, corner resolution
|
||||
and tab trimming happen during emission. Validation uses the actual emitted
|
||||
motions, never the nominal entry point alone. Existing lead styles are not
|
||||
shortened, disabled or substituted as a search fallback.
|
||||
@@ -168,8 +182,9 @@ are not tie breakers.
|
||||
When the next whole part is fixed by the tour or a part is already active, the
|
||||
search generates and checks contour alternatives on demand in contour/entry rank
|
||||
order. It does not emit every sibling before trying the first. Backtracking still
|
||||
visits the remaining alternatives when needed; none is pruned. The free whole-part
|
||||
fallback still evaluates all ready sources before ranking their travel distances.
|
||||
visits the remaining alternatives when needed; none is pruned. The retained full whole-part
|
||||
fallback still evaluates all ready sources before ranking their travel distances;
|
||||
sequence-free recovery instead uses lazy centre-distance order.
|
||||
|
||||
The expansion budget counts evaluated candidates and frontier ranking, including
|
||||
rejections, before emission; it is not a wall-clock timeout. An unvisited sibling
|
||||
@@ -205,7 +220,8 @@ parts the tour stays nearest-first; the look-ahead rank only orders the entries
|
||||
inside one part's contour stage, so distance sorting cannot undo the facing.
|
||||
Uncertain (numerically incomplete) validator answers are not geometric refusals:
|
||||
those candidates can fill remaining retained slots for emitted-prefix checking and
|
||||
complete replay. The total retained entry count stays within `maxEntries`. A
|
||||
complete replay. The normal ranked entry count stays within `maxEntries`; directed recovery can
|
||||
inspect additional native entries as described above. A
|
||||
part/contour with no fitting lead in its fully evaluated catalogue is reported
|
||||
as "No tested lead-in fits on part N, contour M"; budget exhaustion stays a
|
||||
budget finding and incomplete checks are never presented as geometric
|
||||
|
||||
Reference in new issue
Block a user