Files
OpenNest/OpenNest.Engine/CuttingPlanning/SupportTabCapture.cs
T
aj 6e3705eb89 feat(planning): cut holes inside a display perimeter that is never cut
Some jobs cut only cutouts into a supplied blank, such as instrument holes
in a purchased door that is the whole sheet. Its outline has to be known
to the planner (the blank is material that leads, rapids and cut-offs must
respect) but must never be pierced, led or cut.

A new DisplayPerimeter layer marks that outline. It is material, so
nesting, containment, lead clearance and automatic cut-offs all see the
blank; a door that fills its sheet leaves cut-offs nothing to cut. Cutting
preparation keeps it out of the contours entirely: with no cut perimeter
every contour is a hole with internal leads, and the search, best-effort
and safest-route paths stop offering a perimeter. Replay refuses any run
along the display perimeter and completes once every other ring is cut.

A display perimeter must be one whole closed contour enclosing every other
contour; a mixed ring, one inside another contour, or a second one is
refused with that reason. Saving, drawing and posting it are still to do.
2026-10-22 19:32:22 -04:00

97 lines
5.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
using OpenNest.Support;
namespace OpenNest.Engine.CuttingPlanning;
/// <summary>
/// Capture-time support tabs for one regenerated part (docs/support-tabs.md): with a support
/// bed and <see cref="CNC.CuttingStrategy.CuttingParameters.SupportTabs"/> on, a part the bed
/// cannot hold level has its perimeter tabbed where the tab steadies it; a pinned perimeter
/// keeps its pin (the drawing wins) and only carries the analysis for replay to review. A
/// supported part, a missing tab configuration or material the analyzer cannot read leave the
/// preparation as it was.
/// </summary>
internal static class SupportTabCapture
{
internal static PreparedContours Prepare(PreparedContours prepared, Program ownedClean, Vector location,
CuttingPlanRequest request, CancellationToken token)
{
var parameters = request.ConfirmedParameters;
// A display perimeter is a supplied blank: it is never cut free, so it needs no tab.
if (prepared == null || !prepared.HasCutPerimeter || request.SupportBed == null || parameters?.SupportTabs != true
|| parameters.TabConfig == null)
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. The tab keeps clear of
// inside corners by twice its size.
var support = SupportAnalyzer.Analyze(request.SupportBed, request.SupportQuadrant,
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>
/// A note for a planned support-tabbed part whose tab, at its perimeter entry, sits in an
/// inside corner because no lead-in fits clear of one. Advisory: it never refuses or
/// downgrades a plan. A tab that cannot steady its part is <see cref="Review"/>'s warning.
/// </summary>
internal static IEnumerable<CuttingPlanFinding> Notes(FixedProgramPlacement planned)
{
var prepared = planned.Prepared;
if (prepared?.Support == null || prepared.IsPinned(prepared.PerimeterOrdinal)
|| planned.ContourChoices.FirstOrDefault(c => c.ContourOrdinal == prepared.PerimeterOrdinal) is not { } entry
|| prepared.IsClearOfInsideCorners(entry))
yield break;
var tab = entry.Point + planned.Location;
var at = FormattableString.Invariant($"({tab.X:0.####}, {tab.Y:0.####})");
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.");
}
/// <summary>
/// A <see cref="PostVerificationKind.PartSupport"/> warning when a regenerated part its bed
/// cannot hold level is cut without a tab that keeps it level: its pin has no tab or a tab
/// elsewhere, no steadying entry had a lead-in that fits, or no single tab can steady it.
/// Null otherwise. Like other warnings it makes the proposal UNVERIFIED.
/// </summary>
internal static CuttingPlanFinding Review(FixedProgramPlacement source, IReadOnlyList<ContourChoice> choices)
{
var prepared = source.Prepared;
if (prepared?.Support is not { } support || choices == null)
return null;
var perimeter = choices.FirstOrDefault(c => c.ContourOrdinal == prepared.PerimeterOrdinal);
if (perimeter == null)
return null;
var tab = perimeter.Point + prepared.SupportOrigin;
if (prepared.PerimeterTabbed && support.IsSteadiedByTabAt(tab))
return null;
var part = source.SourceOrdinal + 1;
var held = support.Margin == double.NegativeInfinity
? "rests on no support contact"
: support.Margin < 0
? FormattableString.Invariant($"has its center of gravity {-support.Margin:0.####} outside its support contacts")
: FormattableString.Invariant($"has its center of gravity only {support.Margin:0.####} inside its support contacts");
var why = !prepared.PerimeterTabbed
? $"drawing pin {prepared.PinNumberOf(prepared.PerimeterOrdinal)} cuts its outside without a tab"
: prepared.IsPinned(prepared.PerimeterOrdinal)
? $"drawing pin {prepared.PinNumberOf(prepared.PerimeterOrdinal)} puts its tab where it does not steady the part"
: support.TabCanSupport
? "no lead-in fits at a point where its tab would steady the part"
: "no single tab can steady it";
return new CuttingPlanFinding(source.SourceOrdinal, source.SourcePart, null, null,
PostVerificationKind.PartSupport, FormattableString.Invariant(
$"Part {part} {held} (needs {support.RequiredMargin:0.####}) and {why}; it may tip once cut free."));
}
}