mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 16:05:48 -05:00
feat(cutting): tab parts by width when the plate has no support bed
With support tabs on and no support bed, nothing was tabbed and nothing said so. A new no-bed width range (NoBedTabMinWidth..NoBedTabMaxWidth, the short side of the part's minimum-area bounding rectangle) now picks the parts to tab instead. SupportTabSettings.Analyze makes the choice for Plan Cutting, hand-placed lead-ins and the pre-post review; without a bed any tab clear of inside corners counts. With no bed and no range, the plan notes that nothing was tabbed. Cutting Settings > Tabs gains the range fields, and the values persist with the cutting parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
07499fe1cd
commit
479883e7ac
18 files changed
+500
-52
No files matched your search
@@ -42,6 +42,8 @@ internal static class OwnedCuttingParameters
|
||||
: CopyTab(source.TabConfig),
|
||||
TabsEnabled = source.TabsEnabled,
|
||||
SupportTabs = source.SupportTabs,
|
||||
NoBedTabMinWidth = Dimension(source.NoBedTabMinWidth),
|
||||
NoBedTabMaxWidth = Dimension(source.NoBedTabMaxWidth),
|
||||
Sequencing = new SequenceParameters
|
||||
{
|
||||
Method = sequence.Method,
|
||||
|
||||
@@ -90,6 +90,8 @@ internal static class StateFingerprint
|
||||
TabSettings(p.TabConfig);
|
||||
Flag(p.TabsEnabled);
|
||||
Flag(p.SupportTabs);
|
||||
Number(p.NoBedTabMinWidth);
|
||||
Number(p.NoBedTabMaxWidth);
|
||||
Sequence(p.Sequencing);
|
||||
Assignment(p.Assignment);
|
||||
text.Append('}');
|
||||
|
||||
@@ -51,6 +51,32 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
/// </summary>
|
||||
public bool SupportTabs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// With <see cref="SupportTabs"/> on a plate that has no support bed, the narrowest part
|
||||
/// width that is tabbed instead (see <see cref="TabsWithoutBedAt"/>).
|
||||
/// </summary>
|
||||
public double NoBedTabMinWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// With <see cref="SupportTabs"/> on a plate that has no support bed, the widest part
|
||||
/// width that is tabbed instead; 0 turns the rule off, so nothing is tabbed without a bed.
|
||||
/// </summary>
|
||||
public double NoBedTabMaxWidth { get; set; }
|
||||
|
||||
/// <summary>Whether <see cref="NoBedTabMinWidth"/> to <see cref="NoBedTabMaxWidth"/> is a usable range.</summary>
|
||||
public bool HasNoBedTabRange() =>
|
||||
NoBedTabMaxWidth > 0 && double.IsFinite(NoBedTabMaxWidth)
|
||||
&& NoBedTabMinWidth >= 0 && NoBedTabMinWidth <= NoBedTabMaxWidth;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a part <paramref name="width"/> wide (the short side of its minimum-area
|
||||
/// bounding rectangle) is tabbed when its plate has no support bed.
|
||||
/// </summary>
|
||||
public bool TabsWithoutBedAt(double width) =>
|
||||
HasNoBedTabRange()
|
||||
&& width >= NoBedTabMinWidth - Math.Tolerance.Epsilon
|
||||
&& width <= NoBedTabMaxWidth + Math.Tolerance.Epsilon;
|
||||
|
||||
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
|
||||
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ using OpenNest.Support;
|
||||
namespace OpenNest.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-post review of part support (docs/support-tabs.md). On a plate with a support bed, every
|
||||
/// part whose cutting parameters ask for support tabs and that the bed cannot hold level must be
|
||||
/// Pre-post review of part support (docs/support-tabs.md). Every part whose cutting parameters
|
||||
/// ask for support tabs and that needs one (<see cref="SupportTabSettings.Analyze"/>: its plate's
|
||||
/// support bed cannot hold it level or, without a bed, its width is in the no-bed range) must be
|
||||
/// cut with a tab that keeps it level, however its program was made (planned, placed by hand or
|
||||
/// locked). The tab is the gap the outside cut leaves between its end and its start. Reports
|
||||
/// locked); without a bed any tab will do. The tab is the gap the outside cut leaves between its end and its start. Reports
|
||||
/// only; a part whose material or program cannot be read is reported as not checked.
|
||||
/// </summary>
|
||||
internal static class PartSupportReview
|
||||
@@ -21,19 +22,21 @@ internal static class PartSupportReview
|
||||
internal static void Add(Plate plate, int plateNumber, List<PostVerificationFinding> findings,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (plate.SupportBed is not { } bed)
|
||||
return;
|
||||
var bed = plate.SupportBed;
|
||||
for (var index = 0; index < plate.Parts.Count; index++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var part = plate.Parts[index];
|
||||
if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff
|
||||
|| (part.CuttingParameters ?? plate.CuttingParameters)?.SupportTabs != true)
|
||||
var parameters = part?.CuttingParameters ?? plate.CuttingParameters;
|
||||
if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff || parameters?.SupportTabs != true
|
||||
|| (bed == null && !parameters.HasNoBedTabRange()))
|
||||
continue;
|
||||
var partNumber = index + 1;
|
||||
try
|
||||
{
|
||||
var support = SupportAnalyzer.Analyze(bed, plate.Quadrant, part);
|
||||
var support = SupportTabSettings.Analyze(parameters, bed, plate.Quadrant, SupportMaterial.FromPart(part));
|
||||
if (support == null)
|
||||
continue;
|
||||
if (!support.IsComplete)
|
||||
{
|
||||
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, partNumber, null,
|
||||
@@ -100,6 +103,14 @@ internal static class PartSupportReview
|
||||
|
||||
private static string Message(PartSupport support, Vector? tab)
|
||||
{
|
||||
// Without a bed any tab steadies the part, so the only failure is no tab at all.
|
||||
if (!support.HasBed)
|
||||
return FormattableString.Invariant(
|
||||
$"The part is {support.Width:0.####} wide, in the width range tabbed without a support bed, ")
|
||||
+ "and it has no tab; it may tip once cut free."
|
||||
+ (support.BestTabPoint is { } suggested
|
||||
? FormattableString.Invariant($" Tab it near ({suggested.X:0.####}, {suggested.Y:0.####}).")
|
||||
: "");
|
||||
var held = support.Margin == double.NegativeInfinity
|
||||
? "rests on no support contact"
|
||||
: support.Margin < 0
|
||||
|
||||
@@ -58,20 +58,70 @@ namespace OpenNest.Support
|
||||
var contacts = bed.Contacts(material, quadrant, Vector.Zero);
|
||||
|
||||
var corners = InsideCorners(material.Outline);
|
||||
var samples = OutlineSamples(material.Outline)
|
||||
var best = BestTabPoint(material.Outline, center, corners, insideCornerClearance,
|
||||
point => MarginWith(center, hulls, point), out var bestMargin);
|
||||
|
||||
return new PartSupport(center, contacts, margin, bed.StabilityMargin, hulls, best, bestMargin,
|
||||
corners, insideCornerClearance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A part on a plate with no support bed, tabbed for its <see cref="Width"/> instead
|
||||
/// (<see cref="CNC.CuttingStrategy.CuttingParameters.TabsWithoutBedAt"/>). With no contacts
|
||||
/// to judge by, a tab anywhere clear of inside corners is accepted; the best tab point is
|
||||
/// the clear outline point nearest the line through the center of gravity across the
|
||||
/// outline's long side, then the one farthest from an inside corner.
|
||||
/// </summary>
|
||||
/// <param name="insideCornerClearance">
|
||||
/// How far a tab point keeps from an inside corner; 0 keeps it off the corner only.
|
||||
/// </param>
|
||||
public static PartSupport AnalyzeWithoutBed(SupportMaterial material, double insideCornerClearance = 0)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(material);
|
||||
if (!(insideCornerClearance >= 0) || double.IsInfinity(insideCornerClearance))
|
||||
throw new ArgumentOutOfRangeException(nameof(insideCornerClearance));
|
||||
if (!material.IsComplete)
|
||||
return new PartSupport(material.Reason);
|
||||
if (!material.TryGetCenterOfGravity(out var center, out _))
|
||||
return new PartSupport("The part encloses no area.");
|
||||
|
||||
var corners = InsideCorners(material.Outline);
|
||||
var best = BestTabPoint(material.Outline, center, corners, insideCornerClearance,
|
||||
_ => double.PositiveInfinity, out _);
|
||||
return new PartSupport(center, best, corners, insideCornerClearance, Width(material));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The part's width: the short side of its outline's minimum-area bounding rectangle, or
|
||||
/// NaN when its material could not be read.
|
||||
/// </summary>
|
||||
public static double Width(SupportMaterial material)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(material);
|
||||
if (!material.IsComplete || material.Outline.Count < 3)
|
||||
return double.NaN;
|
||||
var box = BoundingRectangle(material.Outline);
|
||||
return System.Math.Min(box.Width, box.Height);
|
||||
}
|
||||
|
||||
// The outline point a tab steadies the part most from, by `score`, clear of every inside
|
||||
// corner where the outline allows it. A tab cannot lift the margin past the contacts' own
|
||||
// hull edges, so a whole stretch of outline often ties; of those, the one nearest the line
|
||||
// through the center across the outline's long side, then the one farthest from an inside
|
||||
// corner, or the first.
|
||||
private static Vector? BestTabPoint(IReadOnlyList<Vector> outline, Vector center, IReadOnlyList<Vector> corners,
|
||||
double insideCornerClearance, Func<Vector, double> score, out double bestMargin)
|
||||
{
|
||||
var samples = OutlineSamples(outline)
|
||||
.Select(point => (Point: point, Clear: Clearance(corners, point)))
|
||||
.ToList();
|
||||
// Clear of every inside corner where the outline allows it.
|
||||
var allowed = samples.Where(s => IsClear(s.Clear, insideCornerClearance)).ToList();
|
||||
if (allowed.Count > 0)
|
||||
samples = allowed;
|
||||
var scored = samples.Select(s => (s.Point, s.Clear, Margin: MarginWith(center, hulls, s.Point))).ToList();
|
||||
var scored = samples.Select(s => (s.Point, s.Clear, Margin: score(s.Point))).ToList();
|
||||
|
||||
// A tab cannot lift the margin past the contacts' own hull edges, so a whole stretch
|
||||
// of outline often ties; of those, the one nearest the line through the center across
|
||||
// the outline's long side, then the one farthest from an inside corner, or the first.
|
||||
var bestMargin = scored.Count == 0 ? double.NegativeInfinity : scored.Max(s => s.Margin);
|
||||
var along = scored.Count == 0 ? Vector.Zero : LongSide(material.Outline);
|
||||
bestMargin = scored.Count == 0 ? double.NegativeInfinity : scored.Max(s => s.Margin);
|
||||
var along = scored.Count == 0 ? Vector.Zero : LongSide(outline);
|
||||
Vector? best = null;
|
||||
var bestOffset = double.PositiveInfinity;
|
||||
var bestClear = double.NegativeInfinity;
|
||||
@@ -88,20 +138,21 @@ namespace OpenNest.Support
|
||||
bestClear = sample.Clear;
|
||||
}
|
||||
}
|
||||
|
||||
return new PartSupport(center, contacts, margin, bed.StabilityMargin, hulls, best, bestMargin,
|
||||
corners, insideCornerClearance);
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>The unit direction of the longer side of the outline's minimum-area bounding rectangle.</summary>
|
||||
private static Vector LongSide(IReadOnlyList<Vector> outline)
|
||||
{
|
||||
// The full angle range checks every hull edge.
|
||||
var box = RotatingCalipers.MinimumBoundingRectangle(ConvexHull.Compute(outline.ToList()), 0, Math.Angle.TwoPI);
|
||||
var box = BoundingRectangle(outline);
|
||||
var angle = box.Width >= box.Height ? box.Angle : box.Angle + Math.Angle.HalfPI;
|
||||
return new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
}
|
||||
|
||||
// The full angle range checks every hull edge.
|
||||
private static BoundingRectangleResult BoundingRectangle(IReadOnlyList<Vector> outline) =>
|
||||
RotatingCalipers.MinimumBoundingRectangle(ConvexHull.Compute(outline.ToList()), 0, Math.Angle.TwoPI);
|
||||
|
||||
// A point `clear` from the nearest inside corner keeps `clearance` from it; with no
|
||||
// clearance it must still not be the corner.
|
||||
internal static bool IsClear(double clear, double clearance) =>
|
||||
@@ -324,6 +375,31 @@ namespace OpenNest.Support
|
||||
InsideCornerClearance = insideCornerClearance;
|
||||
}
|
||||
|
||||
// A part on a plate with no bed, tabbed for its width: it rests on nothing modeled, and a
|
||||
// tab anywhere clear of inside corners is as good as any other.
|
||||
internal PartSupport(Vector centerOfGravity, Vector? bestTabPoint, IReadOnlyList<Vector> insideCorners,
|
||||
double insideCornerClearance, double width)
|
||||
{
|
||||
HasBed = false;
|
||||
CenterOfGravity = centerOfGravity;
|
||||
Contacts = Array.Empty<Vector>();
|
||||
BestTabPoint = bestTabPoint;
|
||||
BestTabMargin = double.PositiveInfinity;
|
||||
InsideCorners = insideCorners;
|
||||
InsideCornerClearance = insideCornerClearance;
|
||||
Width = width;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// False for a part on a plate with no support bed, tabbed for its <see cref="Width"/>
|
||||
/// (<see cref="SupportAnalyzer.AnalyzeWithoutBed"/>): it has no contacts, never counts as
|
||||
/// supported, and any tab clear of inside corners steadies it.
|
||||
/// </summary>
|
||||
public bool HasBed { get; } = true;
|
||||
|
||||
/// <summary>The part's width when analyzed without a bed (see <see cref="SupportAnalyzer.Width"/>); NaN otherwise.</summary>
|
||||
public double Width { get; } = double.NaN;
|
||||
|
||||
/// <summary>True when the part was analyzed; otherwise <see cref="Reason"/> says why not.</summary>
|
||||
public bool IsComplete => Reason == null;
|
||||
|
||||
@@ -363,7 +439,9 @@ namespace OpenNest.Support
|
||||
|
||||
/// <summary>The worst margin with a tab holding the part at <paramref name="tab"/>.</summary>
|
||||
public double MarginWith(Vector tab) =>
|
||||
IsComplete ? SupportAnalyzer.MarginWith(CenterOfGravity, hulls, tab) : double.NegativeInfinity;
|
||||
!IsComplete ? double.NegativeInfinity
|
||||
: !HasBed ? double.PositiveInfinity
|
||||
: SupportAnalyzer.MarginWith(CenterOfGravity, hulls, tab);
|
||||
|
||||
/// <summary>True when a tab at <paramref name="tab"/> holds the part level by the required margin.</summary>
|
||||
public bool IsSteadiedByTabAt(Vector tab) => IsComplete && Meets(MarginWith(tab));
|
||||
|
||||
@@ -5,26 +5,62 @@ using OpenNest.CNC.CuttingStrategy;
|
||||
namespace OpenNest.Support
|
||||
{
|
||||
/// <summary>
|
||||
/// Cutting settings for a lead-in placed by hand under
|
||||
/// <see cref="CuttingParameters.SupportTabs"/>: the tool cuts where it is clicked, so the
|
||||
/// part is tabbed there when its plate's support bed cannot hold it level. Plan Cutting
|
||||
/// chooses the tab point itself (docs/support-tabs.md).
|
||||
/// Which parts <see cref="CuttingParameters.SupportTabs"/> tabs (docs/support-tabs.md): the
|
||||
/// parts the plate's support bed cannot hold level or, on a plate with no bed, the parts
|
||||
/// whose width is in the settings' no-bed range. Also the cutting settings for a lead-in
|
||||
/// placed by hand: the tool cuts where it is clicked, so the part is tabbed there. Plan
|
||||
/// Cutting chooses the tab point itself.
|
||||
/// </summary>
|
||||
public static class SupportTabSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The analysis that tabs a part, or null when support tabs are off or the part is
|
||||
/// outside the rule: with a bed, the bed's analysis; without one, a no-bed analysis
|
||||
/// (<see cref="SupportAnalyzer.AnalyzeWithoutBed"/>) of a part whose width is in
|
||||
/// <see cref="CuttingParameters.TabsWithoutBedAt"/>'s range. The part needs a tab when
|
||||
/// the result is complete and not supported; an incomplete result could not be read.
|
||||
/// </summary>
|
||||
/// <param name="insideCornerClearance">
|
||||
/// How far a tab point keeps from an inside corner; 0 keeps it off the corner only.
|
||||
/// </param>
|
||||
public static PartSupport Analyze(CuttingParameters parameters, SupportBed bed, int quadrant,
|
||||
SupportMaterial material, double insideCornerClearance = 0)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(material);
|
||||
if (parameters?.SupportTabs != true)
|
||||
return null;
|
||||
if (bed != null)
|
||||
return SupportAnalyzer.Analyze(bed, quadrant, material, insideCornerClearance);
|
||||
if (!parameters.HasNoBedTabRange())
|
||||
return null;
|
||||
if (!material.IsComplete)
|
||||
return SupportAnalyzer.AnalyzeWithoutBed(material, insideCornerClearance);
|
||||
return parameters.TabsWithoutBedAt(SupportAnalyzer.Width(material))
|
||||
? SupportAnalyzer.AnalyzeWithoutBed(material, insideCornerClearance)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether support tabs are on but can tab nothing: the plate has no support bed and the
|
||||
/// settings set no no-bed width range.
|
||||
/// </summary>
|
||||
public static bool SkipsWithoutBed(CuttingParameters parameters, SupportBed bed) =>
|
||||
parameters?.SupportTabs == true && bed == null && !parameters.HasNoBedTabRange();
|
||||
|
||||
/// <summary>
|
||||
/// <paramref name="parameters"/> itself, or a copy with tabs enabled when support tabs are
|
||||
/// on, the plate has a support bed, a tab size is set and the bed cannot hold
|
||||
/// <paramref name="part"/> level. The caller's settings are never changed.
|
||||
/// on, a tab size is set and <paramref name="part"/> needs a tab (see <see cref="Analyze"/>).
|
||||
/// The caller's settings are never changed.
|
||||
/// </summary>
|
||||
public static CuttingParameters ForManualLeadIn(CuttingParameters parameters, Plate plate, Part part)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parameters);
|
||||
if (!parameters.SupportTabs || parameters.TabsEnabled || parameters.TabConfig == null
|
||||
|| plate?.SupportBed == null || part == null)
|
||||
|| plate == null || part == null
|
||||
|| (plate.SupportBed == null && !parameters.HasNoBedTabRange()))
|
||||
return parameters;
|
||||
var support = SupportAnalyzer.Analyze(plate.SupportBed, plate.Quadrant, part);
|
||||
if (!support.IsComplete || support.IsSupported)
|
||||
var support = Analyze(parameters, plate.SupportBed, plate.Quadrant, SupportMaterial.FromPart(part));
|
||||
if (support is not { IsComplete: true, IsSupported: false })
|
||||
return parameters;
|
||||
var tabbed = OwnedCuttingParameters.Copy(parameters);
|
||||
tabbed.TabsEnabled = true;
|
||||
|
||||
Reference in new issue
Block a user