mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-26 22:01:06 -04:00
feat(planning): warn about tipping parts and retire legacy lead-in assignment
Plan Cutting is now the only automatic lead-in assignment, and parts the support grid cannot hold level are reported wherever they lack a steadying tab. - Remove Assign Lead-ins (plate and nest), LeadInAssigner, the lead-in tool's Auto-Assign button and the Auto Tab size range; old settings with autoTab sizes still load - Manual Place Lead-in tabs the clicked entry when support tabs are on and the grid cannot hold the part (SupportTabSettings) - New PartSupport finding: Plan Cutting replay and best-effort plans warn when a drawing pin, a lead-in that does not fit or no single tab leaves a part unsteady; the pre-post review checks each placed program's outside tab gap. These warnings replace the advisory weak-tab notes; only the inside-corner tab note stays advisory, and never for a pin - Pinned perimeters keep their pin but carry the support analysis - Draw grid points as pixel-snapped squares without anti-aliasing - Post fixtures build lead-ins through a test-only TestLeadIns helper - Docs: support tabs, pre-post verification and planner notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c3881c5534
commit
8c7dc41ce8
42 files changed
+539
-849
No files matched your search
@@ -39,8 +39,6 @@ internal static class OwnedCuttingParameters
|
||||
ArcCircleLeadOut = CopyLeadOut(source.ArcCircleLeadOut),
|
||||
RoundLeadInAngles = source.RoundLeadInAngles,
|
||||
LeadInAngleIncrement = source.LeadInAngleIncrement,
|
||||
AutoTabMinSize = Dimension(source.AutoTabMinSize),
|
||||
AutoTabMaxSize = Dimension(source.AutoTabMaxSize),
|
||||
TabConfig = source.TabConfig == null
|
||||
? source.TabsEnabled ? throw new ArgumentException("Enabled tabs require a configuration.") : null
|
||||
: CopyTab(source.TabConfig),
|
||||
|
||||
@@ -92,7 +92,8 @@ public sealed class PreparedContours
|
||||
RejectedPins = rejected.AsReadOnly();
|
||||
}
|
||||
|
||||
// A copy that tabs the perimeter for support; every pin keeps its entry, now owned here.
|
||||
// A copy that tabs the perimeter for support; every pin keeps its entry, now owned here, and
|
||||
// a pinned perimeter keeps its pin's settings: the analysis is only kept for review.
|
||||
private PreparedContours(PreparedContours source, PartSupport support, Vector location, Tab tab)
|
||||
: this(source.shapes, source.scribes, source.breakCuts, source.tabbedCutouts, source.parameters)
|
||||
{
|
||||
@@ -104,10 +105,13 @@ public sealed class PreparedContours
|
||||
contourParameters = source.contourParameters == null
|
||||
? new CuttingParameters[Count]
|
||||
: (CuttingParameters[])source.contourParameters.Clone();
|
||||
var perimeter = OwnedCuttingParameters.Copy(source.ParametersFor(PerimeterOrdinal));
|
||||
perimeter.TabsEnabled = true;
|
||||
perimeter.TabConfig = OwnedCuttingParameters.CopyTab(tab);
|
||||
contourParameters[PerimeterOrdinal] = perimeter;
|
||||
if (pinned[PerimeterOrdinal] == null)
|
||||
{
|
||||
var perimeter = OwnedCuttingParameters.Copy(source.ParametersFor(PerimeterOrdinal));
|
||||
perimeter.TabsEnabled = true;
|
||||
perimeter.TabConfig = OwnedCuttingParameters.CopyTab(tab);
|
||||
contourParameters[PerimeterOrdinal] = perimeter;
|
||||
}
|
||||
RejectedPins = source.RejectedPins;
|
||||
this.support = support;
|
||||
supportOrigin = location;
|
||||
@@ -118,7 +122,8 @@ public sealed class PreparedContours
|
||||
/// on its support bed: <paramref name="support"/> analyzed this part's material placed at
|
||||
/// <paramref name="location"/>, and the perimeter's automatic entries keep to points it
|
||||
/// accepts (<see cref="PartSupport.AcceptsTabAt"/>) plus its best tab point. A pinned
|
||||
/// perimeter is returned unchanged: the drawing wins.
|
||||
/// perimeter keeps its pin's entry, leads and tab (the drawing wins); the copy only keeps
|
||||
/// the analysis, so replay can report a pin that leaves the part unsteady.
|
||||
/// </summary>
|
||||
public PreparedContours WithSupportTab(PartSupport support, Vector location, Tab tab)
|
||||
{
|
||||
@@ -127,12 +132,21 @@ public sealed class PreparedContours
|
||||
PostVerificationGeometry.Validate(location);
|
||||
if (!support.IsComplete || support.BestTabPoint == null)
|
||||
throw new ArgumentException("Support tabs need a complete analysis with a tab point.", nameof(support));
|
||||
return IsPinned(PerimeterOrdinal) ? this : new PreparedContours(this, support, location, tab);
|
||||
return new PreparedContours(this, support, location, tab);
|
||||
}
|
||||
|
||||
/// <summary>The support analysis that tabs this perimeter, or null when it is not tabbed for support.</summary>
|
||||
/// <summary>
|
||||
/// The analysis of a part its support bed cannot hold level (see <see cref="WithSupportTab"/>),
|
||||
/// or null; <see cref="SupportOrigin"/> places its local contours in its world frame.
|
||||
/// </summary>
|
||||
public PartSupport Support => support;
|
||||
|
||||
/// <summary>The part's location: world = local + this, for <see cref="Support"/>.</summary>
|
||||
public Vector SupportOrigin => supportOrigin;
|
||||
|
||||
/// <summary>Whether the emitted perimeter is tabbed: by the plate settings, its pin or support.</summary>
|
||||
internal bool PerimeterTabbed => ParametersFor(PerimeterOrdinal).TabsEnabled;
|
||||
|
||||
public int Count => shapes.Length;
|
||||
|
||||
/// <summary>The pierce zone every lead-in pierce of these contours keeps; 0 when off.</summary>
|
||||
@@ -436,7 +450,7 @@ public sealed class PreparedContours
|
||||
/// Whether this preparation tabs its perimeter for support (see <see cref="WithSupportTab"/>):
|
||||
/// its automatic catalogue then keeps to steadying points unless asked for a fallback.
|
||||
/// </summary>
|
||||
internal bool HasSupportTab => support != null;
|
||||
internal bool HasSupportTab => support != null && !IsPinned(PerimeterOrdinal);
|
||||
|
||||
/// <summary>
|
||||
/// The support margin with this perimeter's tab at <paramref name="choice"/> (see
|
||||
|
||||
@@ -89,8 +89,6 @@ internal static class StateFingerprint
|
||||
Number(p.PierceDiameter);
|
||||
Flag(p.RoundLeadInAngles);
|
||||
Number(p.LeadInAngleIncrement);
|
||||
Number(p.AutoTabMinSize);
|
||||
Number(p.AutoTabMaxSize);
|
||||
TabSettings(p.TabConfig);
|
||||
Flag(p.TabsEnabled);
|
||||
Flag(p.SupportTabs);
|
||||
|
||||
@@ -40,9 +40,6 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
public bool RoundLeadInAngles { get; set; }
|
||||
public double LeadInAngleIncrement { get; set; } = 5.0;
|
||||
|
||||
public double AutoTabMinSize { get; set; }
|
||||
public double AutoTabMaxSize { get; set; }
|
||||
|
||||
public Tab TabConfig { get; set; }
|
||||
public bool TabsEnabled { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
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
|
||||
/// 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
|
||||
/// only; a part whose material or program cannot be read is reported as not checked.
|
||||
/// </summary>
|
||||
internal static class PartSupportReview
|
||||
{
|
||||
internal static void Add(Plate plate, int plateNumber, List<PostVerificationFinding> findings,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (plate.SupportBed is not { } bed)
|
||||
return;
|
||||
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)
|
||||
continue;
|
||||
var partNumber = index + 1;
|
||||
try
|
||||
{
|
||||
var support = SupportAnalyzer.Analyze(bed, plate.Quadrant, part);
|
||||
if (!support.IsComplete)
|
||||
{
|
||||
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, partNumber, null,
|
||||
"Support check: " + support.Reason));
|
||||
continue;
|
||||
}
|
||||
if (support.IsSupported)
|
||||
continue;
|
||||
var tab = OutsideTab(ExecutionMotionReader.Read(part.Program, part.Location, null, token));
|
||||
if (tab is { } point && support.IsSteadiedByTabAt(point))
|
||||
continue;
|
||||
findings.Add(new(PostVerificationKind.PartSupport, plateNumber, partNumber, null,
|
||||
Message(support, tab))
|
||||
{ Location = support.CenterOfGravity });
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or InvalidOperationException
|
||||
or NotSupportedException or ArithmeticException)
|
||||
{
|
||||
findings.Add(new(PostVerificationKind.Incomplete, plateNumber, partNumber, null,
|
||||
"Support check: " + exception.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The middle of the gap the outside cut leaves open, or null when it closes: the outside cut
|
||||
/// is the chain of cutting motions with the largest extent.
|
||||
/// </summary>
|
||||
internal static Vector? OutsideTab(OwnedExecution execution)
|
||||
{
|
||||
Vector? start = null, end = null, bestStart = null, bestEnd = null;
|
||||
var minX = double.PositiveInfinity; var minY = double.PositiveInfinity;
|
||||
var maxX = double.NegativeInfinity; var maxY = double.NegativeInfinity;
|
||||
var bestArea = -1.0;
|
||||
foreach (var motion in execution.Motions.Append(null))
|
||||
{
|
||||
if (motion is { Rapid: false, Layer: LayerType.Cut or LayerType.Display })
|
||||
{
|
||||
start ??= motion.Start ?? motion.End;
|
||||
end = motion.End;
|
||||
foreach (var p in new[] { motion.Start ?? motion.End, motion.End, motion.Curve?.Midpoint ?? motion.End })
|
||||
{
|
||||
minX = System.Math.Min(minX, p.X); minY = System.Math.Min(minY, p.Y);
|
||||
maxX = System.Math.Max(maxX, p.X); maxY = System.Math.Max(maxY, p.Y);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Anything else ends a chain: a rapid, a lead, a scribe or a break cut.
|
||||
if (start != null && (maxX - minX) * (maxY - minY) > bestArea)
|
||||
{
|
||||
bestArea = (maxX - minX) * (maxY - minY);
|
||||
bestStart = start;
|
||||
bestEnd = end;
|
||||
}
|
||||
start = end = null;
|
||||
minX = minY = double.PositiveInfinity;
|
||||
maxX = maxY = double.NegativeInfinity;
|
||||
}
|
||||
if (bestStart is not { } first || bestEnd is not { } last
|
||||
|| first.DistanceTo(last) <= PostVerificationGeometry.Epsilon)
|
||||
return null;
|
||||
return (first + last) * 0.5;
|
||||
}
|
||||
|
||||
private static string Message(PartSupport support, Vector? tab)
|
||||
{
|
||||
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 tabText = tab is { } point
|
||||
? FormattableString.Invariant($"its tab at ({point.X:0.####}, {point.Y:0.####}) does not steady it")
|
||||
: "it has no tab";
|
||||
var best = support.BestTabPoint is { } bestPoint && support.TabCanSupport
|
||||
? FormattableString.Invariant($" A tab near ({bestPoint.X:0.####}, {bestPoint.Y:0.####}) would.")
|
||||
: " No single tab can steady it.";
|
||||
return FormattableString.Invariant($"The part {held} (needs {support.RequiredMargin:0.####}) and {tabText}; ")
|
||||
+ "it may tip once cut free." + best;
|
||||
}
|
||||
}
|
||||
@@ -219,6 +219,8 @@ public static class PostVerificationAnalyzer
|
||||
issue.PartBId is { } other ? indices[other] : null, "Overlap check: " + issue.Message));
|
||||
// Parts that do not overlap may still be closer than the plate's part spacing.
|
||||
PartSpacingReview.Add(plate, plateNumber, materialParts, indices, overlap, findings, cancellationToken);
|
||||
// Parts the support bed cannot hold level need a tab that keeps them level.
|
||||
PartSupportReview.Add(plate, plateNumber, findings, cancellationToken);
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new PostVerificationReport(findings);
|
||||
|
||||
@@ -34,7 +34,13 @@ public enum PostVerificationKind
|
||||
/// Two parts are closer than the plate's part spacing without sharing material, judged as
|
||||
/// Auto Nest's layout check judges them.
|
||||
/// </summary>
|
||||
PartSpacing
|
||||
PartSpacing,
|
||||
|
||||
/// <summary>
|
||||
/// A part its plate's support bed cannot hold level has no tab that keeps it level, so it may
|
||||
/// tip once cut free (see docs/support-tabs.md).
|
||||
/// </summary>
|
||||
PartSupport
|
||||
}
|
||||
|
||||
/// <summary>Plate and part numbers are one-based; plate zero denotes a whole-post limitation.</summary>
|
||||
@@ -84,6 +90,8 @@ public sealed class PostVerificationReport
|
||||
Summary(PostVerificationKind.PierceZone, "Pierce zones");
|
||||
if (Findings.Any(finding => finding.Kind == PostVerificationKind.PartSpacing))
|
||||
Summary(PostVerificationKind.PartSpacing, "Part spacing");
|
||||
if (Findings.Any(finding => finding.Kind == PostVerificationKind.PartSupport))
|
||||
Summary(PostVerificationKind.PartSupport, "Part support");
|
||||
foreach (var finding in Findings)
|
||||
{
|
||||
text.Append(finding.PlateNumber == 0 ? "Post processor" : $"Plate {finding.PlateNumber}");
|
||||
|
||||
@@ -365,6 +365,9 @@ namespace OpenNest.Support
|
||||
public double MarginWith(Vector tab) =>
|
||||
IsComplete ? SupportAnalyzer.MarginWith(CenterOfGravity, hulls, tab) : double.NegativeInfinity;
|
||||
|
||||
/// <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));
|
||||
|
||||
/// <summary>
|
||||
/// Whether a tab at <paramref name="tab"/> is good enough: it keeps clear of inside corners,
|
||||
/// and it holds the part level by the required margin or, where no single tab can, it is
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
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).
|
||||
/// </summary>
|
||||
public static class SupportTabSettings
|
||||
{
|
||||
/// <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.
|
||||
/// </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)
|
||||
return parameters;
|
||||
var support = SupportAnalyzer.Analyze(plate.SupportBed, plate.Quadrant, part);
|
||||
if (!support.IsComplete || support.IsSupported)
|
||||
return parameters;
|
||||
var tabbed = OwnedCuttingParameters.Copy(parameters);
|
||||
tabbed.TabsEnabled = true;
|
||||
return tabbed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,8 @@ internal static class BestEffortCuttingPlan
|
||||
f.OtherPartNumber is int otherOrdinal ? snapshot.Placements[otherOrdinal - 1].SourcePart : null,
|
||||
f.Kind, f.Message)
|
||||
{ Witness = f }));
|
||||
if (SupportTabCapture.Review(source, proposal.ContourChoices) is { } unsteady)
|
||||
findings.Add(unsteady);
|
||||
findings.AddRange(SupportTabCapture.Notes(proposal));
|
||||
distance += execution.RapidDistanceFrom(position);
|
||||
if (!double.IsFinite(distance))
|
||||
|
||||
@@ -397,6 +397,9 @@ public static class CuttingPlanService
|
||||
findings: [JointCuttingPlanSearch.Finding(source, null,
|
||||
"Selected program does not preserve complete directed contours, certified tabs or selected entry/style geometry.")],
|
||||
expansions: expansions);
|
||||
// A part the support bed cannot hold level must be tabbed where the tab steadies it.
|
||||
if (SupportTabCapture.Review(source, proposal.ContourChoices) is { } unsteady)
|
||||
warnings.Add(unsteady);
|
||||
distance += execution.RapidDistanceFrom(position);
|
||||
position = execution.DeparturePoint;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Support;
|
||||
|
||||
@@ -12,9 +13,10 @@ 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 supported part, a
|
||||
/// pinned perimeter (the drawing wins), a missing tab configuration or material the analyzer
|
||||
/// cannot read leave the preparation as it was.
|
||||
/// 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
|
||||
{
|
||||
@@ -23,7 +25,7 @@ internal static class SupportTabCapture
|
||||
{
|
||||
var parameters = request.ConfirmedParameters;
|
||||
if (prepared == null || request.SupportBed == null || parameters?.SupportTabs != true
|
||||
|| parameters.TabConfig == null || prepared.IsPinned(prepared.PerimeterOrdinal))
|
||||
|| parameters.TabConfig == null)
|
||||
return prepared;
|
||||
token.ThrowIfCancellationRequested();
|
||||
// The clean program is the drawing's own, rotated as placed: its material at the
|
||||
@@ -38,29 +40,56 @@ internal static class SupportTabCapture
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// 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 is not { } support
|
||||
|| planned.ContourChoices.FirstOrDefault(c => c.ContourOrdinal == prepared.PerimeterOrdinal) is not { } entry)
|
||||
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.####})");
|
||||
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.");
|
||||
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."));
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine
|
||||
{
|
||||
public class LeadInAssigner
|
||||
{
|
||||
public IPartSequencer Sequencer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Assigns the plate's lead-ins to every unlocked part, except parts whose drawing pins
|
||||
/// are active: those keep their programs, since only Plan Cutting cuts drawing pins, and
|
||||
/// this never overwrites a pin with the plate's settings. Returns those parts.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Part> Assign(Plate plate)
|
||||
{
|
||||
var parameters = plate.CuttingParameters;
|
||||
if (parameters == null)
|
||||
return [];
|
||||
|
||||
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
|
||||
var exitPoint = PlateHelper.GetExitPoint(plate);
|
||||
|
||||
// Pass 1: assign lead-ins to establish pierce points
|
||||
var piercePoints = AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: null);
|
||||
|
||||
// Pass 2: re-assign with knowledge of next part's start point
|
||||
AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: piercePoints);
|
||||
|
||||
return plate.Parts.Where(KeepsDrawingLeadIns).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The operator's notice after Assign Lead-ins kept parts with drawing pins, or null when it
|
||||
/// kept none. Part numbers are the editor's (the plate's current order).
|
||||
/// </summary>
|
||||
public static string KeptMessage(IReadOnlyList<(int PlateNumber, IReadOnlyList<int> PartNumbers)> kept)
|
||||
{
|
||||
var count = kept.Sum(k => k.PartNumbers.Count);
|
||||
if (count == 0)
|
||||
return null;
|
||||
var where = string.Join("; ", kept.Where(k => k.PartNumbers.Count != 0).Select(k =>
|
||||
$"plate {k.PlateNumber}: part{(k.PartNumbers.Count == 1 ? "" : "s")} {string.Join(", ", k.PartNumbers)}"));
|
||||
return $"{count} part{(count == 1 ? "" : "s")} kept {(count == 1 ? "its" : "their")} drawing lead-ins ({where}); "
|
||||
+ "use Plan Cutting to apply their drawing pins.";
|
||||
}
|
||||
|
||||
// Locked parts are skipped as before and keep their locked lead-ins whatever their pins.
|
||||
private static bool KeepsDrawingLeadIns(Part part) => !part.LeadInsLocked && part.HasActiveDrawingPins;
|
||||
|
||||
private Vector[] AssignPass(
|
||||
List<SequencedPart> sequenced,
|
||||
CuttingParameters parameters,
|
||||
Vector exitPoint,
|
||||
Vector[] nextPiercePoints
|
||||
)
|
||||
{
|
||||
var piercePoints = new Vector[sequenced.Count];
|
||||
var currentPoint = exitPoint;
|
||||
|
||||
for (var i = 0; i < sequenced.Count; i++)
|
||||
{
|
||||
var part = sequenced[i].Part;
|
||||
|
||||
if (part.LeadInsLocked || KeepsDrawingLeadIns(part))
|
||||
{
|
||||
piercePoints[i] = GetPiercePoint(part);
|
||||
currentPoint = part.Location;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.HasManualLeadIns)
|
||||
part.RemoveLeadIns();
|
||||
|
||||
var localApproach = currentPoint - part.Location;
|
||||
|
||||
if (nextPiercePoints != null && i + 1 < sequenced.Count)
|
||||
{
|
||||
var nextStart = nextPiercePoints[i + 1] - part.Location;
|
||||
part.ApplyLeadIns(parameters, localApproach, nextStart);
|
||||
}
|
||||
else
|
||||
{
|
||||
part.ApplyLeadIns(parameters, localApproach);
|
||||
}
|
||||
|
||||
piercePoints[i] = GetPiercePoint(part);
|
||||
currentPoint = part.Location;
|
||||
}
|
||||
|
||||
return piercePoints;
|
||||
}
|
||||
|
||||
private static Vector GetPiercePoint(Part part)
|
||||
{
|
||||
foreach (var code in part.Program.Codes)
|
||||
{
|
||||
if (code is CNC.Motion motion)
|
||||
return motion.EndPoint + part.Location;
|
||||
}
|
||||
|
||||
return part.Location;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,8 +128,7 @@ public class CuttingParametersSerializerTests
|
||||
Assert.Equal(0.0625, restored.PierceClearance);
|
||||
Assert.False(restored.RoundLeadInAngles);
|
||||
Assert.Equal(5, restored.LeadInAngleIncrement);
|
||||
Assert.Equal(0, restored.AutoTabMinSize);
|
||||
Assert.Equal(0, restored.AutoTabMaxSize);
|
||||
Assert.False(restored.SupportTabs);
|
||||
AssertEquivalent(new AssignmentParameters(), restored.Assignment);
|
||||
AssertEquivalent(new SequenceParameters(), restored.Sequencing);
|
||||
}
|
||||
@@ -304,8 +303,7 @@ public class CuttingParametersSerializerTests
|
||||
PierceDiameter = 0.41,
|
||||
RoundLeadInAngles = true,
|
||||
LeadInAngleIncrement = 13,
|
||||
AutoTabMinSize = 0.62,
|
||||
AutoTabMaxSize = 4.7,
|
||||
SupportTabs = true,
|
||||
TabConfig = CreateTab("Breaker"),
|
||||
TabsEnabled = true,
|
||||
Assignment = new AssignmentParameters
|
||||
|
||||
@@ -31,8 +31,6 @@ public class CuttingParametersDto
|
||||
public double PierceDiameter { get; set; }
|
||||
public bool RoundLeadInAngles { get; set; }
|
||||
public double LeadInAngleIncrement { get; set; }
|
||||
public double AutoTabMinSize { get; set; }
|
||||
public double AutoTabMaxSize { get; set; }
|
||||
public bool SupportTabs { get; set; }
|
||||
public SequenceDto Sequencing { get; set; } = new();
|
||||
public AssignmentDto Assignment { get; set; } = new();
|
||||
|
||||
@@ -72,8 +72,6 @@ public static class CuttingParametersSerializer
|
||||
PierceDiameter = parameters.PierceDiameter,
|
||||
RoundLeadInAngles = parameters.RoundLeadInAngles,
|
||||
LeadInAngleIncrement = parameters.LeadInAngleIncrement,
|
||||
AutoTabMinSize = parameters.AutoTabMinSize,
|
||||
AutoTabMaxSize = parameters.AutoTabMaxSize,
|
||||
SupportTabs = parameters.SupportTabs,
|
||||
Sequencing = ToSequenceDto(parameters.Sequencing),
|
||||
Assignment = ToAssignmentDto(parameters.Assignment),
|
||||
@@ -109,8 +107,6 @@ public static class CuttingParametersSerializer
|
||||
PierceDiameter = dto.PierceDiameter,
|
||||
RoundLeadInAngles = dto.RoundLeadInAngles,
|
||||
LeadInAngleIncrement = dto.LeadInAngleIncrement,
|
||||
AutoTabMinSize = dto.AutoTabMinSize,
|
||||
AutoTabMaxSize = dto.AutoTabMaxSize,
|
||||
SupportTabs = dto.SupportTabs,
|
||||
Sequencing = FromSequenceDto(dto.Sequencing),
|
||||
Assignment = FromAssignmentDto(dto.Assignment),
|
||||
|
||||
@@ -212,7 +212,7 @@ public class BreakoffBuilderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignLeadIns_LeadsBreakCutsInFromTheScrap()
|
||||
public void ApplyLeadIns_LeadsBreakCutsInFromTheScrap()
|
||||
{
|
||||
var (entities, join) = Chord();
|
||||
var drawing = Build(entities, On(join));
|
||||
|
||||
@@ -186,7 +186,7 @@ public class CIFiberPostProcessorTests
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
||||
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
||||
};
|
||||
new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate);
|
||||
TestLeadIns.Apply(plate);
|
||||
|
||||
var nest = new Nest("kerf") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
@@ -65,7 +65,7 @@ public class CIFiberSampleRegressionTests
|
||||
PierceClearance = 0.02,
|
||||
};
|
||||
|
||||
new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate);
|
||||
TestLeadIns.Apply(plate);
|
||||
return nest;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,54 +10,11 @@ using OpenNest.Geometry;
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy Assign Lead-ins never overwrites drawing pins, and the pre-post review reports a part
|
||||
/// whose active pins its program does not apply, plus fixed (locked or pinned) lead violations.
|
||||
/// The pre-post review reports a part whose active pins its program does not apply, plus fixed
|
||||
/// (locked or pinned) lead violations.
|
||||
/// </summary>
|
||||
public class DrawingPinReviewTests
|
||||
{
|
||||
[Fact]
|
||||
public void AssignLeadIns_KeepsPinnedPartsAndReturnsThem()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var pinned = new Part(PinnedContourTests.SquareWithPin(Vector.Zero, PinnedContourTests.Corner()), new Vector(10, 10));
|
||||
var plain = new Part(new Drawing("plain", ExplicitContourTests.Square(false)), new Vector(30, 10));
|
||||
var off = new Part(PinnedContourTests.SquareWithPin(Vector.Zero, PinnedContourTests.Corner()), new Vector(10, 30))
|
||||
{ DrawingPinsOff = true };
|
||||
var locked = new Part(PinnedContourTests.SquareWithPin(Vector.Zero, PinnedContourTests.Corner()), new Vector(30, 30))
|
||||
{ LeadInsLocked = true };
|
||||
foreach (var part in new[] { pinned, plain, off, locked })
|
||||
plate.Parts.Add(part);
|
||||
var pinnedProgram = pinned.Program;
|
||||
var lockedProgram = locked.Program;
|
||||
plate.CuttingParameters = new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } };
|
||||
|
||||
var kept = new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate);
|
||||
|
||||
Assert.Equal([pinned], kept);
|
||||
Assert.Same(pinnedProgram, pinned.Program);
|
||||
Assert.False(pinned.HasManualLeadIns);
|
||||
Assert.Same(lockedProgram, locked.Program);
|
||||
Assert.True(plain.HasManualLeadIns);
|
||||
Assert.True(off.HasManualLeadIns);
|
||||
Assert.Equal("1 part kept its drawing lead-ins (plate 2: part 1); use Plan Cutting to apply their drawing pins.",
|
||||
LeadInAssigner.KeptMessage([(2, [1])]));
|
||||
Assert.Equal("3 parts kept their drawing lead-ins (plate 1: parts 1, 4; plate 3: part 2); use Plan Cutting "
|
||||
+ "to apply their drawing pins.", LeadInAssigner.KeptMessage([(1, [1, 4]), (2, []), (3, [2])]));
|
||||
Assert.Null(LeadInAssigner.KeptMessage([(1, [])]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignLeadIns_WithoutPins_ReturnsNothingAndAssignsAsBefore()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(new Part(new Drawing("plain", ExplicitContourTests.Square(false)), new Vector(10, 10)));
|
||||
plate.CuttingParameters = new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } };
|
||||
|
||||
Assert.Empty(new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate));
|
||||
Assert.True(plate.Parts[0].HasManualLeadIns);
|
||||
Assert.Empty(new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(new Plate(10, 10)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrePost_PinsAppliedByPlanCutting_AreNotReported()
|
||||
{
|
||||
|
||||
@@ -42,8 +42,7 @@ public class OwnedCuttingParameterTests
|
||||
ArcCircleLeadOut = leadOut,
|
||||
TabConfig = tab,
|
||||
TabsEnabled = true,
|
||||
AutoTabMinSize = 0.1,
|
||||
AutoTabMaxSize = 0.5,
|
||||
SupportTabs = true,
|
||||
RoundLeadInAngles = true,
|
||||
LeadInAngleIncrement = 30,
|
||||
Sequencing = new SequenceParameters
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Support;
|
||||
@@ -104,7 +105,7 @@ public class SupportTabPlanningTests
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, new Vector(7, 0),
|
||||
confirmedParameters: Parameters()));
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, string.Join("\n", result.Findings.Select(f => f.Message)));
|
||||
Assert.True(result.Status == CuttingPlanStatus.BestEffort, string.Join("\n", result.Findings.Select(f => f.Message)));
|
||||
var proposal = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, strip));
|
||||
var (entry, cutEnd) = EntryAndCutEnd(proposal);
|
||||
Assert.Equal(TabSize, entry.DistanceTo(cutEnd), 6);
|
||||
@@ -115,7 +116,8 @@ public class SupportTabPlanningTests
|
||||
Assert.True(support.MarginWith(entry) > support.MarginWith(new Vector(12, 2)), $"entry {entry}");
|
||||
Assert.False(support.HoldsLevelWithTabAt(entry));
|
||||
Assert.Contains(result.Findings, f => ReferenceEquals(f.SourcePart, strip)
|
||||
&& f.Message.Contains("no lead-in fits where a tab would"));
|
||||
&& f.Kind == PostVerificationKind.PartSupport
|
||||
&& f.Message.Contains("no lead-in fits at a point where its tab would steady the part"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -130,12 +132,13 @@ public class SupportTabPlanningTests
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, new Vector(0, 0),
|
||||
confirmedParameters: Parameters()));
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
||||
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
|
||||
var (entry, cutEnd) = EntryAndCutEnd(Assert.Single(result.ProposedOrder));
|
||||
Assert.Equal(TabSize, entry.DistanceTo(cutEnd), 6);
|
||||
var note = Assert.Single(result.Findings);
|
||||
Assert.Same(strip, note.SourcePart);
|
||||
Assert.Contains("no single tab can", note.Message);
|
||||
var warning = Assert.Single(result.Findings);
|
||||
Assert.Same(strip, warning.SourcePart);
|
||||
Assert.Equal(PostVerificationKind.PartSupport, warning.Kind);
|
||||
Assert.Contains("no single tab can steady it", warning.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingStrategy;
|
||||
|
||||
public class LeadInAssignerTests
|
||||
{
|
||||
private static Part MakeSquarePartAt(double x, double y)
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("test", pgm);
|
||||
return new Part(drawing, new Vector(x, y));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_SetsHasManualLeadInsOnAllParts()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(MakeSquarePartAt(10, 10));
|
||||
plate.Parts.Add(MakeSquarePartAt(30, 30));
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
Assert.All(plate.Parts, p => Assert.True(p.HasManualLeadIns));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_SkipsLockedParts()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var lockedPart = MakeSquarePartAt(10, 10);
|
||||
lockedPart.LeadInsLocked = true;
|
||||
lockedPart.HasManualLeadIns = true;
|
||||
var originalProgram = lockedPart.Program;
|
||||
|
||||
plate.Parts.Add(lockedPart);
|
||||
plate.Parts.Add(MakeSquarePartAt(30, 30));
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
Assert.Same(originalProgram, lockedPart.Program);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_RemovesExistingLeadInsBeforeReapply()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(MakeSquarePartAt(10, 10));
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
var countAfterFirst = plate.Parts[0].Program.Codes.Count;
|
||||
|
||||
assigner.Assign(plate);
|
||||
var countAfterSecond = plate.Parts[0].Program.Codes.Count;
|
||||
|
||||
Assert.Equal(countAfterFirst, countAfterSecond);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_PartsContainLeadinLayerCodes()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(MakeSquarePartAt(10, 10));
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
var hasLeadin = plate
|
||||
.Parts[0]
|
||||
.Program.Codes.OfType<LinearMove>()
|
||||
.Any(m => m.Layer == LayerType.Leadin);
|
||||
Assert.True(hasLeadin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_PreservesRotationOnRotatedParts()
|
||||
{
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotation = System.Math.PI / 2; // 90 degrees
|
||||
var part = Part.CreateAtOrigin(drawing, rotation);
|
||||
part.Offset(10, 10);
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(part);
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_ThenRemove_PreservesRotation()
|
||||
{
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotation = System.Math.PI / 4; // 45 degrees
|
||||
var part = Part.CreateAtOrigin(drawing, rotation);
|
||||
part.Offset(15, 15);
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(part);
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
|
||||
part.RemoveLeadIns();
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_PreservesLocationOnRotatedParts()
|
||||
{
|
||||
var drawing = MakeSquareDrawing();
|
||||
var part = Part.CreateAtOrigin(drawing, System.Math.PI / 2);
|
||||
part.Offset(20, 30);
|
||||
var location = part.Location;
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(part);
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
Assert.Equal(location.X, part.Location.X, 6);
|
||||
Assert.Equal(location.Y, part.Location.Y, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_ThenRemove_RestoresBoundingBox()
|
||||
{
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotation = System.Math.PI / 2;
|
||||
var part = Part.CreateAtOrigin(drawing, rotation);
|
||||
part.Offset(10, 10);
|
||||
var originalBbox = part.BoundingBox;
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(part);
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
part.RemoveLeadIns();
|
||||
|
||||
Assert.Equal(originalBbox.X, part.BoundingBox.X, 4);
|
||||
Assert.Equal(originalBbox.Y, part.BoundingBox.Y, 4);
|
||||
Assert.Equal(originalBbox.Width, part.BoundingBox.Width, 4);
|
||||
Assert.Equal(originalBbox.Length, part.BoundingBox.Length, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_ProgramGeometryMatchesRotatedOriginal()
|
||||
{
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotation = System.Math.PI / 2;
|
||||
var part = Part.CreateAtOrigin(drawing, rotation);
|
||||
part.Offset(10, 10);
|
||||
|
||||
// Get the original rotated geometry
|
||||
var originalGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program);
|
||||
var originalNonRapid = originalGeometry.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(part);
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
// The lead-in program should produce geometry that contains the
|
||||
// original rotated shape (plus lead-in/out extensions)
|
||||
var leadInGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program);
|
||||
var leadInNonRapid = leadInGeometry
|
||||
.Where(e =>
|
||||
e.Layer != SpecialLayers.Rapid
|
||||
&& e.Layer != SpecialLayers.Leadin
|
||||
&& e.Layer != SpecialLayers.Leadout
|
||||
)
|
||||
.ToList();
|
||||
|
||||
// The bounding box of the cut geometry should be close to original
|
||||
var origBbox = GetEntityBounds(originalNonRapid);
|
||||
var leadBbox = GetEntityBounds(leadInNonRapid);
|
||||
|
||||
Assert.Equal(origBbox.Width, leadBbox.Width, 2);
|
||||
Assert.Equal(origBbox.Length, leadBbox.Length, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_MultipleRotatedParts_AllPreserveRotation()
|
||||
{
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotations = new[] { 0.0, System.Math.PI / 6, System.Math.PI / 4, System.Math.PI / 2 };
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
for (var i = 0; i < rotations.Length; i++)
|
||||
{
|
||||
var part = Part.CreateAtOrigin(drawing, rotations[i]);
|
||||
part.Offset(10 + i * 15, 10);
|
||||
plate.Parts.Add(part);
|
||||
}
|
||||
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
for (var i = 0; i < rotations.Length; i++)
|
||||
Assert.Equal(rotations[i], plate.Parts[i].Rotation, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_Twice_PreservesRotation()
|
||||
{
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotation = System.Math.PI / 3; // 60 degrees
|
||||
var part = Part.CreateAtOrigin(drawing, rotation);
|
||||
part.Offset(10, 10);
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(part);
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
|
||||
// Assign again (this removes first, then re-applies)
|
||||
assigner.Assign(plate);
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Assign_AfterExternalHasManualLeadIns_PreservesRotation()
|
||||
{
|
||||
// Simulates loading a nest where HasManualLeadIns was saved as true
|
||||
// but the program doesn't actually contain lead-in codes.
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotation = System.Math.PI / 2;
|
||||
var part = Part.CreateAtOrigin(drawing, rotation);
|
||||
part.Offset(10, 10);
|
||||
part.HasManualLeadIns = true; // externally set (e.g., by NestReader)
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(part);
|
||||
plate.CuttingParameters = new CuttingParameters
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
assigner.Assign(plate);
|
||||
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLeadIns_AfterExternalHasManualLeadIns_PreservesRotation()
|
||||
{
|
||||
// Simulates the case where HasManualLeadIns is set externally
|
||||
// and then lead-ins are removed.
|
||||
var drawing = MakeSquareDrawing();
|
||||
var rotation = System.Math.PI / 3;
|
||||
var part = Part.CreateAtOrigin(drawing, rotation);
|
||||
part.Offset(10, 10);
|
||||
part.HasManualLeadIns = true;
|
||||
|
||||
part.RemoveLeadIns();
|
||||
|
||||
Assert.Equal(rotation, part.Rotation, 6);
|
||||
}
|
||||
|
||||
private static Drawing MakeSquareDrawing()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("test", pgm);
|
||||
}
|
||||
|
||||
private static Box GetEntityBounds(List<OpenNest.Geometry.Entity> entities)
|
||||
{
|
||||
double minX = double.MaxValue,
|
||||
minY = double.MaxValue;
|
||||
double maxX = double.MinValue,
|
||||
maxY = double.MinValue;
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (entity is OpenNest.Geometry.Line line)
|
||||
{
|
||||
UpdateBounds(line.StartPoint, ref minX, ref minY, ref maxX, ref maxY);
|
||||
UpdateBounds(line.EndPoint, ref minX, ref minY, ref maxX, ref maxY);
|
||||
}
|
||||
}
|
||||
|
||||
return new Box(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
private static void UpdateBounds(
|
||||
Vector pt,
|
||||
ref double minX,
|
||||
ref double minY,
|
||||
ref double maxX,
|
||||
ref double maxY
|
||||
)
|
||||
{
|
||||
if (pt.X < minX)
|
||||
minX = pt.X;
|
||||
if (pt.Y < minY)
|
||||
minY = pt.Y;
|
||||
if (pt.X > maxX)
|
||||
maxX = pt.X;
|
||||
if (pt.Y > maxY)
|
||||
maxY = pt.Y;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class PartLeadInSerializationTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleManualLeadIn_LockedPartIsSkippedByAssignerAfterReload()
|
||||
public void SingleManualLeadIn_StaysLockedAfterReload()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var part = nest.Plates[0].Parts[0];
|
||||
@@ -46,14 +46,9 @@ public class PartLeadInSerializationTests
|
||||
var loaded = Read(Save(nest));
|
||||
var plate = loaded.Plates[0];
|
||||
var restored = plate.Parts[0];
|
||||
var program = restored.Program;
|
||||
plate.CuttingParameters = Parameters();
|
||||
|
||||
new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(plate);
|
||||
|
||||
Assert.True(restored.HasManualLeadIns);
|
||||
Assert.True(restored.LeadInsLocked);
|
||||
Assert.Same(program, restored.Program);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Support;
|
||||
using OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
namespace OpenNest.Tests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Parts the support grid cannot hold level: hand-placed lead-ins tab them, Plan Cutting warns
|
||||
/// when it cannot steady them, and the pre-post review warns about any without a steadying tab.
|
||||
/// </summary>
|
||||
public class SupportReviewTests
|
||||
{
|
||||
// One row of grid points at y = 2.25 runs under the 10 x 1 strip at (2, 2), below its center.
|
||||
private static readonly SupportGrid OneRow = new(2, 4, 0, 2.25, stabilityMargin: 0.1);
|
||||
|
||||
private static CuttingParameters Parameters(bool tabs = false) => new()
|
||||
{
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 90 },
|
||||
SupportTabs = true,
|
||||
TabsEnabled = tabs,
|
||||
TabConfig = new NormalTab { Size = 0.2 },
|
||||
};
|
||||
|
||||
private static (Nest Nest, Plate Plate, Part Part) Strip(SupportBed bed, double height = 1)
|
||||
{
|
||||
var nest = new Nest("support");
|
||||
nest.SetSupportBed(bed);
|
||||
var plate = new Plate(20, 20) { CuttingParameters = Parameters() };
|
||||
nest.Plates.Add(plate);
|
||||
var part = new Part(new Drawing("strip", LeadPathValidationTests.Rectangle(0, 0, 10, height)), new Vector(2, 2));
|
||||
plate.Parts.Add(part);
|
||||
return (nest, plate, part);
|
||||
}
|
||||
|
||||
private static void PlaceLeadIn(Part part, CuttingParameters parameters, Vector point, Line edge) =>
|
||||
part.ApplySingleLeadIn(parameters, point, edge, ContourType.External);
|
||||
|
||||
[Fact]
|
||||
public void ManualLeadIn_TabsAnUnsupportedPartWithoutChangingTheSettings()
|
||||
{
|
||||
var (_, plate, part) = Strip(OneRow);
|
||||
var parameters = Parameters();
|
||||
|
||||
var used = SupportTabSettings.ForManualLeadIn(parameters, plate, part);
|
||||
|
||||
Assert.NotSame(parameters, used);
|
||||
Assert.True(used.TabsEnabled);
|
||||
Assert.False(parameters.TabsEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualLeadIn_KeepsTheSettingsForASupportedPartOrWithSupportTabsOff()
|
||||
{
|
||||
var (_, plate, part) = Strip(new SupportGrid(0.5, 0.5, 0.25, 0.25, stabilityMargin: 0.1));
|
||||
var parameters = Parameters();
|
||||
Assert.Same(parameters, SupportTabSettings.ForManualLeadIn(parameters, plate, part));
|
||||
|
||||
var (_, tipping, strip) = Strip(OneRow);
|
||||
parameters.SupportTabs = false;
|
||||
Assert.Same(parameters, SupportTabSettings.ForManualLeadIn(parameters, tipping, strip));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrePost_WarnsAboutAnUnsupportedPartWithoutATab()
|
||||
{
|
||||
var (nest, _, part) = Strip(OneRow);
|
||||
PlaceLeadIn(part, Parameters(), new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1)));
|
||||
|
||||
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings,
|
||||
f => f.Kind == PostVerificationKind.PartSupport);
|
||||
|
||||
Assert.Equal(1, finding.PartNumber);
|
||||
Assert.Contains("no tab", finding.Message);
|
||||
Assert.Equal(new Vector(7, 2.5), finding.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrePost_AcceptsATabThatSteadiesThePart_AndWarnsAboutOneThatDoesNot()
|
||||
{
|
||||
var (steadied, _, top) = Strip(OneRow);
|
||||
PlaceLeadIn(top, Parameters(tabs: true), new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1)));
|
||||
var (unsteady, _, bottom) = Strip(OneRow);
|
||||
PlaceLeadIn(bottom, Parameters(tabs: true), new Vector(5, 0), new Line(new Vector(10, 0), new Vector(0, 0)));
|
||||
|
||||
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(steadied).Findings,
|
||||
f => f.Kind == PostVerificationKind.PartSupport);
|
||||
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(unsteady).Findings,
|
||||
f => f.Kind == PostVerificationKind.PartSupport);
|
||||
Assert.Contains("does not steady it", finding.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrePost_IgnoresPartsWithoutSupportTabsOrABed()
|
||||
{
|
||||
var (nest, _, part) = Strip(OneRow);
|
||||
var parameters = Parameters();
|
||||
parameters.SupportTabs = false;
|
||||
// A placed lead-in keeps its settings on the part, which the review reads first.
|
||||
PlaceLeadIn(part, parameters, new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1)));
|
||||
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(nest).Findings, f => f.Kind == PostVerificationKind.PartSupport);
|
||||
|
||||
parameters.SupportTabs = true;
|
||||
nest.SetSupportBed(null);
|
||||
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(nest).Findings, f => f.Kind == PostVerificationKind.PartSupport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanCutting_WarnsWhenNoSingleTabCanSteadyThePart()
|
||||
{
|
||||
// The 10 x 0.8 strip lies between grid rows: it rests on nothing, so a tab only hangs it.
|
||||
var (_, plate, _) = Strip(new SupportGrid(1, 1, 0, 0.9, stabilityMargin: 0.1), height: 0.8);
|
||||
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
|
||||
confirmedParameters: Parameters()));
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
|
||||
var finding = Assert.Single(result.Findings, f => f.Kind == PostVerificationKind.PartSupport);
|
||||
Assert.Contains("no single tab can steady it", finding.Message);
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,7 @@ public class TabbedCutoutBuilderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignLeadIns_EntersEachPieceFromTheSlug_WithNoLeadOut()
|
||||
public void ApplyLeadIns_EntersEachPieceFromTheSlug_WithNoLeadOut()
|
||||
{
|
||||
var (entities, square, _) = Part();
|
||||
var drawing = Build(entities, On(square));
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Gives every unlocked part on a plate its lead-ins from the plate's cutting parameters, left
|
||||
/// to right, the way the retired Assign Lead-ins command did. Post fixtures use it to build
|
||||
/// programs with lead-ins without running Plan Cutting, so their expected output is unchanged.
|
||||
/// </summary>
|
||||
internal static class TestLeadIns
|
||||
{
|
||||
public static void Apply(Plate plate)
|
||||
{
|
||||
var parameters = plate.CuttingParameters;
|
||||
if (parameters == null)
|
||||
return;
|
||||
|
||||
var sequenced = new LeftSideSequencer().Sequence(plate.Parts.ToList(), plate);
|
||||
var exitPoint = PlateHelper.GetExitPoint(plate);
|
||||
// The first pass fixes each part's pierce; the second faces each part toward the next pierce.
|
||||
var piercePoints = Pass(null);
|
||||
Pass(piercePoints);
|
||||
|
||||
Vector[] Pass(Vector[] nextPiercePoints)
|
||||
{
|
||||
var pierces = new Vector[sequenced.Count];
|
||||
var current = exitPoint;
|
||||
for (var i = 0; i < sequenced.Count; i++)
|
||||
{
|
||||
var part = sequenced[i].Part;
|
||||
if (!part.LeadInsLocked)
|
||||
{
|
||||
if (part.HasManualLeadIns)
|
||||
part.RemoveLeadIns();
|
||||
var approach = current - part.Location;
|
||||
if (nextPiercePoints != null && i + 1 < sequenced.Count)
|
||||
part.ApplyLeadIns(parameters, approach, nextPiercePoints[i + 1] - part.Location);
|
||||
else
|
||||
part.ApplyLeadIns(parameters, approach);
|
||||
}
|
||||
pierces[i] = Pierce(part);
|
||||
current = part.Location;
|
||||
}
|
||||
return pierces;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector Pierce(Part part)
|
||||
{
|
||||
foreach (var code in part.Program.Codes)
|
||||
if (code is Motion motion)
|
||||
return motion.EndPoint + part.Location;
|
||||
return part.Location;
|
||||
}
|
||||
}
|
||||
@@ -6,31 +6,31 @@ namespace OpenNest.Tests.CuttingStrategy;
|
||||
public class CuttingParametersSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
public void RoundTrip_PreservesAutoTabFields()
|
||||
public void RoundTrip_PreservesSupportTabs()
|
||||
{
|
||||
var original = new CuttingParameters
|
||||
{
|
||||
AutoTabMinSize = 0.5,
|
||||
AutoTabMaxSize = 3.0,
|
||||
SupportTabs = true,
|
||||
ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
|
||||
};
|
||||
|
||||
var json = CuttingParametersSerializer.Serialize(original);
|
||||
var restored = CuttingParametersSerializer.Deserialize(json);
|
||||
|
||||
Assert.Equal(0.5, restored.AutoTabMinSize);
|
||||
Assert.Equal(3.0, restored.AutoTabMaxSize);
|
||||
Assert.True(restored.SupportTabs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_MissingAutoTabFields_DefaultsToZero()
|
||||
public void Deserialize_RetiredAutoTabFields_AreIgnored()
|
||||
{
|
||||
// Settings saved before Auto Tab was removed still load; its sizes are dropped.
|
||||
var json =
|
||||
"{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625}";
|
||||
"{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625,\"autoTabMinSize\":0.5,\"autoTabMaxSize\":3.0}";
|
||||
|
||||
var restored = CuttingParametersSerializer.Deserialize(json);
|
||||
|
||||
Assert.Equal(0.0, restored.AutoTabMinSize);
|
||||
Assert.Equal(0.0, restored.AutoTabMaxSize);
|
||||
Assert.False(restored.TabsEnabled);
|
||||
Assert.False(restored.SupportTabs);
|
||||
Assert.Equal(0.0625, restored.PierceClearance);
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,7 @@ namespace OpenNest.Actions
|
||||
if (form == null)
|
||||
return;
|
||||
|
||||
cuttingPanel = new CuttingPanel { ShowAutoAssign = true };
|
||||
cuttingPanel.AutoAssignClicked += OnAutoAssignClicked;
|
||||
cuttingPanel = new CuttingPanel();
|
||||
cuttingPanel.ParametersChanged += OnToolParametersChanged;
|
||||
|
||||
// Load current parameters or defaults
|
||||
@@ -130,7 +129,6 @@ namespace OpenNest.Actions
|
||||
SaveParameters();
|
||||
|
||||
cuttingPanel.ParametersChanged -= OnToolParametersChanged;
|
||||
cuttingPanel.AutoAssignClicked -= OnAutoAssignClicked;
|
||||
|
||||
var form = plateView.FindForm() as EditNestForm;
|
||||
form?.HideSidePanel();
|
||||
@@ -492,13 +490,12 @@ namespace OpenNest.Actions
|
||||
|
||||
private void CommitLeadIn()
|
||||
{
|
||||
var parameters = GetCurrentParameters();
|
||||
|
||||
if (selectedPart.HasManualLeadIns)
|
||||
selectedPart.RemoveLeadIns();
|
||||
|
||||
ApplyAutoTab(parameters);
|
||||
|
||||
// A part the support grid cannot hold level is tabbed at the clicked entry.
|
||||
var parameters = Support.SupportTabSettings.ForManualLeadIn(GetCurrentParameters(),
|
||||
plateView.Plate, selectedPart);
|
||||
selectedPart.ApplySingleLeadIn(parameters, snapPoint, snapEntity, snapContourType);
|
||||
selectedPart.LeadInsLocked = true;
|
||||
|
||||
@@ -507,38 +504,6 @@ namespace OpenNest.Actions
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void OnAutoAssignClicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (selectedPart == null)
|
||||
return;
|
||||
|
||||
var parameters = GetCurrentParameters();
|
||||
|
||||
if (selectedPart.HasManualLeadIns)
|
||||
selectedPart.RemoveLeadIns();
|
||||
|
||||
ApplyAutoTab(parameters);
|
||||
|
||||
selectedPart.ApplyLeadIns(parameters, Vector.Zero);
|
||||
selectedPart.LeadInsLocked = true;
|
||||
|
||||
selectedLayoutPart.IsDirty = true;
|
||||
UnlockContour();
|
||||
plateView.Invalidate();
|
||||
}
|
||||
|
||||
private void ApplyAutoTab(CuttingParameters parameters)
|
||||
{
|
||||
if (parameters.AutoTabMinSize <= 0 && parameters.AutoTabMaxSize <= 0)
|
||||
return;
|
||||
|
||||
var bbox = selectedPart.Program.BoundingBox();
|
||||
var minDim = System.Math.Min(bbox.Width, bbox.Length);
|
||||
|
||||
if (minDim >= parameters.AutoTabMinSize && minDim <= parameters.AutoTabMaxSize)
|
||||
parameters.TabsEnabled = true;
|
||||
}
|
||||
|
||||
private void DeselectPart()
|
||||
{
|
||||
if (selectedLayoutPart != null)
|
||||
|
||||
@@ -37,28 +37,16 @@ namespace OpenNest.Controls
|
||||
private readonly CheckBox chkTabsEnabled;
|
||||
private readonly NumericUpDown nudTabWidth;
|
||||
private readonly RadioButton rbTabAll;
|
||||
private readonly RadioButton rbAutoTab;
|
||||
private readonly RadioButton rbSupportTab;
|
||||
private readonly NumericUpDown nudAutoTabMin;
|
||||
private readonly NumericUpDown nudAutoTabMax;
|
||||
private readonly NumericUpDown nudPierceClearance;
|
||||
private readonly NumericUpDown nudPierceDiameter;
|
||||
|
||||
private readonly CheckBox chkRoundLeadInAngles;
|
||||
private readonly NumericUpDown nudLeadInAngleIncrement;
|
||||
|
||||
private readonly Button btnAutoAssign;
|
||||
|
||||
private bool suppressEvents;
|
||||
|
||||
public event EventHandler ParametersChanged;
|
||||
public event EventHandler AutoAssignClicked;
|
||||
|
||||
public bool ShowAutoAssign
|
||||
{
|
||||
get => btnAutoAssign.Visible;
|
||||
set => btnAutoAssign.Visible = value;
|
||||
}
|
||||
|
||||
public ContourType? ActiveContourType
|
||||
{
|
||||
@@ -136,7 +124,7 @@ namespace OpenNest.Controls
|
||||
{
|
||||
HeaderText = "Tabs",
|
||||
Dock = DockStyle.Top,
|
||||
ExpandedHeight = 160,
|
||||
ExpandedHeight = 106,
|
||||
IsExpanded = false,
|
||||
};
|
||||
|
||||
@@ -171,46 +159,11 @@ namespace OpenNest.Controls
|
||||
};
|
||||
tabsPanel.ContentPanel.Controls.Add(rbTabAll);
|
||||
|
||||
rbAutoTab = new RadioButton
|
||||
{
|
||||
Text = "Auto-tab when smallest part dimension is between:",
|
||||
Location = new Point(28, 50),
|
||||
AutoSize = true,
|
||||
Enabled = false,
|
||||
};
|
||||
tabsPanel.ContentPanel.Controls.Add(rbAutoTab);
|
||||
|
||||
tabsPanel.ContentPanel.Controls.Add(
|
||||
new Label
|
||||
{
|
||||
Text = "Min:",
|
||||
Location = new Point(44, 76),
|
||||
AutoSize = true,
|
||||
}
|
||||
);
|
||||
|
||||
nudAutoTabMin = CreateNumeric(77, 73, 0, 0.0625);
|
||||
nudAutoTabMin.Enabled = false;
|
||||
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMin);
|
||||
|
||||
tabsPanel.ContentPanel.Controls.Add(
|
||||
new Label
|
||||
{
|
||||
Text = "Max:",
|
||||
Location = new Point(210, 76),
|
||||
AutoSize = true,
|
||||
}
|
||||
);
|
||||
|
||||
nudAutoTabMax = CreateNumeric(245, 73, 0, 0.0625);
|
||||
nudAutoTabMax.Enabled = false;
|
||||
tabsPanel.ContentPanel.Controls.Add(nudAutoTabMax);
|
||||
|
||||
// Plan Cutting tabs only the parts the plate's support grid cannot hold level.
|
||||
rbSupportTab = new RadioButton
|
||||
{
|
||||
Text = "Tab parts the support grid cannot hold level",
|
||||
Location = new Point(28, 102),
|
||||
Location = new Point(28, 50),
|
||||
AutoSize = true,
|
||||
Enabled = false,
|
||||
};
|
||||
@@ -221,27 +174,12 @@ namespace OpenNest.Controls
|
||||
var enabled = chkTabsEnabled.Checked;
|
||||
nudTabWidth.Enabled = enabled;
|
||||
rbTabAll.Enabled = enabled;
|
||||
rbAutoTab.Enabled = enabled;
|
||||
rbSupportTab.Enabled = enabled;
|
||||
nudAutoTabMin.Enabled = enabled && rbAutoTab.Checked;
|
||||
nudAutoTabMax.Enabled = enabled && rbAutoTab.Checked;
|
||||
OnParametersChanged();
|
||||
};
|
||||
|
||||
rbTabAll.CheckedChanged += (s, e) =>
|
||||
{
|
||||
nudAutoTabMin.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
|
||||
nudAutoTabMax.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
|
||||
OnParametersChanged();
|
||||
};
|
||||
|
||||
// Switching between auto and support tabs does not pass through Tab all parts.
|
||||
rbSupportTab.CheckedChanged += (s, e) =>
|
||||
{
|
||||
nudAutoTabMin.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
|
||||
nudAutoTabMax.Enabled = chkTabsEnabled.Checked && rbAutoTab.Checked;
|
||||
OnParametersChanged();
|
||||
};
|
||||
// The two choices toggle together, so one handler sees every change.
|
||||
rbTabAll.CheckedChanged += (s, e) => OnParametersChanged();
|
||||
|
||||
// Pierce section
|
||||
var piercePanel = new CollapsiblePanel
|
||||
@@ -308,26 +246,7 @@ namespace OpenNest.Controls
|
||||
nudLeadInAngleIncrement.ValueChanged += (s, e) => OnParametersChanged();
|
||||
piercePanel.ContentPanel.Controls.Add(nudLeadInAngleIncrement);
|
||||
|
||||
// Auto-Assign button — wrapped in a panel for Dock.Top with padding
|
||||
btnAutoAssign = new Button
|
||||
{
|
||||
Text = "Auto-Assign Lead-ins",
|
||||
Dock = DockStyle.Top,
|
||||
Height = 32,
|
||||
Visible = false,
|
||||
};
|
||||
btnAutoAssign.Click += (s, e) => AutoAssignClicked?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
var btnWrapper = new Panel
|
||||
{
|
||||
Dock = DockStyle.Top,
|
||||
Height = 36,
|
||||
Padding = new Padding(4, 2, 4, 2),
|
||||
};
|
||||
btnWrapper.Controls.Add(btnAutoAssign);
|
||||
|
||||
// Add in reverse order — Dock.Top stacks top-down
|
||||
Controls.Add(btnWrapper);
|
||||
Controls.Add(piercePanel);
|
||||
Controls.Add(tabsPanel);
|
||||
Controls.Add(tabWrapper);
|
||||
@@ -353,10 +272,6 @@ namespace OpenNest.Controls
|
||||
PierceDiameter = (double)nudPierceDiameter.Value,
|
||||
RoundLeadInAngles = chkRoundLeadInAngles.Checked,
|
||||
LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value,
|
||||
AutoTabMinSize =
|
||||
chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMin.Value : 0,
|
||||
AutoTabMaxSize =
|
||||
chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMax.Value : 0,
|
||||
SupportTabs = chkTabsEnabled.Checked && rbSupportTab.Checked,
|
||||
};
|
||||
}
|
||||
@@ -372,11 +287,9 @@ namespace OpenNest.Controls
|
||||
LoadLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn, p.ArcCircleLeadIn);
|
||||
LoadLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut, p.ArcCircleLeadOut);
|
||||
|
||||
var hasAutoTab = p.AutoTabMinSize > 0 || p.AutoTabMaxSize > 0;
|
||||
chkTabsEnabled.Checked = p.TabsEnabled || hasAutoTab || p.SupportTabs;
|
||||
rbAutoTab.Checked = hasAutoTab && !p.SupportTabs;
|
||||
chkTabsEnabled.Checked = p.TabsEnabled || p.SupportTabs;
|
||||
rbSupportTab.Checked = p.SupportTabs;
|
||||
rbTabAll.Checked = !hasAutoTab && !p.SupportTabs;
|
||||
rbTabAll.Checked = !p.SupportTabs;
|
||||
if (p.TabConfig != null)
|
||||
nudTabWidth.Value = (decimal)p.TabConfig.Size;
|
||||
nudPierceClearance.Value = (decimal)p.PierceClearance;
|
||||
@@ -384,8 +297,6 @@ namespace OpenNest.Controls
|
||||
chkRoundLeadInAngles.Checked = p.RoundLeadInAngles;
|
||||
nudLeadInAngleIncrement.Value = (decimal)p.LeadInAngleIncrement;
|
||||
nudLeadInAngleIncrement.Enabled = p.RoundLeadInAngles;
|
||||
nudAutoTabMin.Value = (decimal)p.AutoTabMinSize;
|
||||
nudAutoTabMax.Value = (decimal)p.AutoTabMaxSize;
|
||||
|
||||
suppressEvents = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Runtime.CompilerServices;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Support;
|
||||
@@ -18,6 +19,9 @@ namespace OpenNest.Controls
|
||||
// Grid points are drawn only while they are at least this far apart on screen.
|
||||
private const float MinimumGridSpacing = 6f;
|
||||
|
||||
// Grid points are this many whole pixels square.
|
||||
private const float GridPointSize = 3f;
|
||||
|
||||
private static readonly Color GridColor = Color.FromArgb(110, 120, 120, 120);
|
||||
private static readonly Color ContactColor = Color.FromArgb(220, 30, 150, 60);
|
||||
private static readonly Color SupportedColor = Color.FromArgb(230, 20, 140, 50);
|
||||
@@ -80,8 +84,32 @@ namespace OpenNest.Controls
|
||||
var bottom = plate.Quadrant is 3 or 4 ? -plate.Size.Width : 0;
|
||||
var sheet = new Box(left, bottom, plate.Size.Length, plate.Size.Width);
|
||||
using var brush = new SolidBrush(GridColor);
|
||||
foreach (var point in grid.PointsIn(sheet, plate.Quadrant))
|
||||
Dot(g, view, point, 1.5f, brush);
|
||||
// Anti-aliased dots smear differently at each subpixel offset; whole-pixel squares
|
||||
// drawn without smoothing are all the same size.
|
||||
// The view is translated by a fractional origin, so snap in device pixels.
|
||||
using var transform = g.Transform;
|
||||
var dx = transform.OffsetX;
|
||||
var dy = transform.OffsetY;
|
||||
var half = (GridPointSize - 1) / 2;
|
||||
var smoothing = g.SmoothingMode;
|
||||
var offset = g.PixelOffsetMode;
|
||||
g.SmoothingMode = SmoothingMode.None;
|
||||
g.PixelOffsetMode = PixelOffsetMode.Half;
|
||||
try
|
||||
{
|
||||
foreach (var point in grid.PointsIn(sheet, plate.Quadrant))
|
||||
{
|
||||
var p = view.PointWorldToGraph(point);
|
||||
var x = (float)System.Math.Round(p.X + dx) - dx - half;
|
||||
var y = (float)System.Math.Round(p.Y + dy) - dy - half;
|
||||
g.FillRectangle(brush, x, y, GridPointSize, GridPointSize);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
g.SmoothingMode = smoothing;
|
||||
g.PixelOffsetMode = offset;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Dot(Graphics g, PlateView view, Vector point, float radius, Brush brush)
|
||||
|
||||
@@ -550,6 +550,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
PostVerificationKind.NestedLead => "Nested part lead",
|
||||
PostVerificationKind.PierceZone => "Pierce zone",
|
||||
PostVerificationKind.PartSpacing => "Part spacing",
|
||||
PostVerificationKind.PartSupport => "Part support",
|
||||
_ => kind.ToString(),
|
||||
};
|
||||
|
||||
@@ -615,6 +616,7 @@ internal sealed class CuttingInspectionPanel : UserControl
|
||||
PostVerificationKind.NestedLead => "The ring marks the middle of the lead that meets the other part.",
|
||||
PostVerificationKind.PierceZone => "The ring marks the pierce that is too close to part material.",
|
||||
PostVerificationKind.PartSpacing => "The ring marks where the two parts come closest.",
|
||||
PostVerificationKind.PartSupport => "The ring marks the part's center of gravity.",
|
||||
_ => "The ring marks the affected part; the exact spot is not known."
|
||||
};
|
||||
|
||||
|
||||
@@ -1112,35 +1112,6 @@ namespace OpenNest.Forms
|
||||
CalculateCurrentPlateCutTime();
|
||||
}
|
||||
|
||||
public void AssignLeadIns_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (PlateView?.Plate == null)
|
||||
return;
|
||||
|
||||
var plate = PlateView.Plate;
|
||||
|
||||
var parameters = LoadOrDefaultParameters(plate.CuttingParameters);
|
||||
|
||||
using var dlg = new CuttingParametersDialog();
|
||||
dlg.LoadParameters(parameters);
|
||||
|
||||
if (dlg.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
parameters = dlg.GetParameters();
|
||||
plate.CuttingParameters = parameters;
|
||||
SaveCuttingParameters(parameters);
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
var kept = assigner.Assign(plate);
|
||||
|
||||
foreach (var lp in PlateView.Parts)
|
||||
lp.IsDirty = true;
|
||||
|
||||
PlateView.Invalidate();
|
||||
ShowKeptDrawingLeadIns([(Nest.Plates.IndexOf(plate) + 1, PartNumbers(plate, kept))]);
|
||||
}
|
||||
|
||||
public void RemoveLeadIns_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (PlateView?.Plate == null)
|
||||
@@ -1173,47 +1144,6 @@ namespace OpenNest.Forms
|
||||
PlateView.Invalidate();
|
||||
}
|
||||
|
||||
public void AssignLeadInsAllPlates()
|
||||
{
|
||||
if (Nest == null)
|
||||
return;
|
||||
|
||||
var parameters = LoadOrDefaultParameters(PlateView?.Plate?.CuttingParameters);
|
||||
|
||||
using var dlg = new CuttingParametersDialog();
|
||||
dlg.LoadParameters(parameters);
|
||||
|
||||
if (dlg.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
parameters = dlg.GetParameters();
|
||||
SaveCuttingParameters(parameters);
|
||||
|
||||
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
|
||||
var kept = new List<(int, IReadOnlyList<int>)>();
|
||||
|
||||
for (var index = 0; index < Nest.Plates.Count; index++)
|
||||
{
|
||||
var plate = Nest.Plates[index];
|
||||
plate.CuttingParameters = parameters;
|
||||
kept.Add((index + 1, PartNumbers(plate, assigner.Assign(plate))));
|
||||
}
|
||||
|
||||
PlateView.Invalidate();
|
||||
ShowKeptDrawingLeadIns(kept);
|
||||
}
|
||||
|
||||
// Assign Lead-ins never overwrites drawing pins; say which parts it left for Plan Cutting.
|
||||
private void ShowKeptDrawingLeadIns(IReadOnlyList<(int PlateNumber, IReadOnlyList<int> PartNumbers)> kept)
|
||||
{
|
||||
var message = LeadInAssigner.KeptMessage(kept);
|
||||
if (message != null)
|
||||
MessageBox.Show(this, message, "Assign Lead-ins", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<int> PartNumbers(Plate plate, IReadOnlyList<Part> parts) =>
|
||||
parts.Select(part => plate.Parts.IndexOf(part) + 1).Order().ToList();
|
||||
|
||||
public void RemoveLeadInsAllPlates()
|
||||
{
|
||||
if (Nest == null)
|
||||
|
||||
Generated
+2
-20
@@ -112,7 +112,6 @@ namespace OpenNest.Forms
|
||||
toolStripMenuItem22 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuNestAutomaticCutOff = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestPlanCutting = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestAssignLeadIns = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuNestRemoveLeadIns = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuPlate = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuPlateEdit = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -152,7 +151,6 @@ namespace OpenNest.Forms
|
||||
mnuPlateSimulateCutting = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem21 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuPlatePlanCutting = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuPlateAssignLeadIns = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuPlatePlaceLeadIn = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuPlateRemoveLeadIns = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem20 = new System.Windows.Forms.ToolStripSeparator();
|
||||
@@ -585,7 +583,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// mnuNest
|
||||
//
|
||||
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestSupportGrid, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestClear, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
|
||||
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestSupportGrid, mnuNestImportDrawing, mnuNestShapeLibrary, mnuNestDrawingLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestClear, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestRemoveLeadIns });
|
||||
mnuNest.Name = "mnuNest";
|
||||
mnuNest.Size = new System.Drawing.Size(43, 20);
|
||||
mnuNest.Text = "&Nest";
|
||||
@@ -755,13 +753,6 @@ namespace OpenNest.Forms
|
||||
mnuNestPlanCutting.Text = "Plan Cutting (All Plates)...";
|
||||
mnuNestPlanCutting.Click += NestPlanCutting_Click;
|
||||
//
|
||||
// mnuNestAssignLeadIns
|
||||
//
|
||||
mnuNestAssignLeadIns.Name = "mnuNestAssignLeadIns";
|
||||
mnuNestAssignLeadIns.Size = new System.Drawing.Size(205, 22);
|
||||
mnuNestAssignLeadIns.Text = "Assign Lead-ins (All Plates)";
|
||||
mnuNestAssignLeadIns.Click += NestAssignLeadIns_Click;
|
||||
//
|
||||
// mnuNestRemoveLeadIns
|
||||
//
|
||||
mnuNestRemoveLeadIns.Name = "mnuNestRemoveLeadIns";
|
||||
@@ -771,7 +762,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// mnuPlate
|
||||
//
|
||||
mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateCutOff, mnuPlateAutomaticCutOff, mnuPlateEditClamps, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateArrange, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, mnuPlateSimulateCutting, toolStripMenuItem21, mnuPlatePlanCutting, mnuPlateAssignLeadIns, mnuPlatePlaceLeadIn, mnuPlateRemoveLeadIns, toolStripMenuItem20, mnuPlateViewInCad });
|
||||
mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateCutOff, mnuPlateAutomaticCutOff, mnuPlateEditClamps, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateArrange, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, mnuPlateSimulateCutting, toolStripMenuItem21, mnuPlatePlanCutting, mnuPlatePlaceLeadIn, mnuPlateRemoveLeadIns, toolStripMenuItem20, mnuPlateViewInCad });
|
||||
mnuPlate.Name = "mnuPlate";
|
||||
mnuPlate.Size = new System.Drawing.Size(45, 20);
|
||||
mnuPlate.Text = "&Plate";
|
||||
@@ -1043,13 +1034,6 @@ namespace OpenNest.Forms
|
||||
mnuPlatePlanCutting.Text = "Plan Cutting...";
|
||||
mnuPlatePlanCutting.Click += PlatePlanCutting_Click;
|
||||
//
|
||||
// mnuPlateAssignLeadIns
|
||||
//
|
||||
mnuPlateAssignLeadIns.Name = "mnuPlateAssignLeadIns";
|
||||
mnuPlateAssignLeadIns.Size = new System.Drawing.Size(177, 22);
|
||||
mnuPlateAssignLeadIns.Text = "Assign Lead-ins";
|
||||
mnuPlateAssignLeadIns.Click += PlateAssignLeadIns_Click;
|
||||
//
|
||||
// mnuPlatePlaceLeadIn
|
||||
//
|
||||
mnuPlatePlaceLeadIn.Name = "mnuPlatePlaceLeadIn";
|
||||
@@ -1479,13 +1463,11 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuPlateAutomaticCutOff;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem21;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuPlatePlanCutting;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuPlateAssignLeadIns;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuPlatePlaceLeadIn;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuPlateRemoveLeadIns;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem22;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestAutomaticCutOff;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestPlanCutting;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestAssignLeadIns;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuNestRemoveLeadIns;
|
||||
}
|
||||
}
|
||||
@@ -263,8 +263,6 @@ namespace OpenNest.Forms
|
||||
|
||||
private void CalculateNestCutTime_Click(object sender, EventArgs e) => WithActive(f => f.CalculateNestCutTime());
|
||||
|
||||
private void NestAssignLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.AssignLeadInsAllPlates());
|
||||
|
||||
private void NestRemoveLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.RemoveLeadInsAllPlates());
|
||||
|
||||
#endregion Nest Menu Events
|
||||
|
||||
@@ -150,8 +150,6 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
private void PlateAssignLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.AssignLeadIns_Click(sender, e));
|
||||
|
||||
private void PlatePlaceLeadIn_Click(object sender, EventArgs e) => WithActive(f => f.PlaceLeadIn_Click(sender, e));
|
||||
|
||||
private void PlateRemoveLeadIns_Click(object sender, EventArgs e) => WithActive(f => f.RemoveLeadIns_Click(sender, e));
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ material capture, overlap checks, nesting and area ignore them. `ShapeProfile` a
|
||||
`ConvertGeometry` pass them through in order without re-chaining or re-winding.
|
||||
|
||||
Both Plan Cutting (`PreparedContours`, emitted after scribes and before contours in
|
||||
every full or prefix emission, never in single-contour probes) and Assign Lead-ins
|
||||
every full or prefix emission, never in single-contour probes) and the manual lead-in tool
|
||||
(`ContourCuttingStrategy.Apply`/`ApplySingle`) emit them through
|
||||
`ContourCuttingStrategy.EmitBreakCuts`:
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ returns an owned proposal; `Apply` installs Ready plate-scoped proposals atomica
|
||||
after an exact freshness check. The desktop opens it from `Plate > Plan Cutting...` and
|
||||
`Nest > Plan Cutting (All Plates)...` (see [Desktop workflow](#desktop-workflow)); the console
|
||||
and MCP server expose the same batch (see [Console and MCP](#console-and-mcp)); the older
|
||||
automatic sequencing and lead-in assignment commands remain until they are retired.
|
||||
automatic sequencing command remains until it is retired; the older lead-in assignment was removed.
|
||||
|
||||
## Capture before worker planning
|
||||
|
||||
@@ -486,10 +486,9 @@ clean contours with `CutPinResolver` and prepares them with
|
||||
- The plate state compared by `Apply` includes each drawing's pin list (reference and exact
|
||||
content, `StateFingerprint.Of(pins)`) and each part's pins-off flag, so changing either
|
||||
after capture makes the proposal `Stale`.
|
||||
- Only Plan Cutting applies pins. The legacy Assign Lead-ins (`LeadInAssigner`) leaves every
|
||||
unlocked part with active pins unchanged and returns those parts; the desktop then says
|
||||
"N parts kept their drawing lead-ins (plate P: parts ...); use Plan Cutting to apply their
|
||||
drawing pins." It never overwrites a pin with the plate's settings.
|
||||
- Only Plan Cutting applies pins. Plan Cutting is the only automatic lead-in assignment: the
|
||||
legacy Assign Lead-ins commands, the lead-in tool's Auto-Assign and the Auto Tab size
|
||||
range were removed.
|
||||
- The [pre-post review](post-verification.md) warns "Drawing pins not applied" for a part
|
||||
with active pins whose placed program does not cut its pinned contours as pinned, and
|
||||
re-checks pinned leads like locked ones.
|
||||
@@ -737,7 +736,6 @@ The service does not establish clean-material non-overlap, scrap release by open
|
||||
cutoff cuts or sheet edges, or physical retention strength. It does not write CNC
|
||||
or set posting consent. A `Ready` proposal can still be unsuitable for cutting.
|
||||
|
||||
The lead-in side panel's Auto-Assign Lead-ins remains separate from Plan Cutting.
|
||||
Review the desktop preview and the generated CNC output before cutting.
|
||||
Fresh [pre-post verification](post-verification.md) is still required; it is not a
|
||||
physical safety qualification.
|
||||
|
||||
@@ -62,7 +62,7 @@ agreement, approach-angle handling, the flat-corner clearance fallback, reflex
|
||||
notches, and straight lead-outs.
|
||||
|
||||
Windows manual check: import a part with small rectangular cutouts and use Plate >
|
||||
Assign Lead-ins. Confirm diagonal lead-ins at the cutout corners. Under Plate >
|
||||
Plan Cutting. Confirm diagonal lead-ins at the cutout corners. Under Plate >
|
||||
Place Lead-in, select the part, lock a cutout, and hover a corner: the preview
|
||||
should point into the cutout and the committed lead-in should match. Hover a
|
||||
mid-edge point and confirm perpendicular placement is unchanged. Windows visual
|
||||
|
||||
@@ -96,6 +96,9 @@ shows the first three check categories, even when there are no findings:
|
||||
are not reported. A part whose clean material cannot be captured makes the check
|
||||
incomplete for leads near it. This line appears in the summary only when there is such
|
||||
a finding.
|
||||
- **Part support:** on a plate with a support bed, a part set for support tabs that the bed
|
||||
cannot hold level and whose outside cut leaves no tab that keeps it level; the marker is its
|
||||
center of gravity. See [support tabs](support-tabs.md#pre-post-review).
|
||||
- **Part spacing:** two parts closer than the plate's part spacing without sharing material
|
||||
(overlapping pairs are reported only as overlaps). Candidate pairs come from each part's
|
||||
clean nesting material (leads excluded) tested exactly as Auto Nest's layout check tests
|
||||
|
||||
+33
-10
@@ -71,7 +71,7 @@ other parts keep the plate settings, so supported parts are not tabbed.
|
||||
The emitter tabs a perimeter at its entry: the cut stops short of the entry by the tab size.
|
||||
A support-tabbed perimeter's automatic entries are therefore limited to catalogue points the
|
||||
analysis accepts, plus the closest native point to its best tab point (`AutomaticEntryKind.SupportTab`,
|
||||
never on a reflex or cusp vertex). Ranking, lead checks and replay are unchanged.
|
||||
never on a reflex or cusp vertex). Ranking and lead checks are unchanged.
|
||||
|
||||
If none of those entries has a lead-in that fits, the part is still tabbed. The search falls
|
||||
back first to every catalogue entry clear of inside corners, then to every entry
|
||||
@@ -79,16 +79,39 @@ back first to every catalogue entry clear of inside corners, then to every entry
|
||||
lead-in fits, it keeps every one that holds the part level, or else only the steadiest. The
|
||||
open-scrap pierce fallback then tries the last of those catalogues.
|
||||
|
||||
Replay notes each planned part whose tab, at its perimeter entry, cannot hold it level (no single
|
||||
tab can, or no lead-in fits where one would) or sits in an inside corner
|
||||
(`SupportTabCapture.Notes`). The notes are advisory: a Ready plan keeps its status and lists them
|
||||
in Plan Cutting's warnings, and the console and MCP summaries print them under the plate.
|
||||
A drawing pin on the perimeter wins: its entry, leads and tab are kept as pinned, and the
|
||||
analysis is only carried for review. Replay and best-effort plans (`SupportTabCapture.Review`)
|
||||
warn with `PostVerificationKind.PartSupport` whenever an unsupported part's emitted perimeter is
|
||||
untabbed or its entry does not steady the part: 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. Like every planner
|
||||
warning, this makes the proposal UNVERIFIED, so applying it needs explicit acceptance.
|
||||
|
||||
A planned tab that sits in an inside corner, because no lead-in fits clear of one, gets an
|
||||
advisory note instead (`SupportTabCapture.Notes`; never for a pin): a Ready plan keeps its
|
||||
status and lists it in Plan Cutting's warnings, and the console and MCP summaries print it under
|
||||
the plate.
|
||||
|
||||
## Lead-ins placed by hand
|
||||
|
||||
Plate > Place Lead-in cuts where it is clicked. Under "Tab parts the support grid cannot hold
|
||||
level", `SupportTabSettings.ForManualLeadIn` enables tabs on a copy of the settings when the
|
||||
plate's bed cannot hold the clicked part level, so the tab sits at the clicked entry. The
|
||||
overlay's orange diamond shows where it steadies the part; the pre-post review checks the
|
||||
result. Plan Cutting is the only automatic lead-in assignment.
|
||||
|
||||
## Pre-post review
|
||||
|
||||
`PartSupportReview` runs in every pre-post verification and in Validate Nest. On a plate with a
|
||||
support bed, each part whose settings (its own, else the plate's) ask for support tabs and that
|
||||
the bed cannot hold level is checked, however its program was made. The tab is the gap between
|
||||
the end and the start of the program's outside cut (the chain of cutting motions with the
|
||||
largest extent). A part with no such gap, or whose gap does not steady it, gets a
|
||||
`PartSupport` warning at its center of gravity naming the best tab point; posting needs
|
||||
consent as for every warning. A part whose material cannot be read is reported as not checked.
|
||||
|
||||
Not covered yet:
|
||||
|
||||
- A drawing pin on the perimeter wins: its entry and tab are kept as pinned.
|
||||
- Locked or ineligible parts keep their programs; the older Assign Lead-ins command and
|
||||
Auto Tab sizes ignore the bed.
|
||||
- Locked or ineligible parts keep their programs in Plan Cutting; the pre-post review still
|
||||
checks them.
|
||||
- Only one perimeter tab per part. A part that needs more (one tab cannot reach the stability
|
||||
margin) gets the best single tab and a note.
|
||||
- Pre-post verification does not yet warn about unsupported, untabbed parts.
|
||||
margin) gets the best single tab and a warning.
|
||||
@@ -41,7 +41,7 @@ best-fit, `NestLayoutCheck` and the GPU bitmaps. Programs without tab spans read
|
||||
before. A planned program has open pieces instead of a closed hole, so it bounds no hole
|
||||
either way.
|
||||
|
||||
Both Plan Cutting (`PreparedContours`) and Assign Lead-ins
|
||||
Both Plan Cutting (`PreparedContours`) and the manual lead-in tool
|
||||
(`ContourCuttingStrategy.Apply`/`ApplySingle`) take a closed contour with tab spans out of
|
||||
the contour choices. Every full or prefix emission cuts its pieces after the break cuts
|
||||
and before the contours, never in single-contour probes:
|
||||
|
||||
Reference in new issue
Block a user