mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-29 11:21:06 -04:00
feat(cutting): pierce cutouts too small for their lead-in by erosion
Small text and narrow slots often have no entry where the configured lead-in fits, so Plan Cutting refused them. When every entry of a cutout fails, the planner now erodes the cutout inward (to the pierce clearance, or nine tenths of its inscribed radius when smaller), pierces at the eroded point nearest each ranked entry and leads straight to the closest wall, with no lead-out. Letter strokes pierce on their centre lines; a tapering feature pierces at its widest spot. The lead validator still checks every candidate, and cutouts whose lead fits are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d3d7bc0299
commit
37cdc52f0d
10 files changed
+384
-18
No files matched your search
@@ -58,7 +58,7 @@ Read the relevant contract before changing its behavior:
|
||||
|
||||
- [Nest file format](docs/nest-file-format.md)
|
||||
- [Directional slides](docs/geometry/directional-slides.md) and [pair-spacing limits](docs/geometry/pair-spacing.md)
|
||||
- [Lead-in placement](docs/geometry/lead-in-placement.md) and [pierce zones](docs/geometry/pierce-zone.md): opt-in `PierceDiameter` clearance, the hole pole fallback and the planner's perimeter scrap fallback
|
||||
- [Lead-in placement](docs/geometry/lead-in-placement.md) (including the planner's eroded pierce for cutouts too small for their lead-in) and [pierce zones](docs/geometry/pierce-zone.md): opt-in `PierceDiameter` clearance, the hole pole fallback and the planner's perimeter scrap fallback
|
||||
- [Breakoffs](docs/breakoffs.md): tabbed scrap; `Break`-layer open cuts that are cut but are not material
|
||||
- [Tabbed cutouts](docs/tabbed-cutouts.md): holes cut in pieces around `Tab`-layer spans that are material but never cut; nesting reads material through `NestingMaterial.ForNesting`, so nothing nests in them
|
||||
- [Support tabs](docs/support-tabs.md): the nest's support grid, the center-of-gravity check, and Plan Cutting's tab placement for parts the grid cannot hold level
|
||||
|
||||
@@ -42,6 +42,13 @@ internal enum AutomaticEntryKind
|
||||
/// perimeter tabbed for support has it (see <see cref="PreparedContours.WithSupportTab"/>).
|
||||
/// </summary>
|
||||
SupportTab = 8,
|
||||
|
||||
/// <summary>
|
||||
/// A cutout entry pierced where eroding the cutout leaves room (see
|
||||
/// <see cref="PreparedContours.ErodedPierceEntries"/>): only offered once no configured
|
||||
/// lead-in of the cutout fits.
|
||||
/// </summary>
|
||||
ErodedPierce = 9,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,9 +15,11 @@ public sealed record ContourChoice(int ContourOrdinal, int EntityOrdinal, Vector
|
||||
internal PreparedContours Owner { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A perimeter pierce the planner placed in open scrap for the pierce zone (see
|
||||
/// <see cref="PreparedContours.ScrapPierceEntry"/>): the lead-in runs straight from here to
|
||||
/// <see cref="Point"/> instead of the configured lead-in. Null for every other choice.
|
||||
/// A pierce the planner placed off the contour: a perimeter's in open scrap for the pierce
|
||||
/// zone (see <see cref="PreparedContours.ScrapPierceEntry"/>), or a cutout's where eroding it
|
||||
/// leaves room (see <see cref="PreparedContours.ErodedPierceEntries"/>). The lead-in runs
|
||||
/// straight from here to <see cref="Point"/> instead of the configured lead-in; a cutout so
|
||||
/// pierced has no lead-out. Null for every other choice.
|
||||
/// </summary>
|
||||
public Vector? Pierce { get; internal init; }
|
||||
}
|
||||
@@ -444,13 +446,48 @@ public sealed class PreparedContours
|
||||
/// emission reads only this choice and the perimeter, and the lead validator checks the
|
||||
/// lead as usual. A pinned perimeter keeps its pin's entry.
|
||||
/// </summary>
|
||||
internal ContourChoice ScrapPierceEntry(Vector pierce)
|
||||
internal ContourChoice ScrapPierceEntry(Vector pierce) => PiercedEntry(PerimeterOrdinal, pierce);
|
||||
|
||||
/// <summary>
|
||||
/// Entries of cutout <paramref name="contourOrdinal"/> pierced where eroding it leaves room
|
||||
/// (see <see cref="ErodedPierce"/>), the one nearest each of <paramref name="targets"/> (local
|
||||
/// coordinates, usually its ranked entries) in turn: the planner's fallback when no configured
|
||||
/// lead-in fits the cutout. Each enters at the native point of the cutout closest to its
|
||||
/// pierce, along a straight lead-in, with no lead-out. Nothing for the perimeter or a pinned
|
||||
/// cutout; the lead validator still checks each one.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<ContourChoice> ErodedPierceEntries(int contourOrdinal, IReadOnlyList<Vector> targets,
|
||||
int max, CancellationToken token = default)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var shape = GetShape(contourOrdinal);
|
||||
if (contourOrdinal == PerimeterOrdinal || pinned[contourOrdinal] != null)
|
||||
return Array.Empty<ContourChoice>();
|
||||
var found = ErodedPierce.Find(shape, ParametersFor(contourOrdinal).GetEffectivePierceClearance(), targets, max);
|
||||
var entries = new List<ContourChoice>();
|
||||
foreach (var pierce in found.Pierces)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
entries.Add(PiercedEntry(contourOrdinal, pierce));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// A pierce on the wall itself has no lead; the others still stand.
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// A contour entry at the closest native point to a pierce off it, along a straight lead-in.
|
||||
private ContourChoice PiercedEntry(int contourOrdinal, Vector pierce)
|
||||
{
|
||||
PostVerificationGeometry.Validate(pierce);
|
||||
var shape = GetShape(PerimeterOrdinal);
|
||||
var shape = GetShape(contourOrdinal);
|
||||
var point = shape.ClosestPointTo(pierce, out var entity);
|
||||
point = SnapToVertex(shape, shape.Entities.IndexOf(entity), point);
|
||||
var choice = new ContourChoice(PerimeterOrdinal, shape.Entities.IndexOf(entity), point)
|
||||
var choice = new ContourChoice(contourOrdinal, shape.Entities.IndexOf(entity), point)
|
||||
{
|
||||
Owner = this,
|
||||
Pierce = pierce,
|
||||
@@ -866,10 +903,8 @@ public sealed class PreparedContours
|
||||
if (choice.Pierce is { } pierce)
|
||||
{
|
||||
PostVerificationGeometry.Validate(pierce);
|
||||
if (choice.ContourOrdinal != PerimeterOrdinal)
|
||||
throw new ArgumentException("Only a perimeter entry can be pierced in scrap.");
|
||||
if (pierce.DistanceTo(choice.Point) <= PostVerificationGeometry.Epsilon)
|
||||
throw new ArgumentException("A scrap pierce must lie off its contour.");
|
||||
throw new ArgumentException("A planner-placed pierce must lie off its contour.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -449,6 +449,9 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
|
||||
var leadIn = SelectLeadIn(contourType);
|
||||
var leadOut = SelectLeadOut(contourType);
|
||||
// A cutout the planner pierced by erosion is too small for its configured leads.
|
||||
if (scrapPierce != null && contourType != ContourType.External)
|
||||
leadOut = new NoLeadOut();
|
||||
|
||||
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
||||
{
|
||||
@@ -465,9 +468,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
||||
);
|
||||
}
|
||||
|
||||
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal,
|
||||
Parameters.GetEffectivePierceClearance());
|
||||
leadIn = ResolvePierceZone(shape, point, leadIn, normal, Parameters.GetPierceZoneRadius(), winding);
|
||||
if (scrapPierce is { } holePierce)
|
||||
leadIn = StraightLeadIn(point, holePierce, out normal);
|
||||
else
|
||||
{
|
||||
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal,
|
||||
Parameters.GetEffectivePierceClearance());
|
||||
leadIn = ResolvePierceZone(shape, point, leadIn, normal, Parameters.GetPierceZoneRadius(), winding);
|
||||
}
|
||||
|
||||
// Build hole sub-program relative to (0,0)
|
||||
var holeCenter = circle.Center;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Clipper2Lib;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.CNC.CuttingStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Pierces found by eroding a cutout: the part of it that keeps a clearance from its wall
|
||||
/// (see <see cref="ErodedPierce.Find"/>).
|
||||
/// </summary>
|
||||
/// <param name="Pierces">Distinct pierces, the one nearest each target in turn.</param>
|
||||
/// <param name="Clearance">How far the eroded region keeps every pierce from the wall.</param>
|
||||
public sealed record ErodedPierces(IReadOnlyList<Vector> Pierces, double Clearance)
|
||||
{
|
||||
public static ErodedPierces None { get; } = new(System.Array.Empty<Vector>(), 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The pierce search for cutouts too small for their configured lead-in (text, slots, small
|
||||
/// holes): erode the cutout inward, as a laser kerf offset would, and pierce in what is left.
|
||||
/// The erosion goes as deep as the pierce clearance when the cutout has room for it, else
|
||||
/// almost to its pole of inaccessibility, so a slot or letter stroke is pierced on its
|
||||
/// centre line, where the most clearance is. Each target, an entry the planner ranked, gets
|
||||
/// the eroded point nearest it; the lead then runs straight from there to the wall.
|
||||
/// </summary>
|
||||
public static class ErodedPierce
|
||||
{
|
||||
/// <summary>
|
||||
/// The share of a cutout's inscribed radius the erosion keeps when the full clearance does
|
||||
/// not fit: deep enough to stay near the centre line, with room for every place the
|
||||
/// cutout is about as wide as its widest.
|
||||
/// </summary>
|
||||
public const double DepthFraction = 0.9;
|
||||
|
||||
private const int PoleSearchCells = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// Up to <paramref name="max"/> distinct pierces in the closed cutout <paramref name="hole"/>,
|
||||
/// the eroded point nearest each of <paramref name="targets"/> in turn. A cutout too small to
|
||||
/// erode is pierced at its pole alone. Nothing for an open or degenerate contour.
|
||||
/// </summary>
|
||||
public static ErodedPierces Find(Shape hole, double clearance, IReadOnlyList<Vector> targets, int max)
|
||||
{
|
||||
if (hole == null || !hole.IsClosed() || targets == null || targets.Count == 0 || max <= 0
|
||||
|| !double.IsFinite(clearance))
|
||||
return ErodedPierces.None;
|
||||
|
||||
// The inscribed radius, from the true wall: the deepest any erosion can go.
|
||||
var coarse = ClipperBridge.Flatten(hole, Tolerance(hole.BoundingBox.Width, hole.BoundingBox.Length), false);
|
||||
if (coarse.Vertices.Count < 3)
|
||||
return ErodedPierces.None;
|
||||
var pole = PolyLabel.Find(coarse, null, Tolerance(coarse.BoundingBox.Width, coarse.BoundingBox.Length) / 10,
|
||||
PoleSearchCells);
|
||||
if (!double.IsFinite(pole.X) || !double.IsFinite(pole.Y) || !coarse.ContainsPoint(pole))
|
||||
return ErodedPierces.None;
|
||||
var inscribed = hole.ClosestPointTo(pole, out _).DistanceTo(pole);
|
||||
if (!(inscribed > 0))
|
||||
return ErodedPierces.None;
|
||||
|
||||
// Inscribed flattening can stand off a concave arc, and round joins cut their arcs,
|
||||
// each by the tolerance; Clipper rounds to its precision. Erode past both.
|
||||
var depth = System.Math.Min(System.Math.Max(clearance, 0), inscribed * DepthFraction);
|
||||
var tolerance = System.Math.Max(depth / 100, System.Math.Pow(10, -ClipperBridge.Precision));
|
||||
var margin = 2 * tolerance + System.Math.Pow(10, -ClipperBridge.Precision);
|
||||
if (depth + margin >= inscribed - tolerance)
|
||||
return new(new[] { pole }, inscribed);
|
||||
|
||||
var polygon = ClipperBridge.Flatten(hole, tolerance, circumscribe: false);
|
||||
var region = Clipper.InflatePaths(new PathsD { ClipperBridge.ToPath(polygon, positive: true) },
|
||||
-(depth + margin), JoinType.Round, EndType.Polygon, 2.0, ClipperBridge.Precision, tolerance)
|
||||
.Where(p => p.Count >= 3).ToList();
|
||||
if (region.Count == 0)
|
||||
return new(new[] { pole }, inscribed);
|
||||
|
||||
var pierces = new List<Vector>();
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var pierce = Nearest(region, target);
|
||||
if (pierces.All(p => p.DistanceTo(pierce) > tolerance))
|
||||
pierces.Add(pierce);
|
||||
if (pierces.Count == max)
|
||||
break;
|
||||
}
|
||||
return new(pierces, depth);
|
||||
}
|
||||
|
||||
// A hundredth of the smaller side: fine enough to find a thin cutout's pole.
|
||||
private static double Tolerance(double width, double length) =>
|
||||
System.Math.Max(System.Math.Min(width, length) / 100, System.Math.Pow(10, -ClipperBridge.Precision));
|
||||
|
||||
// The closest point of the region's boundary; targets lie on the wall, outside it.
|
||||
private static Vector Nearest(List<PathD> paths, Vector target)
|
||||
{
|
||||
var best = default(Vector);
|
||||
var bestDistance = double.PositiveInfinity;
|
||||
foreach (var path in paths)
|
||||
for (var i = 0; i < path.Count; i++)
|
||||
{
|
||||
var a = new Vector(path[i].x, path[i].y);
|
||||
var b = new Vector(path[(i + 1) % path.Count].x, path[(i + 1) % path.Count].y);
|
||||
var point = new Line(a, b).ClosestPointTo(target);
|
||||
var distance = point.DistanceTo(target);
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
best = point;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -687,8 +687,16 @@ internal static class JointCuttingPlanSearch
|
||||
// No outside to cut: one pass, complete once every hole is cut.
|
||||
perimeters = [null];
|
||||
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal)
|
||||
.Select(c => prepared.AutomaticEntryCandidatesWithFallbacks(c, token: token)
|
||||
.Where(e => Evaluate(source, e).Status == ContourFeasibilityStatus.Clear).Select(e => e.Choice).ToArray()).ToArray();
|
||||
.Select(c =>
|
||||
{
|
||||
var catalogue = prepared.AutomaticEntryCandidatesWithFallbacks(c, token: token);
|
||||
var clear = catalogue.Where(e => Evaluate(source, e).Status == ContourFeasibilityStatus.Clear).ToArray();
|
||||
// No configured lead-in fits the cutout: pierce where eroding it leaves room.
|
||||
if (clear.Length == 0)
|
||||
clear = PreferredContourEntries.ErodedPierces(prepared, c, catalogue, snapshot.MaxEntries, token)
|
||||
.Where(e => Evaluate(source, e).Status == ContourFeasibilityStatus.Clear).ToArray();
|
||||
return clear.Select(e => e.Choice).ToArray();
|
||||
}).ToArray();
|
||||
var motions = new Dictionary<ContourChoice, OwnedExecution>();
|
||||
var stop = Expansions + (long)StallExpansionsPerEntry * snapshot.MaxEntries * prepared.Count;
|
||||
// The part's scribes and other open cuts come once, before every contour; check
|
||||
@@ -983,6 +991,19 @@ internal static class JointCuttingPlanSearch
|
||||
selection = Steadiest(source.Prepared, ContourEntrySelection.Select(ordered,
|
||||
candidate => Evaluate(source, candidate), snapshot.MaxEntries, token));
|
||||
}
|
||||
// No configured lead-in fits a cutout: pierce where eroding it leaves room.
|
||||
string erodedReason = null;
|
||||
if (contour != source.Prepared.PerimeterOrdinal && selection.Choices.Count == 0
|
||||
&& selection.UncertainChoices.Count == 0 && selection.Shortfall == ContourSelectionShortfall.Exhausted)
|
||||
{
|
||||
var eroded = ContourEntrySelection.Select(
|
||||
PreferredContourEntries.ErodedPierces(source.Prepared, contour, ordered, snapshot.MaxEntries, token),
|
||||
candidate => Evaluate(source, candidate), snapshot.MaxEntries, token);
|
||||
if (eroded.Choices.Count != 0 || eroded.UncertainChoices.Count != 0)
|
||||
selection = eroded;
|
||||
else
|
||||
erodedReason = eroded.Reason ?? "eroding the cutout leaves no room to pierce.";
|
||||
}
|
||||
// No configured lead-in fits the perimeter: with a pierce diameter, try open scrap.
|
||||
string scrapReason = null;
|
||||
if (contour == source.Prepared.PerimeterOrdinal && selection.Choices.Count == 0
|
||||
@@ -1002,6 +1023,7 @@ internal static class JointCuttingPlanSearch
|
||||
+ "Try reducing the lead-in length in Cutting Settings...; if nearby parts obstruct the lead-in, "
|
||||
+ "space the parts farther apart." + PierceZoneHint()
|
||||
+ (scrapReason == null ? "" : $" A pierce in open scrap was also tried: {scrapReason}")
|
||||
+ (erodedReason == null ? "" : $" A pierce found by eroding the cutout was also tried: {erodedReason}")
|
||||
+ " Replan to check the changes."));
|
||||
// Uncertain candidates are NOT refused by the precheck: they reach the emitted-
|
||||
// prefix Check and complete replay, which remain the authority on them.
|
||||
|
||||
@@ -73,6 +73,12 @@ internal static class PreferredContourEntries
|
||||
var catalogue = prepared.AutomaticEntryCandidatesWithFallbacks(contour, target, token);
|
||||
var ranked = catalogue.RankTowardNextCut(target, arrivalProxy).DepartingClear(prepared, target, token);
|
||||
var selection = ContourEntrySelection.Select(ranked, evaluate, 1, token);
|
||||
if (selection.Choices.Count == 0 && selection.Shortfall == ContourSelectionShortfall.Exhausted)
|
||||
{
|
||||
var eroded = ContourEntrySelection.Select(ErodedPierces(prepared, contour, ranked, 1, token), evaluate, 1, token);
|
||||
if (eroded.Choices.Count != 0)
|
||||
selection = eroded;
|
||||
}
|
||||
if (selection.Choices.Count == 0)
|
||||
return new(Array.Empty<ContourChoice>(), selection.Shortfall, contour,
|
||||
$"Hole contour {contour} has no preferred lead-feasible entry: {selection.Reason}");
|
||||
@@ -85,6 +91,17 @@ internal static class PreferredContourEntries
|
||||
return new(choices, ContourSelectionShortfall.None, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The fallback for a cutout none of whose <paramref name="ranked"/> entries has a configured
|
||||
/// lead-in that fits: up to <paramref name="max"/> entries pierced where eroding the cutout
|
||||
/// leaves room, the one nearest each ranked entry in turn (see
|
||||
/// <see cref="PreparedContours.ErodedPierceEntries"/>). Proposals only: the lead checks decide.
|
||||
/// </summary>
|
||||
internal static IReadOnlyList<ContourEntryCandidate> ErodedPierces(PreparedContours prepared, int contour,
|
||||
IReadOnlyList<ContourEntryCandidate> ranked, int max, CancellationToken token) =>
|
||||
prepared.ErodedPierceEntries(contour, ranked.Select(c => c.Choice.Point).ToArray(), max, token)
|
||||
.Select(choice => new ContourEntryCandidate(choice, AutomaticEntryKind.ErodedPierce)).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Stable reordering of one hole's ranked entries: those whose straight rapid to
|
||||
/// <paramref name="target"/> (the next cut's pierce, local) clears the hole itself come
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class ErodedPierceTests
|
||||
{
|
||||
private static readonly Vector Start = new(-2, 1.5);
|
||||
|
||||
[Fact]
|
||||
public void Find_SlotIsPiercedOnItsCentreLineNearEachTarget()
|
||||
{
|
||||
var slot = Shape(Rectangle(2, 1, 2.4, 1.05));
|
||||
|
||||
var found = ErodedPierce.Find(slot, 0.0625, [new Vector(2.1, 1), new Vector(2.4, 1.02)], 4);
|
||||
|
||||
Assert.Equal(2, found.Pierces.Count);
|
||||
// 0.025 is all the slot allows; the erosion keeps nine tenths of it.
|
||||
Assert.Equal(0.025 * ErodedPierce.DepthFraction, found.Clearance, 6);
|
||||
Assert.All(found.Pierces, p => Assert.InRange(p.Y, 1.0225, 1.0275));
|
||||
Assert.Equal(2.1, found.Pierces[0].X, 2);
|
||||
Assert.InRange(found.Pierces[1].X, 2.37, 2.38);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Find_RoomForTheClearanceErodesOnlyThatDeep()
|
||||
{
|
||||
var hole = Shape(Rectangle(0, 0, 1, 1));
|
||||
|
||||
var found = ErodedPierce.Find(hole, 0.0625, [new Vector(0.5, 0)], 1);
|
||||
|
||||
Assert.Equal(0.0625, found.Clearance);
|
||||
Assert.Equal(0.5, Assert.Single(found.Pierces).X, 3);
|
||||
Assert.InRange(found.Pierces[0].Y, 0.0625, 0.066);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Planner_CutoutsTooSmallForTheLeadArePiercedByErosion()
|
||||
{
|
||||
var program = new Program();
|
||||
program.Merge(Rectangle(2, 1, 2.4, 1.05));
|
||||
program.Merge(Circle(5, 1.5, 0.03));
|
||||
program.Merge(Rectangle(0, 0, 10, 3));
|
||||
var part = new Part(new Drawing("tag", program));
|
||||
var parameters = new CuttingParameters
|
||||
{
|
||||
InternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
|
||||
InternalLeadOut = new LineLeadOut { Length = 0.1 },
|
||||
};
|
||||
|
||||
var (snapshot, result) = Plan(parameters, part);
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
var proposal = Assert.Single(result.ProposedOrder);
|
||||
var holes = proposal.ContourChoices.Where(c => c.ContourOrdinal != snapshot.Placements[0].Prepared.PerimeterOrdinal).ToArray();
|
||||
Assert.Equal(2, holes.Length);
|
||||
Assert.All(holes, c => Assert.NotNull(c.Pierce));
|
||||
// Each lead runs from the centre of its cutout to the nearest wall: no longer than the
|
||||
// cutout is deep, and no lead-out follows.
|
||||
var leads = proposal.Execution.Motions.Where(m => m.Layer == LayerType.Leadin).ToArray();
|
||||
Assert.Equal(2, leads.Count(m => m.Length < 0.031));
|
||||
Assert.DoesNotContain(proposal.Execution.Motions, m => m.Layer == LayerType.Leadout);
|
||||
var circlePierce = holes.Single(c => c.Pierce!.Value.X > 4).Pierce!.Value;
|
||||
Assert.True(circlePierce.DistanceTo(new Vector(5, 1.5)) < 0.004);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Planner_CutoutWithRoomForItsLeadKeepsIt()
|
||||
{
|
||||
var program = new Program();
|
||||
program.Merge(Rectangle(2, 1, 4, 2));
|
||||
program.Merge(Rectangle(0, 0, 10, 3));
|
||||
var part = new Part(new Drawing("plate", program));
|
||||
|
||||
var (_, result) = Plan(new CuttingParameters(), part);
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.All(Assert.Single(result.ProposedOrder).ContourChoices, c => Assert.Null(c.Pierce));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capture_CutoutJustBelowAnotherSeamVertexIsNotNestedInIt()
|
||||
{
|
||||
// Text baselines a micro-inch apart: the parity ray from the left cutout's start ran
|
||||
// exactly through the right cutout's seam vertex, whose two copies differ in the last
|
||||
// bit, and counted one of its edges, so the left cutout read as nested in it.
|
||||
var program = new Program();
|
||||
program.MoveTo(4.393012, 0.8892639999999999);
|
||||
program.LineTo(4.242387, 0.8892639999999999);
|
||||
program.LineTo(4.242387, 0.911497);
|
||||
program.LineTo(4.393012, 0.911497);
|
||||
program.LineTo(4.393012, 0.889264);
|
||||
program.MoveTo(1.834001, 0.8892629999999999);
|
||||
program.LineTo(1.834001, 0.95);
|
||||
program.LineTo(1.9, 0.95);
|
||||
program.LineTo(1.9, 0.8892629999999999);
|
||||
program.LineTo(1.834001, 0.8892629999999999);
|
||||
program.Merge(Rectangle(0, 0, 10, 2));
|
||||
|
||||
var material = LeadMaterialSnapshot.Capture(program, Vector.Zero);
|
||||
|
||||
Assert.True(material.IsComplete, material.Reason);
|
||||
}
|
||||
|
||||
private static (CuttingPlanSnapshot, CuttingPlanResult) Plan(CuttingParameters parameters, params Part[] parts)
|
||||
{
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(parts, Start,
|
||||
confirmedParameters: parameters, preservePartOrder: true));
|
||||
return (snapshot, CuttingPlanService.Plan(snapshot));
|
||||
}
|
||||
|
||||
private static Shape Shape(Program program) =>
|
||||
ShapeBuilder.GetShapes(OpenNest.Converters.ConvertProgram.ToGeometry(program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid).ToList()).Single();
|
||||
|
||||
private static Program Rectangle(double x1, double y1, double x2, double y2) =>
|
||||
LeadPathValidationTests.Rectangle(x1, y1, x2, y2);
|
||||
|
||||
private static Program Circle(double x, double y, double radius)
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(x + radius, y);
|
||||
p.ArcTo(x + radius, y, x, y, RotationType.CCW);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static string Describe(CuttingPlanResult result) =>
|
||||
$"{result.Status}: " + string.Join("; ", result.Findings.Select(f => f.Message).Take(5));
|
||||
}
|
||||
@@ -97,8 +97,12 @@ public class FixedLeadWarningTests
|
||||
{
|
||||
var plate = new Nest().CreatePlate();
|
||||
plate.Parts.Add(new Part(new Drawing("tight", TightHole()), new Vector(1, 1)));
|
||||
// The hole is eroded for a pierce when its lead-in does not fit; a pierce zone wider than
|
||||
// the hole leaves even that pierce no room.
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
parameters.PierceDiameter = 0.2;
|
||||
|
||||
var planned = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan().Plates[0];
|
||||
var planned = CuttingPlanBatch.Capture([plate], parameters, false).Plan().Plates[0];
|
||||
|
||||
Assert.False(planned.CanApplyWithWarnings);
|
||||
Assert.Empty(planned.Result.ProposedOrder);
|
||||
|
||||
@@ -70,16 +70,44 @@ corner's edges was picked; the arc then lies wholly on the scrap side of that ed
|
||||
corners and cutout corners keep the picked edge's normal: a tangent arc there would cross the
|
||||
part.
|
||||
|
||||
## Cutouts too small for their lead-in
|
||||
|
||||
Text, narrow slots and small holes often have no entry where the configured lead-in fits. When
|
||||
Plan Cutting has tried every entry of a cutout and none fits, it erodes the cutout instead
|
||||
(`ErodedPierce`, OpenNest.Core):
|
||||
|
||||
1. The cutout's pole of inaccessibility gives its inscribed radius, the deepest any erosion can go.
|
||||
2. The cutout is offset inward (`Clipper.InflatePaths`, negative) by the pierce clearance
|
||||
(`GetEffectivePierceClearance`) when it has room for it, else by nine tenths
|
||||
(`ErodedPierce.DepthFraction`) of its inscribed radius, plus a margin for flattening, round
|
||||
joins and Clipper rounding. What survives keeps a letter stroke's pierces on its centre line
|
||||
and finds the widest spot of a feature that tapers.
|
||||
3. For each of the cutout's ranked entries, the eroded point nearest it is a pierce. The entry
|
||||
becomes the cutout's native point closest to that pierce, and the lead-in runs straight from
|
||||
the pierce to it (`PreparedContours.ErodedPierceEntries`, a `ContourChoice` whose `Pierce` is
|
||||
set). A cutout so pierced has no lead-out: a feature too small for its lead-in has no room for
|
||||
one either.
|
||||
4. A cutout too small to erode is pierced at its pole alone.
|
||||
5. `LeadPathValidator` checks each candidate like any other planner-chosen entry, pierce zone
|
||||
included. When none is clear, the "No tested lead-in fits" finding says why.
|
||||
|
||||
Cutouts where a configured lead-in fits keep it, so plans of other parts are unchanged. Hole entry
|
||||
selection, the preferred hole path and contour recovery all use the fallback. Only Plan Cutting
|
||||
uses it: the emitter's own automatic assignment and the manual lead-in preview keep the
|
||||
configured lead.
|
||||
|
||||
## Regression checks
|
||||
|
||||
Run `dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter "FullyQualifiedName~CutoutCornerLeadInTests|FullyQualifiedName~PerimeterCornerLeadInTests"`.
|
||||
Run `dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter "FullyQualifiedName~CutoutCornerLeadInTests|FullyQualifiedName~PerimeterCornerLeadInTests|FullyQualifiedName~ErodedPierceTests"`.
|
||||
The cutout tests exercise generated part programs, default automatic placement, every
|
||||
rectangular corner with both adjoining edges and windings, part rotation, acute
|
||||
and obtuse angles, reflex corners, line/arc corners, preview agreement, and
|
||||
unchanged/fallback behavior. The perimeter tests cover every square corner under both
|
||||
windings and a rotation, auto and manual placement from either edge, preview
|
||||
agreement, approach-angle handling, the flat-corner clearance fallback, reflex
|
||||
notches, and straight lead-outs. `ArcLeadInTangentTests` checks curved-lead tangency, direction
|
||||
notches, and straight lead-outs. `ErodedPierceTests` covers the eroded pierces of a slot and a small round hole, a cutout that keeps
|
||||
its configured lead, and the containment of text cutouts that sit a micro-inch apart.
|
||||
`ArcLeadInTangentTests` checks curved-lead tangency, direction
|
||||
and sweep on perimeters and cutouts under both windings, and at every perimeter corner from
|
||||
either edge.
|
||||
|
||||
|
||||
Reference in new issue
Block a user