feat(cutting): slice slugs the support grid can't hold level

On an enclosed machine a cutout slug that tips once cut free can stand up
above the sheet and catch the enclosure door when the pallet changes. The
head already keeps off cut cutouts, so this is about clearing the door, not
the head.

Cutting Settings > Cutouts > "Slice slugs the support grid can't hold
level" (CuttingParameters.SliceTippingSlugs, off by default, saved per
machine thickness and written only when on) makes Plan Cutting cut each
such slug along the bed's contact lines (grid rows and columns, slat center
lines) before the cutout, so no piece keeps a contact and the pieces drop
between the supports. Each slice stops a kerf short of the cutout, so the
pieces stay attached until the cutout's own kerf frees them together. A
slug the bed holds level, or one on no contact, is left whole; a cutout
holding another part or another of its part's cuts is never sliced.

Slices are break cuts emitted right after the drawing's own break cuts,
pierced on themselves and led straight back to their start. The
released-contour proof now leaves out straight break cuts lying wholly
inside one of the part's closed holes, since the grid cells they free are
scrap; break cuts in material still take part. The plan fingerprint only
changes when the setting is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-12 17:04:27 -05:00
1 parent b9fc618a50
commit e6d438629f
23 files changed
+801 -8

No files matched your search

+1 -1
View File
@@ -73,7 +73,7 @@ Read the relevant contract before changing its behavior:
- [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
- [Dimensions](docs/dimensions.md): CAD Converter annotations that store placement only, are measured from the cut geometry when shown and never affect the program
- [Common-line cutting](docs/common-line-cutting.md): parts one kerf apart in `Plate.CommonLines` share an edge; spans are derived, never stored, and spacing checks hold group members to the kerf. Plan Cutting cuts each shared span once (the owner, cut first, starts its compensated outside on the common line; partners cut only their remainders) unless the plate is set to cut them twice; only posts implementing `ICommonLinePostProcessor` (CI Fiber) may write it, others refuse. Machine trial pending. Auto Nest and Fill Area pair `AllowCommonLine` drawings at the nest's `CommonLineUsage` (Off by default, byte-identical to no common lines); `NestLayoutCheck` exempts only explicit result groups
- [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
- [Support tabs](docs/support-tabs.md): the nest's support grid, the center-of-gravity check, Plan Cutting's tab placement for parts the grid cannot hold level, and its opt-in slicing of slugs the grid cannot hold level
- [Material-overlap diagnostics](docs/geometry/visual-overlap-check.md)
- [Automatic cutoffs and sequencing](docs/automatic-scrap-cutoffs.md)
- [Pre-post verification](docs/post-verification.md)
@@ -46,6 +46,7 @@ internal static class OwnedCuttingParameters
TabSizeMinWidth = Dimension(source.TabSizeMinWidth),
TabSizeMaxWidth = Dimension(source.TabSizeMaxWidth),
TabHeldMaxLength = Dimension(source.TabHeldMaxLength),
SliceTippingSlugs = source.SliceTippingSlugs,
Sequencing = new SequenceParameters
{
Method = sequence.Method,
@@ -57,6 +57,9 @@ public sealed class PreparedContours
// Closed cutouts with tab spans, in program order. Like break cuts they are not choices:
// every full or prefix emission cuts their pieces after the break cuts.
private readonly List<Shape> tabbedCutouts;
// Slices of the slugs the support bed cannot hold level, in cutting order: open cuts inside
// their cutouts, cut right after the break cuts so every slug is sliced before its cutout.
private readonly IReadOnlyList<SlugSlice> slugSlices;
// Pierce-zone resolutions by contour, shared with every copy: the contours are the same by ordinal.
private readonly PierceZoneMemo pierceZones;
// By contour ordinal; all null without pins. A pinned contour has exactly one entry and
@@ -86,8 +89,9 @@ public sealed class PreparedContours
private PreparedContours(Shape[] shapes, List<Entity> scribes, List<Shape> breakCuts, List<Shape> tabbedCutouts,
CuttingParameters parameters, Shape displayOutline, PierceZoneMemo pierceZones = null,
CommonLinePerimeter commonLine = null)
CommonLinePerimeter commonLine = null, IReadOnlyList<SlugSlice> slugSlices = null)
{
this.slugSlices = slugSlices ?? Array.Empty<SlugSlice>();
this.pierceZones = pierceZones ?? new PierceZoneMemo();
this.displayOutline = displayOutline;
this.commonLine = commonLine;
@@ -122,7 +126,7 @@ public sealed class PreparedContours
private PreparedContours(PreparedContours unpinned, IReadOnlyList<CutPinResolution> pins, CancellationToken token)
: this(unpinned.shapes, unpinned.scribes, unpinned.breakCuts, unpinned.tabbedCutouts, unpinned.parameters,
unpinned.displayOutline, unpinned.pierceZones, unpinned.commonLine)
unpinned.displayOutline, unpinned.pierceZones, unpinned.commonLine, unpinned.slugSlices)
{
contourParameters = new CuttingParameters[shapes.Length];
var rejected = new List<CutPinResolution>();
@@ -161,7 +165,7 @@ public sealed class PreparedContours
// 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,
source.displayOutline, source.pierceZones, source.commonLine)
source.displayOutline, source.pierceZones, source.commonLine, source.slugSlices)
{
for (var contour = 0; contour < Count; contour++)
{
@@ -205,6 +209,79 @@ public sealed class PreparedContours
return new PreparedContours(this, support, location, tab);
}
// A copy that slices slugs; everything else, pins and support analysis included, is kept.
private PreparedContours(PreparedContours source, IReadOnlyList<SlugSlice> slices)
: this(source.shapes, source.scribes, source.breakCuts, source.tabbedCutouts, source.parameters,
source.displayOutline, source.pierceZones, source.commonLine, slices)
{
for (var contour = 0; contour < Count; contour++)
{
pinned[contour] = source.pinned[contour] is { } pin ? pin with { Owner = this } : null;
pinNumbers[contour] = source.pinNumbers[contour];
}
contourParameters = (CuttingParameters[])source.contourParameters?.Clone();
RejectedPins = source.RejectedPins;
support = source.support;
supportOrigin = source.supportOrigin;
}
/// <summary>
/// A copy that slices the slug of each cutout <paramref name="bed"/> cannot hold level
/// (<see cref="SlugSlicer.Tips"/>), with the part placed at <paramref name="location"/> on a
/// sheet in <paramref name="quadrant"/>, along the bed's contact lines, each slice stopping the
/// kerf short of its cutout (docs/support-tabs.md#slug-slices). Every full or prefix emission
/// cuts the slices right after the break cuts, so each slug is sliced before its cutout.
/// A cutout that holds another of this part's cuts, or whose box holds one of
/// <paramref name="otherParts"/> (world boxes of the plate's other parts), is not sliced:
/// a slice would cut what is nested in it. Without a bed or a kerf, or with nothing to slice,
/// this preparation is returned unchanged.
/// </summary>
internal PreparedContours WithSlugSlices(SupportBed bed, int quadrant, Vector location,
IReadOnlyList<Box> otherParts, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(otherParts);
PostVerificationGeometry.Validate(location);
var standoff = parameters.Kerf;
if (bed == null || !(standoff > 0) || !double.IsFinite(standoff))
return this;
// Pierced on the slice where the cutouts' own lead-in would pierce, and at least as far
// from the cutout as any pierce keeps from its contour.
var clearance = parameters.GetEffectivePierceClearance();
var reach = System.Math.Max(clearance,
ContourCuttingStrategy.CommonLineLeadInReach(parameters.InternalLeadIn ?? new NoLeadIn()));
var slices = new List<SlugSlice>();
for (var ordinal = 0; ordinal < PerimeterOrdinal; ordinal++)
{
token.ThrowIfCancellationRequested();
var hole = shapes[ordinal];
var box = hole.BoundingBox;
var world = new Box(box.Left + location.X, box.Bottom + location.Y, box.Length, box.Width);
if (otherParts.Any(other => other != null && world.Contains(other)))
continue;
var polygon = hole.ToPolygonWithTolerance(SupportMaterial.ChordTolerance);
if (EnclosesOtherCuts(ordinal, polygon)
|| !SlugSlicer.Tips(bed, quadrant, SupportMaterial.FromPolygons(polygon.Vertices.Select(v => v + location))))
continue;
slices.AddRange(SlugSlicer.Slices(bed, quadrant, polygon.Vertices, location, standoff, reach, clearance));
}
return slices.Count == 0 ? this : new PreparedContours(this, slices.AsReadOnly());
}
// Whether another contour, break cut or tabbed cutout of this part lies inside the cutout.
private bool EnclosesOtherCuts(int ordinal, Polygon cutout)
{
var others = shapes.Where((_, index) => index != ordinal && index != PerimeterOrdinal)
.Concat(breakCuts).Concat(tabbedCutouts);
return others.Any(other => other.Entities.Count > 0 && cutout.ContainsPoint(other.Entities[0] switch
{
Circle circle => circle.Center + new Vector(circle.Radius, 0),
var entity => Start(entity),
}));
}
/// <summary>The slug slices this preparation cuts after its break cuts and before its contours.</summary>
public IReadOnlyList<SlugSlice> SlugSlices => slugSlices;
/// <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.
@@ -1028,7 +1105,7 @@ public sealed class PreparedContours
shapes.Select(s => (Shape)s.Clone()).ToArray(), scribes.Select(e => e.Clone()).ToList(), choices,
contourParameters, breakCuts.Select(s => (Shape)s.Clone()).ToList(),
tabbedCutouts.Select(s => (Shape)s.Clone()).ToList(), PerimeterOrdinal,
complete ? (Shape)displayOutline?.Clone() : null, commonLine);
complete ? (Shape)displayOutline?.Clone() : null, commonLine, slugSlices);
/// <summary>Open break cuts this preparation cuts before its contours.</summary>
public int BreakCutCount => breakCuts.Count;
@@ -316,9 +316,13 @@ public sealed class ReleasedContourState
// A common-line member's open cuts are its remainder pieces: they end in shared kerf, not at
// tabs, and the planner's contour accounting proves they cut its outside exactly once.
var remainders = member && !cutoff && unfinished.Count > 0;
// Slug slices (docs/support-tabs.md#slug-slices) cross one another inside one of the part's
// own holes: what they enclose is that hole's slug, scrap, so they take no part in the proof.
var retained = !cutoff && (unfinished.Count > 1 || deferred.Count > 0)
&& (remainders || unfinished.All(LeadsClearOpenEnds)
&& OpenCutsEncloseNothing(closed, unfinished.Concat(otherOpen).Append(loose).ToList(), token));
&& OpenCutsEncloseNothing(closed, unfinished
.Concat(otherOpen.Where(group => !CutsOnlySlug(group, closed, unfinished, token)))
.Append(loose).ToList(), token));
for (var i = deferred.Count - 1; i >= 0 && !retained; i--)
deferred[i].Target.Insert(deferred[i].Index, deferred[i].Finding);
// Only one open fragment per part can be the planner's own certified tab, so a
@@ -461,6 +465,35 @@ public sealed class ReleasedContourState
|| lead.End.DistanceTo(point) <= PostVerificationGeometry.Epsilon;
}
/// <summary>
/// Whether break cut <paramref name="group"/>, its leads included, lies wholly inside one of
/// the part's closed holes and touches none of it: whatever it frees is that hole's slug. Only
/// straight cuts qualify. The part's outside is its largest cut, never a hole.
/// </summary>
private static bool CutsOnlySlug(CutGroup group, IReadOnlyList<CutGroup> closed, IReadOnlyList<CutGroup> unfinished,
CancellationToken token)
{
var cuts = group.Curves.Concat(group.Leads).ToList();
if (cuts.Count == 0 || !cuts.TrueForAll(cut => cut.IsLine))
return false;
var outside = closed.Concat(unfinished).Where(cut => cut.Curves.Count > 0).MaxBy(cut =>
{
var extent = cut.Curves.Aggregate(PostVerificationGeometry.Extent.None, (sum, curve) => sum.Union(curve.Extent));
return (extent.MaxX - extent.MinX) * (extent.MaxY - extent.MinY);
});
foreach (var hole in closed)
{
token.ThrowIfCancellationRequested();
if (ReferenceEquals(hole, outside) || !PostVerificationGeometry.Closed(hole.Curves))
continue;
if (cuts.TrueForAll(cut => PostVerificationGeometry.Inside(cut.Start, hole.Curves, token)
&& PostVerificationGeometry.Inside(cut.End, hole.Curves, token)
&& PostVerificationGeometry.ContactPoints(cut.Start, cut.End, hole.Curves, token).Count == 0))
return true;
}
return false;
}
private static bool OpenCutsEncloseNothing(IReadOnlyList<CutGroup> closed, IReadOnlyList<CutGroup> open,
CancellationToken token)
{
@@ -94,6 +94,9 @@ internal static class StateFingerprint
Number(p.TabSizeMinWidth);
Number(p.TabSizeMaxWidth);
Number(p.TabHeldMaxLength);
// Only when on, so fingerprints taken before the setting existed still match.
if (p.SliceTippingSlugs)
text.Append("slugSlices;");
Sequence(p.Sequencing);
Assignment(p.Assignment);
text.Append('}');
@@ -268,11 +268,12 @@ namespace OpenNest.CNC.CuttingStrategy
IReadOnlyList<CuttingPlanning.ContourChoice> choices,
IReadOnlyList<CuttingParameters> contourParameters = null, IReadOnlyList<Shape> breakCuts = null,
IReadOnlyList<Shape> tabbedCutouts = null, int? perimeterOrdinal = null, Shape displayTail = null,
CuttingPlanning.CommonLinePerimeter commonLine = null)
CuttingPlanning.CommonLinePerimeter commonLine = null, IReadOnlyList<Support.SlugSlice> slugSlices = null)
{
var result = new Program(Mode.Absolute);
EmitScribeContours(result, scribes);
EmitBreakCuts(result, breakCuts, shapes);
EmitSlugSlices(result, slugSlices);
foreach (var cutout in tabbedCutouts ?? Array.Empty<Shape>())
EmitTabbedCutout(result, cutout);
EmitChosenContours(result, shapes, choices, contourParameters, perimeterOrdinal ?? shapes.Length - 1,
@@ -1583,6 +1584,22 @@ namespace OpenNest.CNC.CuttingStrategy
}
}
// Each slug slice is a break cut inside its cutout's slug: pierced on itself, led straight
// back along itself to its start, then cut. It has no lead-out and keeps the kerf from
// the cutout, so its slug's pieces stay attached until the cutout is cut.
private static void EmitSlugSlices(Program program, IReadOnlyList<Support.SlugSlice> slices)
{
if (slices == null)
return;
foreach (var slice in slices)
{
program.Codes.Add(new RapidMove(slice.Pierce));
if (slice.Pierce.DistanceTo(slice.Start) > Tolerance.Epsilon)
program.Codes.Add(new LinearMove(slice.Start) { Layer = LayerType.Leadin });
program.Codes.Add(new LinearMove(slice.End) { Layer = LayerType.Break });
}
}
/// <summary>
/// Where a break cut (traveling with its scrap on the left) is entered: the end with more
/// scrap before <paramref name="contours"/>, the unit direction into the scrap there, and
@@ -105,6 +105,14 @@ namespace OpenNest.CNC.CuttingStrategy
TabHeldMaxLength > 0 && double.IsFinite(TabHeldMaxLength)
&& length <= TabHeldMaxLength + Math.Tolerance.Epsilon;
/// <summary>
/// Slices the slug of each cutout its plate's support bed cannot hold level along the
/// bed's contact lines before the cutout is cut, so the slug drops as pieces between the
/// supports instead of tipping (see docs/support-tabs.md#slug-slices). Each slice stops
/// <see cref="Kerf"/> short of the cutout, so nothing is sliced without a kerf.
/// </summary>
public bool SliceTippingSlugs { get; set; }
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
}
+209
View File
@@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Clipper2Lib;
using OpenNest.Geometry;
namespace OpenNest.Support
{
/// <summary>
/// One straight slice of a cutout's slug, in the part's local coordinates: pierced at
/// <see cref="Pierce"/> on the slice, led back along it to <see cref="Start"/>, then cut to
/// <see cref="End"/>. <see cref="Pierce"/> equals <see cref="Start"/> when there is no lead.
/// </summary>
public sealed record SlugSlice(Vector Pierce, Vector Start, Vector End);
/// <summary>
/// Slices for the slug of a cutout its support bed cannot hold level (see
/// docs/support-tabs.md#slug-slices). The slug is cut along every line the bed's contacts lie
/// on (<see cref="SupportBed.ContactLines"/>), so each piece keeps no contact and drops between
/// the supports once the cutout is cut, instead of tipping. Each slice stops a standoff (the
/// kerf) short of the cutout, so the pieces stay attached until the cutout itself is cut and
/// no slice reaches the part.
/// </summary>
public static class SlugSlicer
{
private const double RoundingAllowance = 2e-4;
/// <summary>
/// Whether a cutout's slug, <paramref name="slug"/> (world coordinates), would tip once cut
/// free: it rests on at least one contact of <paramref name="bed"/> but the bed cannot hold
/// it level. A slug on no contact falls through whole, so it is not sliced.
/// </summary>
public static bool Tips(SupportBed bed, int quadrant, SupportMaterial slug)
{
ArgumentNullException.ThrowIfNull(bed);
ArgumentNullException.ThrowIfNull(slug);
var support = SupportAnalyzer.Analyze(bed, quadrant, slug);
return support.IsComplete && !support.IsSupported && support.Contacts.Count > 0;
}
/// <summary>
/// The slices of the slug inside <paramref name="hole"/> (a closed polygon in the part's
/// local coordinates; world = local + <paramref name="location"/>), along the contact lines
/// of <paramref name="bed"/> on a sheet in <paramref name="quadrant"/>. Each slice keeps at
/// least <paramref name="standoff"/> from the hole, is longer than it, and is pierced
/// <paramref name="reach"/> along itself from its start (at most halfway) where the pierce
/// keeps <paramref name="pierceClearance"/> from the hole; a slice whose pierce fits at
/// neither end is left out. Rows (lines along X) come first, then columns, each set from
/// its end nearer the last slice, and each slice starts at its end nearer where the one
/// before it ended, so the head runs back and forth.
/// </summary>
public static IReadOnlyList<SlugSlice> Slices(SupportBed bed, int quadrant, IReadOnlyList<Vector> hole,
Vector location, double standoff, double reach, double pierceClearance)
{
ArgumentNullException.ThrowIfNull(bed);
ArgumentNullException.ThrowIfNull(hole);
if (!(standoff > 0) || !double.IsFinite(standoff))
throw new ArgumentOutOfRangeException(nameof(standoff), "The standoff must be finite and positive.");
if (!(reach >= 0) || !double.IsFinite(reach))
throw new ArgumentOutOfRangeException(nameof(reach));
if (!(pierceClearance >= 0) || !double.IsFinite(pierceClearance))
throw new ArgumentOutOfRangeException(nameof(pierceClearance));
var ring = Ring(hole);
if (ring.Count < 3)
return Array.Empty<SlugSlice>();
// Clipper rounds to 1e-4 (ClipperBridge.Precision): erode a little further so no slice
// end comes inside the standoff.
var eroded = Clipper.InflatePaths(new PathsD { ToPath(ring) }, -(standoff + RoundingAllowance),
JoinType.Round, EndType.Polygon, 2.0, ClipperBridge.Precision);
if (eroded.Count == 0)
return Array.Empty<SlugSlice>();
var bounds = Bounds(ring);
var world = new Box(bounds.Left + location.X, bounds.Bottom + location.Y, bounds.Length, bounds.Width);
var lines = bed.ContactLines(world, quadrant);
// Each line's pieces inside the eroded hole, local; lines that miss it are dropped.
var groups = new[] { true, false }.Select(alongX => lines
.Where(line => line.AlongX == alongX)
.OrderBy(line => line.At)
.Select(line => Segments(eroded, line, location, standoff))
.Where(segments => segments.Count > 0)
.ToList())
.ToArray();
var slices = new List<SlugSlice>();
var clearance = System.Math.Max(pierceClearance, standoff);
Vector? position = null;
foreach (var group in groups)
{
if (group.Count == 0)
continue;
// Rows, then columns, each set from the end nearer the last slice, back and forth:
// every slice starts at its end nearer where the one before it ended.
if (position is { } from && Nearest(group[^1], from) < Nearest(group[0], from))
group.Reverse();
foreach (var segments in group)
{
if (position is { } here && Nearest([segments[^1]], here) < Nearest([segments[0]], here))
segments.Reverse();
foreach (var (a, b) in segments)
{
var (start, end) = position is { } at && b.DistanceTo(at) < a.DistanceTo(at) ? (b, a) : (a, b);
var slice = Pierced(start, end, reach, ring, clearance)
?? Pierced(end, start, reach, ring, clearance);
if (slice == null)
continue;
slices.Add(slice);
position = slice.End;
}
}
}
return slices;
}
// The pieces of one contact line inside the eroded hole, local, low to high, each longer
// than the standoff.
private static List<(Vector A, Vector B)> Segments(PathsD eroded, ContactLine line, Vector location,
double standoff)
{
var at = line.At - (line.AlongX ? location.Y : location.X);
return Spans(eroded, line.AlongX, at)
.Where(span => span.High - span.Low > standoff)
.Select(span => line.AlongX
? (new Vector(span.Low, at), new Vector(span.High, at))
: (new Vector(at, span.Low), new Vector(at, span.High)))
.ToList();
}
private static double Nearest(List<(Vector A, Vector B)> segments, Vector point) =>
segments.Min(segment => System.Math.Min(segment.A.DistanceTo(point), segment.B.DistanceTo(point)));
// A slice from start to end pierced on itself, or null when the pierce is too close to the hole.
private static SlugSlice Pierced(Vector start, Vector end, double reach, IReadOnlyList<Vector> ring,
double clearance)
{
var length = start.DistanceTo(end);
var pierce = start + (end - start) * (System.Math.Min(reach, length / 2) / length);
return Distance(pierce, ring) >= clearance - Math.Tolerance.Epsilon
? new SlugSlice(pierce, start, end)
: null;
}
// Where the line at `at` (Y along X, or X along Y) runs inside the eroded paths, sorted.
private static List<(double Low, double High)> Spans(PathsD paths, bool alongX, double at)
{
var spans = new List<(double, double)>();
foreach (var path in paths)
{
var crossings = new List<double>();
for (var i = 0; i < path.Count; i++)
{
var p = path[i];
var q = path[(i + 1) % path.Count];
var (pa, pb, qa, qb) = alongX ? (p.y, p.x, q.y, q.x) : (p.x, p.y, q.x, q.y);
if (pa > at == qa > at)
continue;
crossings.Add(pb + (at - pa) * (qb - pb) / (qa - pa));
}
crossings.Sort();
for (var i = 0; i + 1 < crossings.Count; i += 2)
spans.Add((crossings[i], crossings[i + 1]));
}
spans.Sort((x, y) => x.Item1.CompareTo(y.Item1));
return spans;
}
private static double Distance(Vector point, IReadOnlyList<Vector> ring)
{
var best = double.PositiveInfinity;
for (var i = 0; i < ring.Count; i++)
{
var a = ring[i];
var b = ring[(i + 1) % ring.Count];
var edge = b - a;
var squared = edge.X * edge.X + edge.Y * edge.Y;
var t = squared > 0
? System.Math.Clamp(((point.X - a.X) * edge.X + (point.Y - a.Y) * edge.Y) / squared, 0, 1)
: 0;
best = System.Math.Min(best, point.DistanceTo(a + edge * t));
}
return best;
}
// The polygon without a repeated closing vertex.
private static List<Vector> Ring(IReadOnlyList<Vector> polygon)
{
var ring = polygon.ToList();
if (ring.Count > 1 && ring[0].DistanceTo(ring[^1]) <= Math.Tolerance.Epsilon)
ring.RemoveAt(ring.Count - 1);
return ring;
}
private static PathD ToPath(IReadOnlyList<Vector> ring)
{
var path = new PathD(ring.Count);
foreach (var vertex in ring)
path.Add(new PointD(vertex.X, vertex.Y));
return path;
}
private static Box Bounds(IReadOnlyList<Vector> ring)
{
var left = ring.Min(v => v.X);
var bottom = ring.Min(v => v.Y);
return new Box(left, bottom, ring.Max(v => v.X) - left, ring.Max(v => v.Y) - bottom);
}
}
}
+14
View File
@@ -12,6 +12,12 @@ namespace OpenNest.Support
/// The laser grid (<see cref="SupportGrid"/>) is the built-in layout; straight or curved
/// slats imported from a CAD layout can follow as further subclasses.
/// </summary>
/// <summary>
/// A line a support bed's contacts lie on: along X at Y = <see cref="At"/> when
/// <see cref="AlongX"/>, else along Y at X = <see cref="At"/> (world coordinates).
/// </summary>
public readonly record struct ContactLine(bool AlongX, double At);
public abstract class SupportBed : IEquatable<SupportBed>
{
protected SupportBed(double registrationTolerance, double stabilityMargin)
@@ -47,6 +53,14 @@ namespace OpenNest.Support
public abstract IReadOnlyList<Vector> Contacts(SupportMaterial material, int quadrant, Vector shift,
bool extremesOnly = false);
/// <summary>
/// The straight lines every contact lies on across <paramref name="region"/> (world
/// coordinates) on a sheet in <paramref name="quadrant"/>: a grid's rows and columns, a
/// slat's center line. Material cut along all of them keeps no contact, so it drops
/// between the supports (see <see cref="SlugSlicer"/>).
/// </summary>
public abstract IReadOnlyList<ContactLine> ContactLines(Box region, int quadrant);
/// <summary>This layout with every length multiplied by <paramref name="factor"/> (a unit change).</summary>
public abstract SupportBed Scaled(double factor);
+16
View File
@@ -91,6 +91,22 @@ namespace OpenNest.Support
return points;
}
/// <summary>Every row and column of contacts that crosses <paramref name="region"/>.</summary>
public override IReadOnlyList<ContactLine> ContactLines(Box region, int quadrant)
{
ArgumentNullException.ThrowIfNull(region);
var lines = new List<ContactLine>();
var baseX = SignX(quadrant) * OffsetX;
var baseY = SignY(quadrant) * OffsetY;
for (var row = System.Math.Ceiling((region.Bottom - baseY) / PitchY);
row <= System.Math.Floor((region.Top - baseY) / PitchY); row++)
lines.Add(new ContactLine(true, baseY + row * PitchY));
for (var column = System.Math.Ceiling((region.Left - baseX) / PitchX);
column <= System.Math.Floor((region.Right - baseX) / PitchX); column++)
lines.Add(new ContactLine(false, baseX + column * PitchX));
return lines;
}
public override SupportBed Scaled(double factor) =>
new SupportGrid(PitchX * factor, PitchY * factor, OffsetX * factor, OffsetY * factor,
RegistrationTolerance * factor, StabilityMargin * factor);
+18
View File
@@ -112,6 +112,24 @@ namespace OpenNest.Support
return slats;
}
/// <summary>
/// The center line of every slat that crosses <paramref name="region"/>. Material between
/// two neighboring center lines touches at most those slats' edges.
/// </summary>
public override IReadOnlyList<ContactLine> ContactLines(Box region, int quadrant)
{
ArgumentNullException.ThrowIfNull(region);
var lines = new List<ContactLine>();
var alongX = Axis == SlatAxis.X;
var baseline = alongX ? SignY(quadrant) * Offset : SignX(quadrant) * Offset;
var low = alongX ? region.Bottom : region.Left;
var high = alongX ? region.Top : region.Right;
for (var slat = System.Math.Ceiling((low - baseline) / Pitch);
slat <= System.Math.Floor((high - baseline) / Pitch); slat++)
lines.Add(new ContactLine(alongX, baseline + slat * Pitch));
return lines;
}
public override SupportBed Scaled(double factor) =>
new SupportSlats(Axis, Pitch * factor, Thickness * factor, Offset * factor,
RegistrationTolerance * factor, StabilityMargin * factor);
+2
View File
@@ -130,6 +130,7 @@ public static class MachineCuttingDefaults
TabHeldMaxLength = parameters.TabHeldMaxLength * scale,
TabSizeMinWidth = parameters.TabSizeMinWidth * scale,
TabSizeMaxWidth = parameters.TabSizeMaxWidth * scale,
SliceTippingSlugs = parameters.SliceTippingSlugs,
PierceClearance = parameters.PierceClearance * scale,
PierceDiameter = parameters.PierceDiameter * scale,
Unknown = previous?.Unknown,
@@ -147,6 +148,7 @@ public static class MachineCuttingDefaults
parameters.TabHeldMaxLength = cutting.TabHeldMaxLength * scale;
parameters.TabSizeMinWidth = cutting.TabSizeMinWidth * scale;
parameters.TabSizeMaxWidth = cutting.TabSizeMaxWidth * scale;
parameters.SliceTippingSlugs = cutting.SliceTippingSlugs;
parameters.PierceClearance = cutting.PierceClearance * scale;
parameters.PierceDiameter = cutting.PierceDiameter * scale;
}
+4
View File
@@ -29,6 +29,10 @@ public class ThicknessCuttingConfig
/// <summary>The widest part tabbed by size; 0 tabs nothing by size.</summary>
public double TabSizeMaxWidth { get; set; }
/// <summary>Whether cutouts the support grid can't hold level have their slugs sliced; written only when on.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool SliceTippingSlugs { get; set; }
public double PierceClearance { get; set; } = 0.0625;
/// <summary>The pierce zone's diameter; 0 turns it off.</summary>
@@ -65,6 +65,8 @@ public static class CuttingPlanService
var nodes = new List<DependencyNode>();
// Reuse the reader's coordinate validation without publishing its native kernel.
var identities = new HashSet<Part>(ReferenceEqualityComparer.Instance);
// Every part's box, read once, for slug slices to keep clear of parts nested in a cutout.
Box[] partBoxes = null;
for (var index = 0; index < request.Parts.Count; index++)
{
token.ThrowIfCancellationRequested();
@@ -170,6 +172,11 @@ public static class CuttingPlanService
prepared = DrawingPinCapture.Prepare(source, index, ownedClean, request.ConfirmedParameters,
warnings, token);
prepared = SupportTabCapture.Prepare(prepared, ownedClean, location, request, token);
if (request.ConfirmedParameters.SliceTippingSlugs)
{
partBoxes ??= request.Parts.Select(part => part?.BoundingBox).ToArray();
prepared = SlugSliceCapture.Prepare(prepared, location, request, partBoxes, token);
}
}
else if (source.HasActiveDrawingPins)
warnings.Add(DrawingPinCapture.NotApplied(source, index));
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Threading;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Geometry;
namespace OpenNest.Engine.CuttingPlanning;
/// <summary>
/// Capture-time slug slices for one regenerated part (docs/support-tabs.md#slug-slices): with
/// <see cref="CNC.CuttingStrategy.CuttingParameters.SliceTippingSlugs"/> on and a support bed,
/// the slug of each cutout the bed cannot hold level is sliced along the bed's contact lines
/// before the cutout is cut (<see cref="PreparedContours.WithSlugSlices"/>).
/// </summary>
internal static class SlugSliceCapture
{
internal static PreparedContours Prepare(PreparedContours prepared, Vector location, CuttingPlanRequest request,
IReadOnlyList<Box> partBoxes, CancellationToken token)
{
if (prepared == null || request.ConfirmedParameters?.SliceTippingSlugs != true || request.SupportBed == null)
return prepared;
token.ThrowIfCancellationRequested();
// A cutout's box can never hold its own part's box, so every part's box may be passed.
return prepared.WithSlugSlices(request.SupportBed, request.SupportQuadrant, location, partBoxes, token);
}
}
+3
View File
@@ -38,6 +38,9 @@ public class CuttingParametersDto
public double NoBedTabMinWidth { get; set; }
public double NoBedTabMaxWidth { get; set; }
public double TabHeldMaxLength { get; set; }
// Written only when on, so files and settings saved without it are unchanged.
[System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)]
public bool SliceTippingSlugs { get; set; }
public SequenceDto Sequencing { get; set; } = new();
public AssignmentDto Assignment { get; set; } = new();
@@ -75,6 +75,7 @@ public static class CuttingParametersSerializer
NoBedTabMinWidth = parameters.TabSizeMinWidth,
NoBedTabMaxWidth = parameters.TabSizeMaxWidth,
TabHeldMaxLength = parameters.TabHeldMaxLength,
SliceTippingSlugs = parameters.SliceTippingSlugs,
Sequencing = ToSequenceDto(parameters.Sequencing),
Assignment = ToAssignmentDto(parameters.Assignment),
};
@@ -112,6 +113,7 @@ public static class CuttingParametersSerializer
TabSizeMinWidth = dto.NoBedTabMinWidth,
TabSizeMaxWidth = dto.NoBedTabMaxWidth,
TabHeldMaxLength = dto.TabHeldMaxLength,
SliceTippingSlugs = dto.SliceTippingSlugs,
Sequencing = FromSequenceDto(dto.Sequencing),
Assignment = FromAssignmentDto(dto.Assignment),
};
+285
View File
@@ -0,0 +1,285 @@
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
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>
/// Slug slices (docs/support-tabs.md#slug-slices): the slug of a cutout its support bed cannot
/// hold level is cut along the bed's contact lines, a kerf short of the cutout, before the cutout.
/// </summary>
public class SlugSliceTests
{
private const double Kerf = 0.06;
// A 2 inch laser grid with no registration slack, so contacts are exactly on its lines.
private static readonly SupportGrid Bed = new(2, 2);
// An 8 x 1 slot at world (3, 3.5)-(11, 4.5): its slug rests on the grid's row at Y = 4 alone.
private static readonly Vector[] Slot =
[new(2, 2.5), new(10, 2.5), new(10, 3.5), new(2, 3.5)];
private static readonly Vector Location = new(1, 1);
private static CuttingParameters Parameters(bool slice = true, double kerf = Kerf)
{
var parameters = ExplicitContourTests.Parameters();
parameters.Kerf = kerf;
parameters.SliceTippingSlugs = slice;
return parameters;
}
// A 12 inch square plate part with one rectangular hole, both local.
private static Drawing Frame(double x1 = 2, double y1 = 2.5, double x2 = 10, double y2 = 3.5)
{
var program = LeadPathValidationTests.Rectangle(0, 0, 12, 12);
var hole = LeadPathValidationTests.Rectangle(x1, y1, x2, y2);
program.Codes.AddRange(hole.Codes);
return new Drawing("frame", program);
}
private static (Nest Nest, Plate Plate, Part Part) Plate(CuttingParameters parameters, Drawing drawing = null,
SupportBed bed = null)
{
var nest = new Nest("slug slices");
var plate = new Plate(30, 30) { CuttingParameters = parameters };
nest.Plates.Add(plate);
nest.SetSupportBed(bed ?? Bed);
var part = new Part(drawing ?? Frame(), Location);
plate.Parts.Add(part);
return (nest, plate, part);
}
private static IReadOnlyList<ExecutionMotion> Motions(FixedProgramPlacement proposal) =>
ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, null, default).Motions;
private static double DistanceToSlot(Vector world)
{
// The slot in world coordinates: (3, 3.5)-(11, 4.5); inside it, the nearest side.
return System.Math.Min(System.Math.Min(world.X - 3, 11 - world.X), System.Math.Min(world.Y - 3.5, 4.5 - world.Y));
}
[Fact]
public void ContactLines_AreTheGridsRowsAndColumnsAndTheSlatsCenters()
{
var region = new Box(3, 3.5, 8, 1);
var grid = Bed.ContactLines(region, 1);
var slats = new SupportSlats(SlatAxis.Y, 3, 0.25, offset: 1.5).ContactLines(region, 1);
var mirrored = new SupportGrid(2, 2, 1, 0).ContactLines(new Box(-5, 0, 2, 1), 2);
Assert.Equal([new(true, 4), new(false, 4), new(false, 6), new(false, 8), new(false, 10)], grid);
Assert.Equal([new(false, 4.5), new(false, 7.5), new(false, 10.5)], slats);
// A sheet in quadrant 2 sees the grid's X offset mirrored: columns at -1, -3, -5, ...
Assert.Equal([new(true, 0), new(false, -5), new(false, -3)], mirrored);
}
[Fact]
public void Tips_OnlyForASlugOnContactsTheBedCannotHoldLevel()
{
SupportMaterial World(double x1, double y1, double x2, double y2) =>
SupportMaterial.FromPolygons([new(x1, y1), new(x2, y1), new(x2, y2), new(x1, y2)]);
// On one row of contacts: it balances on a line and tips.
Assert.True(SlugSlicer.Tips(Bed, 1, World(3, 3.5, 11, 4.5)));
// On a block of contacts around its center: it lies level.
Assert.False(SlugSlicer.Tips(Bed, 1, World(3, 3, 11, 9)));
// Between contacts: it falls through whole.
Assert.False(SlugSlicer.Tips(Bed, 1, World(2.5, 2.5, 3.5, 3.5)));
}
[Fact]
public void Slices_RunAlongTheContactLinesAKerfShortOfTheCutout()
{
var slices = SlugSlicer.Slices(Bed, 1, Slot, Location, Kerf, reach: 0.3, pierceClearance: 0.05);
// The row at world Y = 4 left to right, then the columns from X = 10 back to 4, back and forth.
Assert.Equal(5, slices.Count);
var row = slices[0];
Assert.Equal(3, row.Start.Y, 9);
Assert.Equal(3, row.End.Y, 9);
Assert.True(row.End.X > row.Start.X);
Assert.Equal([9, 7, 5, 3], slices.Skip(1).Select(s => System.Math.Round(s.Start.X, 9)));
for (var i = 2; i < slices.Count; i++)
Assert.NotEqual(slices[i - 1].End.Y > slices[i - 1].Start.Y, slices[i].End.Y > slices[i].Start.Y);
foreach (var slice in slices)
{
// Within the Clipper rounding allowance of the kerf, never closer.
foreach (var end in new[] { slice.Start, slice.End })
{
var distance = DistanceToSlot(end + Location);
Assert.InRange(distance, Kerf, Kerf + 1e-3);
}
// Pierced on the slice, its lead-in reach (at most halfway) from the start.
var length = slice.Start.DistanceTo(slice.End);
Assert.Equal(System.Math.Min(0.3, length / 2), slice.Pierce.DistanceTo(slice.Start), 9);
Assert.Equal(length, slice.Pierce.DistanceTo(slice.Start) + slice.Pierce.DistanceTo(slice.End), 9);
}
}
[Fact]
public void Slices_LeaveOutSlicesWhosePierceFitsNeitherEnd()
{
// Pierced 0.1 along a slice from an end 0.06 from the slot, no pierce keeps 0.2 from it.
Assert.Empty(SlugSlicer.Slices(Bed, 1, Slot, Location, Kerf, reach: 0.1, pierceClearance: 0.2));
}
[Fact]
public void PlanCutting_SlicesATippingSlugBeforeItsCutout()
{
var (nest, plate, _) = Plate(Parameters());
var proposal = CuttingPlanBatch.Capture([plate], Parameters(), preservePartOrder: false).Plan();
var result = proposal.Plates[0].Result;
Assert.True(result.Status == CuttingPlanStatus.Ready,
string.Join("\n", proposal.Describe("in").Concat(result.Findings.Select(f => f.Message))));
Assert.True(proposal.CanApply);
Assert.Empty(result.Findings);
var motions = Motions(Assert.Single(result.ProposedOrder));
var slices = motions.Where(m => !m.Rapid && m.Layer == LayerType.Break).ToList();
Assert.Equal(5, slices.Count);
foreach (var slice in slices)
{
Assert.InRange(DistanceToSlot(slice.Curve.Start), Kerf - 1e-9, 1);
Assert.InRange(DistanceToSlot(slice.Curve.End), Kerf - 1e-9, 1);
}
// Every slice is cut before any contour, so before the slot.
var lastSlice = motions.ToList().FindLastIndex(m => !m.Rapid && m.Layer == LayerType.Break);
var firstCut = motions.ToList().FindIndex(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display);
Assert.InRange(lastSlice, 0, firstCut - 1);
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply().Status);
var review = PostVerificationAnalyzer.Analyze(nest).Findings;
Assert.DoesNotContain(review, f => f.Kind is PostVerificationKind.Incomplete or PostVerificationKind.RapidCrossing
or PostVerificationKind.MissingLeadIn or PostVerificationKind.FixedLead or PostVerificationKind.PierceZone);
}
[Theory]
[InlineData(false, Kerf)] // off
[InlineData(true, 0)] // no kerf to stop short by
public void PlanCutting_SlicesNothing_WhenOffOrWithoutAKerf(bool slice, double kerf)
{
var (_, plate, _) = Plate(Parameters(slice, kerf));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(slice, kerf)));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.DoesNotContain(Motions(Assert.Single(result.ProposedOrder)), m => m.Layer == LayerType.Break);
}
[Fact]
public void PlanCutting_DoesNotSliceASlugTheBedHoldsOrThatFallsThrough()
{
// A 6 x 6 hole rests level on a block of contacts; a 1 x 1 hole between contacts falls through.
var program = LeadPathValidationTests.Rectangle(0, 0, 12, 12);
program.Codes.AddRange(LeadPathValidationTests.Rectangle(2, 2, 8, 8).Codes);
program.Codes.AddRange(LeadPathValidationTests.Rectangle(9.5, 9.5, 10.5, 10.5).Codes);
var (_, plate, _) = Plate(Parameters(), new Drawing("two holes", program));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.DoesNotContain(Motions(Assert.Single(result.ProposedOrder)), m => m.Layer == LayerType.Break);
}
[Fact]
public void PlanCutting_DoesNotSliceACutoutAnotherPartIsNestedIn()
{
// A 7.5 x 0.6 strip nested in the slot would be cut through by the slices.
var (_, plate, _) = Plate(Parameters());
plate.Parts.Add(new Part(new Drawing("strip", LeadPathValidationTests.Rectangle(0, 0, 7.5, 0.6)),
new Vector(3.25, 3.7)));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.True(result.Status == CuttingPlanStatus.Ready, string.Join("\n", result.Findings.Select(f => f.Message)));
Assert.All(result.ProposedOrder, proposal =>
Assert.DoesNotContain(Motions(proposal), m => m.Layer == LayerType.Break));
}
[Fact]
public void PlanCutting_SlicesAlongSlats()
{
// Slats along X every 2 inches: the slot's slug rests on the one at Y = 4 alone, too
// narrow under it for the stability margin.
var (_, plate, _) = Plate(Parameters(), bed: new SupportSlats(SlatAxis.X, 2, 0.125, stabilityMargin: 0.25));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
var slice = Assert.Single(Motions(Assert.Single(result.ProposedOrder)), m => !m.Rapid && m.Layer == LayerType.Break);
Assert.Equal(4, slice.Curve.Start.Y, 9);
Assert.Equal(4, slice.Curve.End.Y, 9);
}
[Fact]
public void Settings_RoundTripAndAreLeftOutWhenOff()
{
var on = OpenNest.IO.CuttingParametersSerializer.Serialize(Parameters());
var off = OpenNest.IO.CuttingParametersSerializer.Serialize(Parameters(slice: false));
Assert.True(OpenNest.IO.CuttingParametersSerializer.Deserialize(on).SliceTippingSlugs);
Assert.False(OpenNest.IO.CuttingParametersSerializer.Deserialize(off).SliceTippingSlugs);
Assert.DoesNotContain("sliceTippingSlugs", off);
Assert.True(OwnedCuttingParameters.Copy(Parameters()).SliceTippingSlugs);
}
[Fact]
public void ReleasedContours_CrossingCutsInsideAHoleCutOnlyItsSlug()
{
// Crossing break cuts in the slot, then the slot, then the outside cut in two open pieces,
// so the open-cut proof runs.
var findings = new ReleasedContourState().Check(
ExecutionMotionReader.Read(TwoPieceFrameWithBreaks(inHole: true), Vector.Zero, null, default), Vector.Zero, 1);
Assert.DoesNotContain(findings, f => f.Kind == PostVerificationKind.Incomplete);
}
[Fact]
public void ReleasedContours_CrossingBreakCutsInMaterialStillNeedReview()
{
var findings = new ReleasedContourState().Check(
ExecutionMotionReader.Read(TwoPieceFrameWithBreaks(inHole: false), Vector.Zero, null, default), Vector.Zero, 1);
Assert.Contains(findings, f => f.Kind == PostVerificationKind.Incomplete
&& f.Message.Contains("Multiple interrupted/open"));
}
// A 14 inch square cut in two open pieces with a slot at (3, 3.5)-(11, 4.5), and a row and two
// columns of break cuts crossing either inside the slot or in the material above it.
private static Program TwoPieceFrameWithBreaks(bool inHole)
{
var y = inHole ? 4 : 8;
var program = new Program();
program.MoveTo(3.1, y);
program.Codes.Add(new LinearMove(10.9, y) { Layer = LayerType.Break });
foreach (var x in new[] { 4.0, 6.0 })
{
program.MoveTo(x, y - 0.4);
program.Codes.Add(new LinearMove(x, y + 0.4) { Layer = LayerType.Break });
}
program.MoveTo(3, 3.5);
program.LineTo(11, 3.5);
program.LineTo(11, 4.5);
program.LineTo(3, 4.5);
program.LineTo(3, 3.5);
program.MoveTo(0.2, 0);
program.LineTo(14, 0);
program.LineTo(14, 14);
program.LineTo(7.2, 14);
program.MoveTo(6.8, 14);
program.LineTo(0, 14);
program.LineTo(0, 0.2);
return program;
}
}
+18
View File
@@ -31,6 +31,7 @@ namespace OpenNest.Controls
private readonly NumericUpDown nudTabSizeMin;
private readonly NumericUpDown nudTabSizeMax;
private readonly NumericUpDown nudTabHeldMax;
private readonly CheckBox chkSliceSlugs;
private readonly NumericUpDown nudPierceClearance;
private readonly NumericUpDown nudPierceDiameter;
@@ -267,6 +268,21 @@ namespace OpenNest.Controls
nudTabSizeMax.ValueChanged += (s, e) => OnParametersChanged();
nudTabHeldMax.ValueChanged += (s, e) => OnParametersChanged();
// Cutouts: slugs that would tip are cut in pieces that drop between the supports.
AddSection("Cutouts");
chkSliceSlugs = new CheckBox
{
Text = "Slice slugs the support grid can't hold level",
AutoSize = true,
Margin = new Padding(0, 3, 0, 3),
};
toolTip.SetToolTip(chkSliceSlugs,
"Plan Cutting first cuts each cutout slug that would tip along the support grid's lines, "
+ "stopping a kerf short of the cutout, so its pieces drop between the supports.");
layout.Controls.Add(chkSliceSlugs);
chkSliceSlugs.CheckedChanged += (s, e) => OnParametersChanged();
// Pierce
AddSection("Pierce");
@@ -356,6 +372,7 @@ namespace OpenNest.Controls
parameters.TabSizeMinWidth = (double)nudTabSizeMin.Value;
parameters.TabSizeMaxWidth = (double)nudTabSizeMax.Value;
parameters.TabHeldMaxLength = (double)nudTabHeldMax.Value;
parameters.SliceTippingSlugs = chkSliceSlugs.Checked;
return parameters;
}
@@ -382,6 +399,7 @@ namespace OpenNest.Controls
nudTabHeldMax.Value = Clamp(nudTabHeldMax, p.TabHeldMaxLength);
tabOptions.Enabled = chkTabsEnabled.Checked;
UpdateSupportTabOptions();
chkSliceSlugs.Checked = p.SliceTippingSlugs;
nudPierceClearance.Value = Clamp(nudPierceClearance, p.PierceClearance);
nudPierceDiameter.Value = Clamp(nudPierceDiameter, p.PierceDiameter);
chkRoundLeadInAngles.Checked = p.RoundLeadInAngles;
+3
View File
@@ -50,6 +50,9 @@ every full or prefix emission, never in single-contour probes) and the manual le
scrap direction (`BreakCutEntry`), because the scrap narrows to a wedge near the
outline. Entering at the far end cuts it in reverse.
Plan Cutting also cuts [slug slices](support-tabs.md#slug-slices) on the `Break` layer: open
cuts inside a cutout's slug, pierced on themselves with a straight lead-in along the cut.
Checks treat them as intentional open cuts retained by tabs: `ReleasedContourState`
never makes them rapid obstacles or "open fragment" warnings, but reports a missing
lead-in like any contour. `LeadPathValidator` lets a lead into a break cut run
+5
View File
@@ -153,6 +153,11 @@ the final replay enforce them:
contour, entered from the scrap with the internal lead-in. Their leads may run
through the part's own material but not touch its boundaries; they are never rapid
obstacles or unexplained open fragments.
- [Slug slices](support-tabs.md#slug-slices) (`CuttingParameters.SliceTippingSlugs`) are break
cuts capture adds inside each cutout whose slug the plate's support bed cannot hold level,
along the bed's contact lines and a kerf short of the cutout. Every full and prefix emission
cuts them right after the drawing's break cuts, pierced on themselves; the open-cut proof leaves
out those wholly inside one of the part's closed holes.
- A drawing's tabbed cutouts (closed holes with `LayerType.Tab` spans, see
[tabbed cutouts](tabbed-cutouts.md)) are material but not contour choices. Every full
and prefix emission cuts their pieces after the break cuts and before any contour,
+2 -1
View File
@@ -27,7 +27,8 @@ save them on close; thicknesses matching none stay blank.
Every setting in Plan Cutting's Cutting Parameters dialog and the Place Lead-in panel is kept on
the nest's machine thickness: leads on the Outside Cuts and Inside Cuts tabs, lead-in angle
rounding, tabs and pierce on the Tabs & Pierce tab (`ThicknessConfig.Cutting`). So 0.25 in can
rounding, tabs, [slug slicing](support-tabs.md#slug-slices) and pierce on the Tabs & Pierce tab
(`ThicknessConfig.Cutting`). So 0.25 in can
tab parts while 0.5 in tabs none, and in Database mode every workstation gets the same
settings. The dialog names what it edits above its fields ("Saved for Fiber 1 · Mild Steel A36
0.25 in") or why it edits this PC's settings instead (no machine, no material or thickness, or
+42
View File
@@ -190,6 +190,48 @@ advisory note instead (`SupportTabCapture.Notes`; never for a pin): a Ready plan
status and lists it in Plan Cutting's warnings, and the console and MCP summaries print it under
the plate.
## Slug slices
A cutout's slug drops onto the bed too, and one the bed cannot hold level can tip up into the
head's path. Cutting Settings > Cutouts > "Slice slugs the support grid can't hold level" sets
`CuttingParameters.SliceTippingSlugs` (off by default, kept per machine thickness like every
cutting setting, and written to settings and nest files only when on). With it on and a support
bed, Plan Cutting cuts each such slug into pieces that hold no contact before it cuts the cutout,
so the pieces drop between the supports (or lie flat across them) instead of tipping.
- Which slugs: `SlugSlicer.Tips` analyzes the cutout's inside as material on the bed (as above,
at the part's placed position). A slug the bed holds level is not sliced, and nor is one that
rests on no contact, which falls through whole.
- Where: along every line the bed's contacts lie on (`SupportBed.ContactLines`): the grid's rows
and columns through its points, a slat bed's slat center lines. Every contact under the slug is
then under a slice's kerf, so no piece rests on one: a grid's pieces are its cells, a slat bed's
are strips between slats.
- How far: each slice stops `CuttingParameters.Kerf` short of the cutout (measured from the
cutout chorded at 0.001, eroded with round corners), so the pieces stay attached to the part
until the cutout itself is cut, its kerf meets the slices' ends and they all drop together. No
kerf, no slices. A slice no longer than the kerf is left out.
- Which cutouts: one that holds another of the part's cuts (a contour, break cut or tabbed
cutout), or whose box holds another part's box (a part nested in it), is never sliced, since a
slice would cut what is inside. Tabbed cutouts keep their slugs, so they are never sliced.
- Cutting: the slices are break cuts (`LayerType.Break`) inside the slug: never material, never
rapid obstacles, cut by every post that cuts break cuts. `PreparedContours.WithSlugSlices` keeps
them in cutting order, and every full or prefix emission cuts them right after the drawing's
own break cuts and before any contour, so every slug is sliced before its cutout. The grid's
rows come first, then its columns, each set from its end nearer the last slice, every slice
started at its end nearer where the one before ended. Each is pierced on itself, the cutouts'
lead-in reach (but at least the pierce clearance, and at most halfway) along it, and led
straight back to its start; it has no lead-out. That pierce must keep the larger of the
effective pierce clearance and the kerf from the cutout, from one end or the other; a slice
whose pierce fits at neither is left out.
- Checks: inner grid cells are cut free as soon as their four slices are cut. The released-contour
check's open-cut proof therefore leaves out a straight break cut, leads included, that lies
wholly inside one of the part's closed holes (any closed cut but its largest) and touches none
of it: what it encloses is that hole's slug. Break cuts in material still take part in the proof.
The hole's own lead-in is chosen as usual and may pierce in a slice's kerf or a piece already
dropped. Slices are planned for the nominal bed: a sheet registered off by up to its tolerance can
leave a contact just inside a piece's edge.
## Lead-ins placed by hand
Plate > Place Lead-in cuts where it is clicked. With tabs on, `SupportTabSettings.ForManualLeadIn`