mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 09:21:05 -04:00
fix(planning): refuse misplaced display perimeters by name
Review found three blanks that slipped past or failed silently. Enclosure was checked against the outline's bounding box, so a cutout in the notch of an L-shaped blank reached an unverified plan that warning acceptance could apply; every cut point is now tested inside the outline itself, and the material capture names the display perimeter when no ring encloses the rest. A blank whose only cutouts are tabbed left the search with nothing to choose and ended without a finding; it is now refused with its reason, as is a blank with nothing to cut, which used to get only the generic noncutting message. None of these offers even an unverified plan. Clean-bounds containment, seam overcut trimming and incremental closure drift now treat the display perimeter like the other material boundaries.
This commit is contained in:
1 parent
b45e9df593
commit
73e54fbd67
7 files changed
+86
-16
No files matched your search
@@ -49,8 +49,8 @@ internal static class IncrementalClosureDrift
|
||||
|
||||
// The chains material capture closes: cut, display and tab moves between rapids and other layers.
|
||||
private static bool IsContour(Motion motion) => !motion.Suppressed
|
||||
&& motion is LinearMove { Layer: LayerType.Cut or LayerType.Display or LayerType.Tab }
|
||||
or ArcMove { Layer: LayerType.Cut or LayerType.Display or LayerType.Tab };
|
||||
&& motion is LinearMove { Layer: LayerType.Cut or LayerType.Display or LayerType.Tab or LayerType.DisplayPerimeter }
|
||||
or ArcMove { Layer: LayerType.Cut or LayerType.Display or LayerType.Tab or LayerType.DisplayPerimeter };
|
||||
|
||||
private static bool TryClose(Program program, List<Motion> run, Vector start, Vector end, int next)
|
||||
{
|
||||
|
||||
@@ -134,7 +134,9 @@ public sealed class LeadMaterialSnapshot
|
||||
}
|
||||
}
|
||||
if (outer < 0)
|
||||
throw new ArgumentException("Material must have exactly one enclosing perimeter.");
|
||||
throw new ArgumentException(ringDisplay.Contains(true)
|
||||
? "A display perimeter must enclose every other contour."
|
||||
: "Material must have exactly one enclosing perimeter.");
|
||||
if (ringTabs[outer].Any(tab => tab))
|
||||
throw new ArgumentException("A perimeter cannot have tabs.");
|
||||
if (ringDisplay.Where((_, i) => i != outer).Any(display => display))
|
||||
|
||||
@@ -54,7 +54,7 @@ internal static class NestedPartContainment
|
||||
/// <summary>Material extent of a clean program at a location: rapids and scribe marks excluded.</summary>
|
||||
internal static Box CleanBounds(Program clean, Vector location, CancellationToken token) =>
|
||||
ExecutionMotionReader.ReadSupported(clean, location, null, token).Motions
|
||||
.Where(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display && m.Curve != null)
|
||||
.Where(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display or LayerType.DisplayPerimeter && m.Curve != null)
|
||||
.Select(m => m.Curve.ToEntity().BoundingBox).ToList().GetBoundingBox();
|
||||
|
||||
/// <summary>Whether a host's bounds admit an inner part's bounds as a containment candidate.</summary>
|
||||
|
||||
@@ -293,15 +293,16 @@ public sealed class PreparedContours
|
||||
throw new ArgumentException("Preparation requires nonempty closed contours.");
|
||||
if (contours.Count == 0 && tabbedCutouts.Count == 0 && breakCuts.Count == 0)
|
||||
throw new ArgumentException("A display perimeter with no cutouts has nothing to cut.");
|
||||
var outline = displayPerimeter.BoundingBox;
|
||||
// Every cut must lie on the blank: a non-rectangular blank's box is not enough.
|
||||
var outline = displayPerimeter.ToPolygon();
|
||||
foreach (var shape in contours.Concat(tabbedCutouts).Concat(breakCuts))
|
||||
{
|
||||
shape.UpdateBounds();
|
||||
var box = shape.BoundingBox;
|
||||
if (box.Left < outline.Left - PostVerificationGeometry.Epsilon || box.Right > outline.Right + PostVerificationGeometry.Epsilon
|
||||
|| box.Bottom < outline.Bottom - PostVerificationGeometry.Epsilon || box.Top > outline.Top + PostVerificationGeometry.Epsilon)
|
||||
throw new ArgumentException("A display perimeter must enclose every other contour.");
|
||||
}
|
||||
foreach (var entity in shape.Entities)
|
||||
if (!outline.ContainsPoint(entity is Circle circle ? circle.Center + new Vector(circle.Radius, 0) : Start(entity)))
|
||||
throw new ArgumentException("A display perimeter must enclose every other contour.");
|
||||
// The search plans a part through its closed contours; tabbed cutouts and break cuts
|
||||
// alone leave it nothing to choose.
|
||||
if (contours.Count == 0)
|
||||
throw new ArgumentException("A display perimeter needs at least one closed cutout without tabs.");
|
||||
foreach (var cut in breakCuts)
|
||||
cut.UpdateBounds();
|
||||
// Never a contour: nothing can choose, emit or lead it. Every contour is a hole.
|
||||
|
||||
@@ -72,8 +72,8 @@ internal static class SeamOvercut
|
||||
// Incremental coordinates only need a consistent origin within one contour.
|
||||
var reference = incremental ? position ?? new Vector() : new Vector();
|
||||
var end = reference + motion.EndPoint;
|
||||
var cut = !motion.Suppressed && motion is LinearMove { Layer: LayerType.Cut or LayerType.Display }
|
||||
or ArcMove { Layer: LayerType.Cut or LayerType.Display };
|
||||
var cut = !motion.Suppressed && motion is LinearMove { Layer: LayerType.Cut or LayerType.Display or LayerType.DisplayPerimeter }
|
||||
or ArcMove { Layer: LayerType.Cut or LayerType.Display or LayerType.DisplayPerimeter };
|
||||
if (!cut || position == null)
|
||||
{
|
||||
Finish();
|
||||
|
||||
@@ -105,7 +105,9 @@ public static class CuttingPlanService
|
||||
}
|
||||
var clean = ExecutionMotionReader.ReadSupported(source.BaseDrawing.Program, Vector.Zero, null, token);
|
||||
if (!clean.HasCuttingContour)
|
||||
throw new NotSupportedException("Scribe-only or noncutting source drawings are outside this route slice.");
|
||||
throw new NotSupportedException(clean.Motions.Any(m => m.Layer == LayerType.DisplayPerimeter)
|
||||
? "A display perimeter with no cutouts has nothing to cut."
|
||||
: "Scribe-only or noncutting source drawings are outside this route slice.");
|
||||
var execution = ExecutionMotionReader.ReadSupported(source.Program, source.Location, request.StartPoint, token);
|
||||
if (!execution.HasCuttingContour)
|
||||
throw new ArgumentException("Placed program has no nonzero cutting contour motions.");
|
||||
|
||||
@@ -90,13 +90,19 @@ public class DisplayPerimeterTests
|
||||
[Theory]
|
||||
[InlineData("mixed", "one whole closed contour")]
|
||||
[InlineData("hole", "enclose every other contour")]
|
||||
[InlineData("nothing", "noncutting")]
|
||||
[InlineData("nothing", "display perimeter with no cutouts")]
|
||||
[InlineData("notch", "enclose every other contour")]
|
||||
[InlineData("second", "enclose every other contour")]
|
||||
[InlineData("tabbedOnly", "closed cutout without tabs")]
|
||||
public void InvalidDisplayPerimeters_AreRefusedWithTheirReason(string layout, string reason)
|
||||
{
|
||||
var program = layout switch
|
||||
{
|
||||
"mixed" => Door(lastEdge: LayerType.Cut),
|
||||
"nothing" => Door(cutouts: false),
|
||||
"notch" => NotchedDoor(),
|
||||
"second" => SecondDisplayRing(),
|
||||
"tabbedOnly" => TabbedOnly(),
|
||||
_ => HoleOnDisplay(),
|
||||
};
|
||||
var plate = DoorPlate(program);
|
||||
@@ -106,6 +112,38 @@ public class DisplayPerimeterTests
|
||||
|
||||
Assert.NotEqual(CuttingPlanStatus.Ready, result.Status);
|
||||
Assert.Contains(result.Findings, f => f.Message.Contains(reason, StringComparison.OrdinalIgnoreCase));
|
||||
// Not even an unverified plan: nothing about a refused blank may be applied.
|
||||
var proposal = CuttingPlanBatch.Capture([DoorPlate(program)], ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.False(proposal.CanApply);
|
||||
Assert.False(proposal.CanApplyWithWarnings);
|
||||
|
||||
// An L-shaped blank with a cutout in its notch: inside the outline's box, outside the blank.
|
||||
static Program NotchedDoor()
|
||||
{
|
||||
var program = new Program();
|
||||
Ring(program, LayerType.DisplayPerimeter, (0, 0), (0, 20), (15, 20), (15, 10), (30, 10), (30, 0));
|
||||
Ring(program, LayerType.Cut, (2, 2), (2, 6), (6, 6), (6, 2));
|
||||
Ring(program, LayerType.Cut, (20, 14), (20, 18), (25, 18), (25, 14));
|
||||
return program;
|
||||
}
|
||||
|
||||
static Program SecondDisplayRing()
|
||||
{
|
||||
var program = Door();
|
||||
Ring(program, LayerType.DisplayPerimeter, (26, 14), (26, 18), (28, 18), (28, 14));
|
||||
return program;
|
||||
}
|
||||
|
||||
static Program TabbedOnly()
|
||||
{
|
||||
var program = Door(cutouts: false);
|
||||
program.Codes.Add(new RapidMove(5, 5));
|
||||
program.Codes.Add(new LinearMove(5, 10) { Layer = LayerType.Cut });
|
||||
program.Codes.Add(new LinearMove(10, 10) { Layer = LayerType.Tab });
|
||||
program.Codes.Add(new LinearMove(10, 5) { Layer = LayerType.Cut });
|
||||
program.Codes.Add(new LinearMove(5, 5) { Layer = LayerType.Cut });
|
||||
return program;
|
||||
}
|
||||
|
||||
static Program HoleOnDisplay()
|
||||
{
|
||||
@@ -117,6 +155,33 @@ public class DisplayPerimeterTests
|
||||
}
|
||||
}
|
||||
|
||||
private static void Ring(Program program, LayerType layer, params (double X, double Y)[] points)
|
||||
{
|
||||
program.Codes.Add(new RapidMove(points[0].X, points[0].Y));
|
||||
foreach (var (x, y) in points.Skip(1).Append(points[0]))
|
||||
program.Codes.Add(new LinearMove(x, y) { Layer = layer });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Preparation_HasNoCutPerimeterAndTreatsEveryContourAsAHole()
|
||||
{
|
||||
var prepared = PreparedContours.Capture(Door(), ExplicitContourTests.Parameters());
|
||||
|
||||
Assert.False(prepared.HasCutPerimeter);
|
||||
Assert.Equal(3, prepared.Count);
|
||||
Assert.Equal(prepared.Count, prepared.PerimeterOrdinal);
|
||||
Assert.True(PreparedContours.Capture(Door(LayerType.Cut), ExplicitContourTests.Parameters()).HasCutPerimeter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CleanBounds_IncludeTheDisplayPerimeter()
|
||||
{
|
||||
var bounds = NestedPartContainment.CleanBounds(Door(), Vector.Zero, default);
|
||||
|
||||
Assert.Equal(30, bounds.Length, 9);
|
||||
Assert.Equal(20, bounds.Width, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replay_RefusesAProgramThatCutsTheDisplayPerimeter()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user