mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 08:21:05 -04:00
fix(cut-offs): accept a blank's outline a rounding step past the sheet
Automatic cut-offs refused any part reaching past the physical sheet by more than Tolerance.Epsilon. Imported supplied blanks (panel doors, remnants) equal their sheet but carry PEP's rounding: a 0.000006 outline skew plus placements rounded to 0.001 put the never-cut outline 0.00001 to 0.00003 in past the sheet, so Plan Cutting refused every such nest. A display perimeter may now extend up to DisplayPerimeterSheetAllowance (0.0001 model units) past the sheet. Nothing cuts it, and a blank that fills its sheet gets no cut-offs. Everything else on the part keeps the ordinary tolerance: cuts, leads, scribes and travel, except a rapid that ends on the outline itself (the move to its first corner, or an applied program's display tail). A larger overhang is still refused.
This commit is contained in:
1 parent
c188014a30
commit
bbadd6aa75
2 files changed
+131
-6
No files matched your search
@@ -90,6 +90,15 @@ public sealed class AutomaticCutOffPlan
|
||||
public static class AutomaticCutOffPlanner
|
||||
{
|
||||
public const double GeometryTolerance = Tolerance.Epsilon;
|
||||
|
||||
/// <summary>
|
||||
/// How far a display perimeter (a supplied blank's never-cut outline) may extend past the
|
||||
/// physical sheet, in model units. Imported blanks equal their sheet but carry the source CAM's
|
||||
/// rounding (PEP: a 0.000006 outline skew plus placements rounded to 0.001), a few hundred-
|
||||
/// thousandths of an inch. Nothing cuts that outline, so it is accepted; anything cut, or a
|
||||
/// larger overhang that signals a real placement error, is still refused.
|
||||
/// </summary>
|
||||
public const double DisplayPerimeterSheetAllowance = 1e-4;
|
||||
public const double MinimumSpacing = 2 * GeometryTolerance;
|
||||
public const int MaximumCandidateCount = 10000;
|
||||
public const int MaximumShiftCandidateCount = 20000;
|
||||
@@ -432,19 +441,31 @@ public static class AutomaticCutOffPlanner
|
||||
Require(ValidBox(cached) && Near(box.Left, cached.Left) && Near(box.Right, cached.Right) &&
|
||||
Near(box.Bottom, cached.Bottom) && Near(box.Top, cached.Top),
|
||||
"Part geometry has stale bounds; update it before planning.", "plate");
|
||||
Require(Inside(box, sheet), "Part geometry extends outside the physical sheet.", "plate");
|
||||
var hasDisplayPerimeter = DisplayPerimeterTail.HasDisplayPerimeterMotion(part.Program);
|
||||
Require(Inside(box, sheet, hasDisplayPerimeter ? DisplayPerimeterSheetAllowance : GeometryTolerance),
|
||||
"Part geometry extends outside the physical sheet.", "plate");
|
||||
|
||||
// Checking raw coordinates above prevents NaNs being hidden by min/max comparisons.
|
||||
// Checking converted entities catches overflowing incremental moves and curve bounds.
|
||||
var hasMaterial = false;
|
||||
var occupied = sign > 0 ? box.Right : -box.Left;
|
||||
var roundoff = CutOff.GetBoundsRoundoff(part);
|
||||
foreach (var entity in ConvertProgram.ToGeometry(part.Program))
|
||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||
var outline = entities.Where(e => e.Layer == SpecialLayers.DisplayPerimeter).ToList();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var entityBox = entity.BoundingBox;
|
||||
Require(ValidBox(entityBox), "Part contains invalid converted geometry.", "plate");
|
||||
if (!SpecialLayers.IsMaterial(entity.Layer))
|
||||
{
|
||||
// Travel and marks (a scribe is lasered) keep the ordinary tolerance. Only a rapid
|
||||
// ending on the outline (its first corner, or an applied program's display tail)
|
||||
// shares the outline's allowance.
|
||||
Require(Inside(entityBox.Translate(part.Location), sheet)
|
||||
|| hasDisplayPerimeter && IsRapidOntoOutline(entity, outline, part.Location, sheet),
|
||||
"Part geometry extends outside the physical sheet.", "plate");
|
||||
continue;
|
||||
}
|
||||
entityBox = entityBox.Translate(part.Location);
|
||||
// Permit only bounded floating-point roundoff, with the same conservative
|
||||
// padding in CutOff's broad phase and fallback. Geometry-scale protrusions
|
||||
@@ -452,7 +473,9 @@ public static class AutomaticCutOffPlanner
|
||||
Require(entityBox.Left >= cached.Left - roundoff && entityBox.Right <= cached.Right + roundoff &&
|
||||
entityBox.Bottom >= cached.Bottom - roundoff && entityBox.Top <= cached.Top + roundoff,
|
||||
"Converted material extends outside cached part bounds; repair it before planning.", "plate");
|
||||
Require(Inside(entityBox, sheet), "Part geometry extends outside the physical sheet.", "plate");
|
||||
Require(Inside(entityBox, sheet,
|
||||
entity.Layer == SpecialLayers.DisplayPerimeter ? DisplayPerimeterSheetAllowance : GeometryTolerance),
|
||||
"Part geometry extends outside the physical sheet.", "plate");
|
||||
occupied = System.Math.Max(occupied, sign > 0 ? entityBox.Right : -entityBox.Left);
|
||||
hasMaterial |= entityBox.Length > 0 || entityBox.Width > 0;
|
||||
}
|
||||
@@ -531,9 +554,21 @@ public static class AutomaticCutOffPlanner
|
||||
Nonnegative(box.Length) && Nonnegative(box.Width) &&
|
||||
double.IsFinite(box.Right) && double.IsFinite(box.Top);
|
||||
|
||||
private static bool Inside(Box box, Box sheet) =>
|
||||
box.Left >= sheet.Left - GeometryTolerance && box.Right <= sheet.Right + GeometryTolerance &&
|
||||
box.Bottom >= sheet.Bottom - GeometryTolerance && box.Top <= sheet.Top + GeometryTolerance;
|
||||
// A rapid that leaves the sheet only at an end lying on the display perimeter, within its allowance.
|
||||
private static bool IsRapidOntoOutline(Entity entity, List<Entity> outline, Vector location, Box sheet)
|
||||
{
|
||||
if (entity.Layer != SpecialLayers.Rapid || entity is not Line rapid
|
||||
|| !Inside(rapid.BoundingBox.Translate(location), sheet, DisplayPerimeterSheetAllowance))
|
||||
return false;
|
||||
bool Allowed(Vector point) =>
|
||||
Inside(new Box(point.X + location.X, point.Y + location.Y, 0, 0), sheet)
|
||||
|| outline.Any(e => e.ClosestPointTo(point).DistanceTo(point) <= GeometryTolerance);
|
||||
return Allowed(rapid.StartPoint) && Allowed(rapid.EndPoint);
|
||||
}
|
||||
|
||||
private static bool Inside(Box box, Box sheet, double tolerance = GeometryTolerance) =>
|
||||
box.Left >= sheet.Left - tolerance && box.Right <= sheet.Right + tolerance &&
|
||||
box.Bottom >= sheet.Bottom - tolerance && box.Top <= sheet.Top + tolerance;
|
||||
|
||||
private static bool Finite(Vector point) => double.IsFinite(point.X) && double.IsFinite(point.Y);
|
||||
private static bool Nonnegative(double value) => double.IsFinite(value) && value >= 0;
|
||||
|
||||
@@ -234,6 +234,96 @@ public class DisplayPerimeterTests
|
||||
Assert.All(motions.Where(m => !m.Rapid), m => Assert.True(InsideACutout(m.End), $"({m.End.X}, {m.End.Y}) leaves the cutouts."));
|
||||
}
|
||||
|
||||
// PEP draws a supplied blank a few hundred-thousandths of an inch past its sheet (an outline
|
||||
// skew of 0.000006 plus placements rounded to 0.001). The outline is never cut, so automatic
|
||||
// cut-offs accept it within DisplayPerimeterSheetAllowance; anything larger, or the same overhang
|
||||
// on geometry that is cut, is still refused.
|
||||
[Theory]
|
||||
[InlineData(LayerType.DisplayPerimeter, 3e-5, 0, true)]
|
||||
[InlineData(LayerType.DisplayPerimeter, 0, 3e-5, true)]
|
||||
[InlineData(LayerType.DisplayPerimeter, 2e-4, 0, false)]
|
||||
[InlineData(LayerType.Cut, 3e-5, 0, false)]
|
||||
public void AutomaticCutOffs_AcceptOnlyADisplayPerimeterWithinTheAllowancePastTheSheet(
|
||||
LayerType outline, double overRight, double overBottom, bool accepted)
|
||||
{
|
||||
var plate = new Plate(new Size(20, 30 - overRight));
|
||||
plate.Parts.Add(new Part(new Drawing("door", Door(outline)), new Vector(0, -overBottom)));
|
||||
|
||||
var create = () => AutomaticCutOffPlanner.Create(plate,
|
||||
new AutomaticCutOffOptions { Spacing = 35, MinimumStripLength = 17.5, MinimumTailLength = 12 },
|
||||
new CutOffSettings());
|
||||
|
||||
if (!accepted)
|
||||
{
|
||||
var refusal = Assert.Throws<ArgumentException>(create);
|
||||
Assert.Contains("outside the physical sheet", refusal.Message);
|
||||
return;
|
||||
}
|
||||
// The blank fills the sheet: nothing is left to cut off.
|
||||
Assert.Empty(create().Definitions);
|
||||
}
|
||||
|
||||
// Cut-offs are staged before planning checks the door, so a malformed one can still carry a
|
||||
// cut past the sheet while its outline is inside: the allowance covers the outline only.
|
||||
[Fact]
|
||||
public void AutomaticCutOffs_RefuseACutPastTheSheetEvenOnADisplayPerimeterPart()
|
||||
{
|
||||
var program = Door();
|
||||
program.MoveTo(29, 1); program.LineTo(30.00003, 1); program.LineTo(30.00003, 2); program.LineTo(29, 2); program.LineTo(29, 1);
|
||||
var plate = new Plate(new Size(20, 30));
|
||||
plate.Parts.Add(new Part(new Drawing("door", program), Vector.Zero));
|
||||
|
||||
var refusal = Assert.Throws<ArgumentException>(() => AutomaticCutOffPlanner.Create(plate,
|
||||
new AutomaticCutOffOptions { Spacing = 35, MinimumStripLength = 17.5, MinimumTailLength = 12 },
|
||||
new CutOffSettings()));
|
||||
|
||||
Assert.Contains("outside the physical sheet", refusal.Message);
|
||||
}
|
||||
|
||||
private static AutomaticCutOffPlan StageCutOffs(Plate plate) => AutomaticCutOffPlanner.Create(plate,
|
||||
new AutomaticCutOffOptions { Spacing = 35, MinimumStripLength = 17.5, MinimumTailLength = 12 },
|
||||
new CutOffSettings());
|
||||
|
||||
// The allowance belongs to the outline alone: a scribe (lasered) or a rapid past the sheet on a
|
||||
// door is refused at the ordinary tolerance, except a rapid ending on the outline itself.
|
||||
[Theory]
|
||||
[InlineData("scribe")]
|
||||
[InlineData("rapid")]
|
||||
public void AutomaticCutOffs_RefuseAScribeOrRapidPastTheSheetOnADisplayPerimeterPart(string motion)
|
||||
{
|
||||
var program = Door();
|
||||
if (motion == "scribe")
|
||||
{
|
||||
program.Codes.Add(new RapidMove(29, 1));
|
||||
program.Codes.Add(new LinearMove(30.00003, 1) { Layer = LayerType.Scribe });
|
||||
program.Codes.Add(new RapidMove(29, 2));
|
||||
}
|
||||
else
|
||||
program.Codes.Add(new RapidMove(30.00005, 1));
|
||||
var plate = new Plate(new Size(20, 30));
|
||||
plate.Parts.Add(new Part(new Drawing("door", program), Vector.Zero));
|
||||
|
||||
var refusal = Assert.Throws<ArgumentException>(() => StageCutOffs(plate));
|
||||
|
||||
Assert.Contains("outside the physical sheet", refusal.Message);
|
||||
}
|
||||
|
||||
// The door's rapid to its outline's first corner, in the drawing and again in an applied
|
||||
// program's display tail, ends on the outline, so it shares the outline's allowance.
|
||||
[Fact]
|
||||
public void AutomaticCutOffs_AcceptTheRapidToAnOverhangingOutlineBeforeAndAfterApply()
|
||||
{
|
||||
var plate = new Plate(new Size(20, 30 - 3e-5));
|
||||
var part = new Part(new Drawing("door", Door()), new Vector(-3e-5, 0));
|
||||
plate.Parts.Add(part);
|
||||
|
||||
Assert.Empty(StageCutOffs(plate).Definitions);
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: false).Status);
|
||||
Assert.True(DisplayPerimeterTail.HasTail(part.Program));
|
||||
Assert.Empty(StageCutOffs(plate).Definitions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Door_AutomaticCutOffsAddNoLineAcrossTheBlank()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user