mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 08:21:05 -04:00
fix(cutting): route best-effort holes instead of cutting them in source order
The best-effort fallback (used whenever a part has touching or uncertain material boundaries, which sends the whole plate there) cut each part's holes in raw source order with the nearest entry. On hole grids that order doubles back, so the rapids between holes cross; any crossing means a shorter uncrossed order exists. On A0228-01 plate 1 this cost ~300 in of rapid travel and drew crossing rapids on PT08 in the simulator. Tour the holes with CuttingHoleOrder (the nearest-neighbour + 2-opt/Or-opt proposal the strict search already uses), as an open path from the arrival, perimeter still last. Hole-centre selection is shared with the strict search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
baa5f8481a
commit
a1458a355b
5 files changed
+71
-12
No files matched your search
@@ -9,7 +9,8 @@ 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.
|
||||
/// contour preparation and emission, retaining source part order except for proven
|
||||
/// prerequisites; each part's holes follow a proposed tour, its perimeter last.
|
||||
/// Unknown program semantics, invalid motions and unrepresentable contours still refuse.
|
||||
/// </summary>
|
||||
internal static class BestEffortCuttingPlan
|
||||
@@ -44,7 +45,13 @@ internal static class BestEffortCuttingPlan
|
||||
{
|
||||
var choices = new List<ContourChoice>();
|
||||
var approach = position - source.Location;
|
||||
for (var contour = 0; contour < prepared.Count; contour++)
|
||||
// Source order is not a route: tour the holes from the arrival (the same
|
||||
// nearest-neighbour + 2-opt proposal the search uses) so the rapids between
|
||||
// them do not double back across each other. The perimeter is still last.
|
||||
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal).ToArray();
|
||||
var route = CuttingHoleOrder.Plan(holes, CuttingHoleOrder.Centres(prepared, approach, token),
|
||||
approach, null, token);
|
||||
foreach (var contour in route.Append(prepared.PerimeterOrdinal))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
choices.Add(prepared.ClosestEntry(contour, approach));
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
@@ -18,14 +19,30 @@ namespace OpenNest.Engine.CuttingPlanning;
|
||||
/// </summary>
|
||||
internal static class CuttingHoleOrder
|
||||
{
|
||||
/// <summary>
|
||||
/// One representative point per hole ordinal of <paramref name="prepared"/>, in its LOCAL
|
||||
/// coordinates. The tool arriving inside a cutout from a part nested there says nothing
|
||||
/// useful about that cutout's centre: it is routed from where the tool is instead, before
|
||||
/// a later hop to it has to cross the other parts nested in it.
|
||||
/// </summary>
|
||||
internal static Vector[] Centres(PreparedContours prepared, Vector localArrival, CancellationToken token)
|
||||
{
|
||||
var centres = prepared.HoleCentres(token).Select(c => c ?? Vector.Zero).ToArray();
|
||||
for (var hole = 0; hole < prepared.PerimeterOrdinal; hole++)
|
||||
if (prepared.HoleContains(hole, localArrival))
|
||||
centres[hole] = localArrival;
|
||||
return centres;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Orders <paramref name="holes"/> (stable ordinals, each appearing exactly once in the
|
||||
/// result, never the perimeter) to minimize arrival -> holes -> <paramref
|
||||
/// name="perimeterEntry"/> distance. <paramref name="centresByOrdinal"/> supplies one
|
||||
/// representative point per hole ordinal. Empty holes give an empty order.
|
||||
/// representative point per hole ordinal. Empty holes give an empty order. Without a
|
||||
/// perimeter entry the path is open: it ends at whichever hole keeps it shortest.
|
||||
/// </summary>
|
||||
internal static IReadOnlyList<int> Plan(IReadOnlyList<int> holes,
|
||||
IReadOnlyList<Vector> centresByOrdinal, Vector arrival, Vector perimeterEntry,
|
||||
IReadOnlyList<Vector> centresByOrdinal, Vector arrival, Vector? perimeterEntry,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (holes == null)
|
||||
|
||||
@@ -770,15 +770,9 @@ internal static class JointCuttingPlanSearch
|
||||
{
|
||||
var source = active.Source;
|
||||
var prepared = source.Prepared;
|
||||
var centres = prepared.HoleCentres(token).Select(c => c ?? Vector.Zero).ToArray();
|
||||
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal).ToArray();
|
||||
var localArrival = active.Arrival - source.Location;
|
||||
// The tool arrives inside a cutout from a part nested there. That cutout's centre
|
||||
// says nothing about where its cut can be reached: route it from where the tool is,
|
||||
// before a later hop to it has to cross the other parts nested in it.
|
||||
foreach (var hole in holes)
|
||||
if (prepared.HoleContains(hole, localArrival))
|
||||
centres[hole] = localArrival;
|
||||
var centres = CuttingHoleOrder.Centres(prepared, localArrival, token);
|
||||
var endpoint = perimeter.Point;
|
||||
var entries = new Dictionary<int, ContourChoice>();
|
||||
var pierces = holes.ToDictionary(h => h, h => centres[h]);
|
||||
|
||||
@@ -237,6 +237,45 @@ public class BestEffortCuttingPlanTests
|
||||
Assert.All(parts, p => Assert.False(p.HasManualLeadIns));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Holes_FollowARouteWhoseRapidsDoNotCross()
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(0, 0); program.LineTo(0, 20); program.LineTo(40, 20);
|
||||
program.LineTo(40, 0); program.LineTo(0, 0);
|
||||
// Touching holes force the best-effort plan; the others zig-zag in source order.
|
||||
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);
|
||||
foreach (var (x, y) in new[] { (10, 5), (30, 15), (30, 5), (10, 15), (20, 5), (20, 15) })
|
||||
{
|
||||
program.MoveTo(x - 0.5, y - 0.5); program.LineTo(x + 0.5, y - 0.5); program.LineTo(x + 0.5, y + 0.5);
|
||||
program.LineTo(x - 0.5, y + 0.5); program.LineTo(x - 0.5, y - 0.5);
|
||||
}
|
||||
var part = new Part(new Drawing("zig-zag holes", program), new Vector(1, 1));
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([Plate(part)], ExplicitContourTests.Parameters(), false).Plan();
|
||||
|
||||
var result = Assert.Single(proposal.Plates).Result;
|
||||
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
|
||||
var generated = Assert.Single(result.ProposedOrder);
|
||||
Assert.Equal(9, generated.ContourChoices.Count);
|
||||
var rapids = ExecutionMotionReader.ReadSupported(generated.CopyProgram(), part.Location, null).Motions
|
||||
.Where(m => m.Rapid && m.Start != null).Select(m => (Start: m.Start!.Value, m.End)).ToArray();
|
||||
Assert.Equal(8, rapids.Length);
|
||||
for (var i = 0; i < rapids.Length; i++)
|
||||
for (var j = i + 1; j < rapids.Length; j++)
|
||||
Assert.False(Cross(rapids[i].Start, rapids[i].End, rapids[j].Start, rapids[j].End),
|
||||
$"Rapid {i + 1} crosses rapid {j + 1}.");
|
||||
|
||||
static bool Cross(Vector a, Vector b, Vector c, Vector d)
|
||||
{
|
||||
static double Side(Vector p, Vector q, Vector r) => (q.X - p.X) * (r.Y - p.Y) - (q.Y - p.Y) * (r.X - p.X);
|
||||
return Side(c, d, a) * Side(c, d, b) < 0 && Side(a, b, c) * Side(a, b, d) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData(0)]
|
||||
[InlineData(45)]
|
||||
|
||||
@@ -627,7 +627,9 @@ automatically attempts a bounded, deterministic best-effort proposal. Touching,
|
||||
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
|
||||
departure, with the confirmed lead styles. The internal contours follow the same open
|
||||
nearest-neighbour + 2-opt/Or-opt tour over hole centres that the strict search proposes, not
|
||||
source order, so their rapids do not cross each other. 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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user