mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-10 04:15:44 -05:00
fix(cutting): route a part's holes from the end of the segment cut before it
On P230323-02 plate 7 the frame part cut after a cut-off segment took its
upper small hole first, so the rapid up from the segment's end at the
bottom of the sheet crossed the rapid from the lower hole to the window.
The free search plans parts as if segments were absent, so the part's hole
route was ranked from where the tool was before the segment (the previous
part's end, top right), not from where it really is.
After CutOffInsertion places the segments, each holed part cut straight
after one now keeps its outside entry and order but takes the hole route
and preferred hole entries planned from the segment's end, when that
chain has an entry for every hole and passes the checks. The search's
look-through of segments keeps ranking outside entries from the
pre-segment point (so routes stay as planned without the lines) but plans
hole routes from the real position, so a kept-order replan of an applied
plan picks the same holes. Plates without cut-offs are unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0ede9443de)
This commit is contained in:
1 parent
242ef88a97
commit
7474eaa989
3 files changed
+146
-12
No files matched your search
@@ -52,7 +52,7 @@ internal static class JointCuttingPlanSearch
|
||||
{
|
||||
var inserted = CutOffInsertion.Insert(snapshot, outcome.Order, attached, walk.CountExpansion, token);
|
||||
if (inserted != null)
|
||||
return walk.Ready(inserted);
|
||||
return walk.Ready(walk.FollowSegments(inserted));
|
||||
}
|
||||
catch (BudgetExceededException)
|
||||
{
|
||||
@@ -523,8 +523,9 @@ internal static class JointCuttingPlanSearch
|
||||
CountExpansion(source);
|
||||
var checker = node.Checker.Copy();
|
||||
if (Check(cut, cut.Execution, node.Position, checker, attempt))
|
||||
// A segment is transparent: the part after it ranks its entries and hole route
|
||||
// from where the tool was before the segment, as planned without it.
|
||||
// A segment is transparent to the part after it: that part ranks its outside entry
|
||||
// from where the tool was before the segment, as planned without it. Its hole
|
||||
// route starts where the tool is, the segment's end, as FollowSegments plans it.
|
||||
yield return new(new([.. node.Order, cut], cut.Execution.DeparturePoint, checker, null, node,
|
||||
IsSegment(cut) ? node.RoutePosition : null),
|
||||
cut.Execution.RapidDistanceFrom(node.Position), cut.SourceOrdinal, -1, -1);
|
||||
@@ -559,7 +560,7 @@ internal static class JointCuttingPlanSearch
|
||||
}
|
||||
var preference = node.Active?.Preference;
|
||||
if (node.Active?.Perimeter is { } outside && preference == null)
|
||||
preference = PlanHoles(node.Active, outside);
|
||||
preference = PlanHoles(node.Active.Source, node.Active.Boundary.Position, outside);
|
||||
var choices = node.Active?.Choices ?? [];
|
||||
var arrival = node.Active?.Arrival ?? node.Position;
|
||||
var before = node.Active?.Before ?? node.Checker;
|
||||
@@ -1216,12 +1217,13 @@ internal static class JointCuttingPlanSearch
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private HolePreference PlanHoles(ActivePart active, ContourChoice perimeter)
|
||||
// The hole route and preferred hole entries from the tool's global arrival onto the part,
|
||||
// ending at the outside entry.
|
||||
private HolePreference PlanHoles(FixedProgramPlacement source, Vector arrival, ContourChoice perimeter)
|
||||
{
|
||||
var source = active.Source;
|
||||
var prepared = source.Prepared;
|
||||
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal).ToArray();
|
||||
var localArrival = active.Boundary.RoutePosition - source.Location;
|
||||
var localArrival = arrival - source.Location;
|
||||
var centres = CuttingHoleOrder.Centres(prepared, localArrival, token);
|
||||
var endpoint = perimeter.Point;
|
||||
var entries = new Dictionary<int, ContourChoice>();
|
||||
@@ -1255,6 +1257,72 @@ internal static class JointCuttingPlanSearch
|
||||
return new(route, entries, pierces, endpoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The order with segments placed, where each holed part cut straight after a segment
|
||||
/// takes the hole route and entries planned from the segment's end, where the tool
|
||||
/// really is. The free search planned the part as if the segment were not there, from
|
||||
/// where the tool was before it. The part's outside entry and the order stay, so the
|
||||
/// route is still the one planned without the lines; a kept order replanned later plans
|
||||
/// the same holes from the same point. A part keeps its planned holes when the new
|
||||
/// route has no preferred entry for every hole or does not pass the checks.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<FixedProgramPlacement> FollowSegments(IReadOnlyList<FixedProgramPlacement> order)
|
||||
{
|
||||
var followed = order.ToArray();
|
||||
var checker = new ReleasedContourState(reportMissingLeadIns: false);
|
||||
var position = snapshot.StartPoint;
|
||||
try
|
||||
{
|
||||
for (var index = 0; index < followed.Length; index++)
|
||||
{
|
||||
if (index > 0 && IsSegment(followed[index - 1])
|
||||
&& FromSegment(followed[index], position, checker) is { } replanned)
|
||||
followed[index] = replanned;
|
||||
var placement = followed[index];
|
||||
checker.Check(placement.Execution, position, placement.SourceOrdinal + 1, placement.IsCutOff,
|
||||
placement.FixedMotions(placement.Execution), token);
|
||||
position = placement.Execution.DeparturePoint;
|
||||
}
|
||||
}
|
||||
catch (BudgetExceededException)
|
||||
{
|
||||
// The plan is complete without this refinement; keep it as placed.
|
||||
return order;
|
||||
}
|
||||
return followed;
|
||||
}
|
||||
|
||||
// The part re-planned from arrival with its outside entry kept, or null to keep it.
|
||||
private FixedProgramPlacement FromSegment(FixedProgramPlacement placement, Vector arrival,
|
||||
ReleasedContourState checker)
|
||||
{
|
||||
var source = snapshot.Placements[placement.SourceOrdinal];
|
||||
var prepared = source.Prepared;
|
||||
if (prepared is not { HasCutPerimeter: true } || prepared.PerimeterOrdinal == 0
|
||||
|| placement.ContourChoices.Count != prepared.Count
|
||||
|| placement.ContourChoices[^1].ContourOrdinal != prepared.PerimeterOrdinal)
|
||||
return null;
|
||||
var perimeter = placement.ContourChoices[^1];
|
||||
var preference = PlanHoles(source, arrival, perimeter);
|
||||
if (preference.Entries.Count != prepared.PerimeterOrdinal)
|
||||
return null;
|
||||
var choices = preference.Route.Select(hole => preference.Entries[hole]).Append(perimeter).ToArray();
|
||||
Program program;
|
||||
OwnedExecution execution;
|
||||
try
|
||||
{
|
||||
CountExpansion(source);
|
||||
program = prepared.Emit(choices);
|
||||
execution = ExecutionMotionReader.Read(program, source.Location, arrival, token);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or ArithmeticException or NotSupportedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Check(source, execution, arrival, checker.Copy(), new Attempt(null), choices)
|
||||
? source.Propose(program, execution, choices, token) : null;
|
||||
}
|
||||
|
||||
// The placements cut at a node, and the excluded ones, which count as cut throughout.
|
||||
private HashSet<int> Finished(Node node)
|
||||
{
|
||||
@@ -1568,8 +1636,8 @@ internal static class JointCuttingPlanSearch
|
||||
ContourChoice Perimeter = null, HolePreference Preference = null);
|
||||
/// <summary>
|
||||
/// A search state; Previous links a whole-part boundary to the boundary before it. Anchor,
|
||||
/// when set, is where the tool was before the cut-off segments cut last: entries and hole
|
||||
/// routes rank from <see cref="RoutePosition"/>, every rapid is checked from Position.
|
||||
/// when set, is where the tool was before the cut-off segments cut last: outside entries
|
||||
/// rank from <see cref="RoutePosition"/>; hole routes and every rapid start from Position.
|
||||
/// </summary>
|
||||
private sealed record Node(FixedProgramPlacement[] Order, Vector Position, ReleasedContourState Checker,
|
||||
ActivePart Active, Node Previous, Vector? Anchor = null)
|
||||
|
||||
@@ -109,6 +109,65 @@ public class CutOffSegmentPlanningTests
|
||||
Assert.Equal(result.RapidDistance, kept.RapidDistance, 6);
|
||||
}
|
||||
|
||||
// A frame with two small holes on its left strip, one above the other, and a window,
|
||||
// between a locked square A at the top left, cut before it, and a locked square B to its
|
||||
// right, cut after it. A line through the frame's right strip stops halfway up, so its one
|
||||
// segment runs from the bottom edge to just below the frame and is cut straight before it.
|
||||
// Planned without the segment, from A, the frame would cut its upper hole first; the tool
|
||||
// is really at the bottom, so it cuts the lower hole first instead of crossing its own
|
||||
// rapid. Replanned with the order kept, the frame plans the same holes from the same point.
|
||||
[Fact]
|
||||
public void PartAfterSegment_PlansItsHolesFromTheSegmentsEnd()
|
||||
{
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Size = new Size(60, 120);
|
||||
plate.Parts.Add(LockedSquare("A", new Vector(2, 42), topLeft: false));
|
||||
var frame = new Part(new Drawing("frame", Frame()), new Vector(30, 5));
|
||||
plate.Parts.Add(frame);
|
||||
plate.Parts.Add(LockedSquare("B", new Vector(80, 40), topLeft: false));
|
||||
var cutOff = new CutOff(new Vector(65, 0), CutOffAxis.Vertical) { EndLimit = 25 };
|
||||
plate.CutOffs.Add(cutOff);
|
||||
plate.RegenerateCutOffs(new CutOffSettings());
|
||||
PlateSequencing.PlaceCutOffs(plate, [cutOff]);
|
||||
Assert.Single(CutOffSegments.Of(plate, cutOff));
|
||||
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, confirmedParameters: parameters));
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready && result.IndependentlyReplayed, Describe(result));
|
||||
var order = result.ProposedOrder.Select(o => o.SourcePart.BaseDrawing.IsCutOff ? "s" : o.SourcePart.BaseDrawing.Name);
|
||||
Assert.Equal("A s frame B", string.Join(" ", order));
|
||||
var planned = result.ProposedOrder[2];
|
||||
var rapids = planned.Execution.Motions.Where(m => m.Rapid).Select(m => m.End).ToArray();
|
||||
// Into the lower hole (y 14.5 to 15.1 on the sheet), then the upper one (34.5 to 35.1).
|
||||
Assert.InRange(rapids[0].Y, 14, 16);
|
||||
Assert.InRange(rapids[1].Y, 34, 36);
|
||||
|
||||
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
|
||||
var kept = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, confirmedParameters: parameters,
|
||||
preservePartOrder: true));
|
||||
Assert.True(kept.Status == CuttingPlanStatus.Ready && kept.IndependentlyReplayed, Describe(kept));
|
||||
Assert.Equal(result.RapidDistance, kept.RapidDistance, 6);
|
||||
}
|
||||
|
||||
// 40 x 40 with 0.6 square holes at (2.5, 9.5) and (2.5, 29.5) and an 18 x 30 window.
|
||||
private static Program Frame()
|
||||
{
|
||||
var program = new Program();
|
||||
void Loop(params Vector[] points)
|
||||
{
|
||||
program.MoveTo(points[0].X, points[0].Y);
|
||||
foreach (var point in points.Skip(1).Append(points[0]))
|
||||
program.LineTo(point.X, point.Y);
|
||||
}
|
||||
Loop(new(0, 0), new(0, 40), new(40, 40), new(40, 0));
|
||||
foreach (var y in new[] { 29.5, 9.5 })
|
||||
Loop(new(2.5, y), new(3.1, y), new(3.1, y + 0.6), new(2.5, y + 0.6));
|
||||
Loop(new(12, 5), new(30, 5), new(30, 35), new(12, 35));
|
||||
return program;
|
||||
}
|
||||
|
||||
private const double Gap = 0.25, Width = 10.6, Neck = 5.6, Height = 20, Margin = 0.25;
|
||||
// The slanted shoulders need more horizontal room than the gap.
|
||||
private const double Pitch = (Width + Neck) / 2 + 2 * Gap;
|
||||
|
||||
+10
-3
@@ -247,7 +247,14 @@ segment that must precede some part (`CutOffInsertion`) goes as late as it can b
|
||||
the first part in its span of the line, into the latest gap where both rapids cross
|
||||
nothing cut before that point: the part after it keeps the program it was planned with,
|
||||
and only its incoming rapid is checked again from the segment's end. Segments due in the
|
||||
same gap are chained in the order placed, latest-due first. This keeps cutting local:
|
||||
same gap are chained in the order placed, latest-due first. A holed part cut straight
|
||||
after a segment was planned from where the tool was before the segment, but the tool is
|
||||
at the segment's end, often across the part from there: its holes were routed toward the
|
||||
wrong side and the rapid onto its first hole could cross the rapids between the others.
|
||||
So once the segments are placed, each such part keeps its outside entry and its place in
|
||||
the order, and takes the hole route and preferred hole entries planned from the
|
||||
segment's end; it keeps its planned holes when that route has no preferred entry for
|
||||
every hole or fails the checks. This keeps cutting local:
|
||||
the segment below a row of parts is cut while the route passes it, not up front, and
|
||||
never before a part it need not precede unless the geometry allows nothing later (a
|
||||
segment whose pierce is walled off by every part cut after a point goes before that
|
||||
@@ -258,8 +265,8 @@ separator or a line through open scrap precedes nothing and is planned like a pa
|
||||
separator after everything, the other at its tour position.
|
||||
|
||||
Wherever a segment sits in an order, the search looks through it: the part before it
|
||||
faces the next part, not the segment, and the part after it ranks its entries and hole
|
||||
route from where the tool was before the segment, while every rapid is still checked
|
||||
faces the next part, not the segment, and the part after it ranks its outside entry
|
||||
from where the tool was before the segment, while its hole route and every rapid start
|
||||
from the segment's end. A kept order with segments where a plan put them (an applied
|
||||
plan replanned with `Keep the current part order`) therefore gets the entries and
|
||||
routes the plan had, and replays Ready again.
|
||||
|
||||
Reference in new issue
Block a user