mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 05:01:05 -04:00
feat(planning): keep support tabs out of inside corners and report weak tabs
The best tab point used to land in inside corners: equally good points run along a straight edge, vertices were tried first and the first tie won. Operators cannot grind a nub flush there. - SupportAnalyzer finds inside corners (inward-turning vertex runs of at least 30 degrees, so chorded fillets count) and keeps tab points a clearance from them; of equally good points it picks the one farthest from an inside corner. Plan Cutting and the overlay use twice the tab size; AcceptsTabAt rejects closer points - When no steadying entry has a lead-in that fits, the search falls back to entries clear of inside corners, then to every entry, each ordered by how well the tab steadies the part, keeping only the steadiest that fit - Replay notes parts whose planned tab cannot hold them level or sits in an inside corner; the notes are advisory and leave a Ready plan Ready - Console/MCP plate summaries print a ready plan's notes - docs/support-tabs.md and cutting-planner notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a7c0747e11
commit
f51e5aefdd
13 files changed
+435
-52
No files matched your search
@@ -75,6 +75,7 @@ internal static class BestEffortCuttingPlan
|
||||
f.OtherPartNumber is int otherOrdinal ? snapshot.Placements[otherOrdinal - 1].SourcePart : null,
|
||||
f.Kind, f.Message)
|
||||
{ Witness = f }));
|
||||
findings.AddRange(SupportTabCapture.Notes(proposal));
|
||||
distance += execution.RapidDistanceFrom(position);
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentException("Best-effort travel is not finite.");
|
||||
|
||||
@@ -460,6 +460,9 @@ public sealed class CuttingPlanProposal
|
||||
yield return heading + $"ready. {Count(plate.PartCount, "part")}, {plate.RegeneratedCount} "
|
||||
+ $"regenerated, {kept} kept as is; rapid travel "
|
||||
+ $"{result.RapidDistance.ToString("0.##", CultureInfo.CurrentCulture)} {unit}. {order}";
|
||||
// A ready plan's findings are advisory notes (support tabs that may not hold a part level).
|
||||
foreach (var line in Limit(result.Findings.Select(DescribeFinding)))
|
||||
yield return line;
|
||||
yield break;
|
||||
}
|
||||
|
||||
|
||||
@@ -405,12 +405,14 @@ public static class CuttingPlanService
|
||||
return new(findings.Any(f => f.Kind == PostVerificationKind.Incomplete)
|
||||
? CuttingPlanStatus.UnsupportedGeometry : CuttingPlanStatus.ConstraintConflict,
|
||||
findings: findings, expansions: expansions);
|
||||
// Support tabs that cannot hold their part level are noted, never refused.
|
||||
var notes = order.SelectMany(SupportTabCapture.Notes).ToList();
|
||||
// Every planner-chosen motion passed. Fixed-emission warnings make the proposal
|
||||
// UNVERIFIED: never Ready, applicable only through explicit warning acceptance.
|
||||
if (warnings.Count != 0)
|
||||
return new(CuttingPlanStatus.BestEffort, order, warnings.Distinct(), expansions,
|
||||
return new(CuttingPlanStatus.BestEffort, order, warnings.Concat(notes).Distinct(), expansions,
|
||||
distance, independentlyReplayed: true);
|
||||
return new(CuttingPlanStatus.Ready, order, expansions: expansions,
|
||||
return new(CuttingPlanStatus.Ready, order, notes, expansions: expansions,
|
||||
rapidDistance: distance, independentlyReplayed: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -803,18 +803,21 @@ internal static class JointCuttingPlanSearch
|
||||
private IReadOnlyList<ContourChoice> SelectEntries(FixedProgramPlacement source, int contour,
|
||||
Vector? target, Vector arrival, Vector[] sightTargets = null)
|
||||
{
|
||||
var ordered = RankedEntries(source, contour, target, arrival, sightTargets, ignoreSupport: false);
|
||||
var ordered = RankedEntries(source, contour, target, arrival, sightTargets, SupportEntries.Steadying);
|
||||
var selection = ContourEntrySelection.Select(ordered,
|
||||
candidate => Evaluate(source, candidate), snapshot.MaxEntries, token);
|
||||
// No steadying point of a support-tabbed perimeter has a lead-in that fits: it is
|
||||
// still tabbed, wherever its configured lead-ins fit.
|
||||
if (contour == source.Prepared.PerimeterOrdinal && source.Prepared.HasSupportTab
|
||||
&& selection.Choices.Count == 0 && selection.UncertainChoices.Count == 0)
|
||||
{
|
||||
ordered = RankedEntries(source, contour, target, arrival, sightTargets, ignoreSupport: true);
|
||||
selection = ContourEntrySelection.Select(ordered,
|
||||
candidate => Evaluate(source, candidate), snapshot.MaxEntries, token);
|
||||
}
|
||||
// still tabbed, where a lead-in fits and the tab steadies it most, clear of inside
|
||||
// corners unless no entry clear of them fits.
|
||||
if (contour == source.Prepared.PerimeterOrdinal && source.Prepared.HasSupportTab)
|
||||
foreach (var fallback in new[] { SupportEntries.ClearOfInsideCorners, SupportEntries.Any })
|
||||
{
|
||||
if (selection.Choices.Count != 0 || selection.UncertainChoices.Count != 0)
|
||||
break;
|
||||
ordered = RankedEntries(source, contour, target, arrival, sightTargets, fallback);
|
||||
selection = Steadiest(source.Prepared, ContourEntrySelection.Select(ordered,
|
||||
candidate => Evaluate(source, candidate), snapshot.MaxEntries, token));
|
||||
}
|
||||
// No configured lead-in fits the perimeter: with a pierce diameter, try open scrap.
|
||||
string scrapReason = null;
|
||||
if (contour == source.Prepared.PerimeterOrdinal && selection.Choices.Count == 0
|
||||
@@ -840,15 +843,33 @@ internal static class JointCuttingPlanSearch
|
||||
return selection.Choices.Concat(selection.UncertainChoices).Take(snapshot.MaxEntries).ToArray();
|
||||
}
|
||||
|
||||
// One contour's automatic catalogue ranked toward the next cut, before lead checks.
|
||||
// A support fallback's selection keeps only the fitting entries whose tab steadies the
|
||||
// part most: every one that holds it level, or else those as good as the best.
|
||||
private static ContourSelectionResult Steadiest(PreparedContours prepared, ContourSelectionResult selection)
|
||||
{
|
||||
var fitting = selection.Choices.Concat(selection.UncertainChoices).ToArray();
|
||||
if (fitting.Length == 0)
|
||||
return selection;
|
||||
var best = fitting.Max(prepared.SupportMarginAt);
|
||||
bool Keep(ContourChoice choice) => prepared.HoldsLevelAt(choice)
|
||||
|| prepared.SupportMarginAt(choice) >= best - OpenNest.Math.Tolerance.Epsilon && !fitting.Any(prepared.HoldsLevelAt);
|
||||
return selection with
|
||||
{
|
||||
Choices = selection.Choices.Where(Keep).ToArray(),
|
||||
UncertainChoices = selection.UncertainChoices.Where(Keep).ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
// One contour's automatic catalogue ranked toward the next cut, before lead checks. A
|
||||
// support fallback leads with the entries whose tab steadies the part most.
|
||||
private IReadOnlyList<ContourEntryCandidate> RankedEntries(FixedProgramPlacement source, int contour,
|
||||
Vector? target, Vector arrival, Vector[] sightTargets, bool ignoreSupport)
|
||||
Vector? target, Vector arrival, Vector[] sightTargets, SupportEntries supportEntries)
|
||||
{
|
||||
// Last contour has no downstream target, but still needs its exact native
|
||||
// closest-arrival fallback. Keep target null in the ranker: arrival is not
|
||||
// a next cut and must not acquire facing-side priority.
|
||||
var catalogue = source.Prepared.AutomaticEntryCandidatesWithFallbacks(contour, target ?? arrival, token,
|
||||
ignoreSupport);
|
||||
supportEntries);
|
||||
var ordered = catalogue.RankTowardNextCut(target, arrival);
|
||||
// With no lead-out the tool leaves the part from its outside entry. From inside a
|
||||
// slot or pocket the straight way to the next cut runs back over the part just
|
||||
@@ -864,6 +885,8 @@ internal static class JointCuttingPlanSearch
|
||||
// pierce clears the hole itself lead.
|
||||
if (target is { } next && contour != source.Prepared.PerimeterOrdinal)
|
||||
ordered = ordered.DepartingClear(source.Prepared, next, token);
|
||||
if (supportEntries != SupportEntries.Steadying && contour == source.Prepared.PerimeterOrdinal)
|
||||
ordered = ordered.OrderByDescending(c => source.Prepared.SupportMarginAt(c.Choice)).ToList();
|
||||
return ordered;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
@@ -24,11 +27,40 @@ internal static class SupportTabCapture
|
||||
return prepared;
|
||||
token.ThrowIfCancellationRequested();
|
||||
// The clean program is the drawing's own, rotated as placed: its material at the
|
||||
// part's location is the material that drops onto the bed.
|
||||
// part's location is the material that drops onto the bed. The tab keeps clear of
|
||||
// inside corners by twice its size.
|
||||
var support = SupportAnalyzer.Analyze(request.SupportBed, request.SupportQuadrant,
|
||||
SupportMaterial.FromProgram(ownedClean, location));
|
||||
SupportMaterial.FromProgram(ownedClean, location),
|
||||
SupportAnalyzer.InsideCornerClearance(parameters.TabConfig.Size));
|
||||
if (!support.IsComplete || support.IsSupported || support.BestTabPoint == null)
|
||||
return prepared;
|
||||
return prepared.WithSupportTab(support, location, parameters.TabConfig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notes for a planned support-tabbed part whose tab, at its perimeter entry, cannot hold it
|
||||
/// level or sits in an inside corner. Advisory: they never refuse or downgrade a plan.
|
||||
/// </summary>
|
||||
internal static IEnumerable<CuttingPlanFinding> Notes(FixedProgramPlacement planned)
|
||||
{
|
||||
var prepared = planned.Prepared;
|
||||
if (prepared?.Support is not { } support
|
||||
|| planned.ContourChoices.FirstOrDefault(c => c.ContourOrdinal == prepared.PerimeterOrdinal) is not { } entry)
|
||||
yield break;
|
||||
var tab = entry.Point + planned.Location;
|
||||
var at = FormattableString.Invariant($"({tab.X:0.####}, {tab.Y:0.####})");
|
||||
if (!support.TabCanSupport)
|
||||
yield return JointCuttingPlanSearch.Finding(planned, null,
|
||||
$"The support grid cannot hold it level, and no single tab can: its tab at {at} steadies it "
|
||||
+ "as far as one tab can, but it may still tip. Add tabs by hand or move the part, then replan.");
|
||||
else if (!prepared.HoldsLevelAt(entry))
|
||||
yield return JointCuttingPlanSearch.Finding(planned, null,
|
||||
$"The support grid cannot hold it level, and no lead-in fits where a tab would: its tab at {at} "
|
||||
+ "is the steadiest that fits, but it may still tip. Shorten the lead-in in Cutting Settings or "
|
||||
+ "space the parts farther apart, then replan.");
|
||||
if (!prepared.IsClearOfInsideCorners(entry))
|
||||
yield return JointCuttingPlanSearch.Finding(planned, null,
|
||||
$"Its support tab at {at} is in an inside corner, because no lead-in fits clear of one; "
|
||||
+ "its nub will be hard to grind flush.");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user