fix(cutoffs): account for bounds translation roundoff safely

This commit is contained in:
aj
2026-09-28 22:19:02 -04:00
parent a8204bb51f
commit a5fddec0e9
4 changed files with 146 additions and 10 deletions
+11 -9
View File
@@ -99,8 +99,7 @@ public static class AutomaticCutOffPlanner
if (part.BaseDrawing.IsCutOff)
continue;
var partBounds = ValidatePart(part, bounds);
occupied = System.Math.Max(occupied, sign > 0 ? partBounds.Right : -partBounds.Left);
occupied = System.Math.Max(occupied, ValidatePart(part, bounds, sign));
hasParts = true;
}
if (!hasParts)
@@ -232,7 +231,7 @@ public static class AutomaticCutOffPlanner
"Plate parts and cut-off collections are required.", nameof(plate));
}
private static Box ValidatePart(Part part, Box sheet)
private static double ValidatePart(Part part, Box sheet, int sign)
{
Require(Finite(part.Location) && double.IsFinite(part.Rotation),
"Part pose must be finite.", "plate");
@@ -250,6 +249,8 @@ public static class AutomaticCutOffPlanner
// 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 entityBox = entity.BoundingBox;
@@ -257,17 +258,18 @@ public static class AutomaticCutOffPlanner
if (!SpecialLayers.IsMaterial(entity.Layer))
continue;
entityBox = entityBox.Translate(part.Location);
// Conversion can refit arc centers beyond the raw-program/cached bounds.
// CutOff's broad phase uses cached bounds without tolerance: even a smaller
// protrusion can be crossed by a repeated line, so require strict containment.
Require(entityBox.Left >= cached.Left && entityBox.Right <= cached.Right &&
entityBox.Bottom >= cached.Bottom && entityBox.Top <= cached.Top,
// Permit only bounded floating-point roundoff, with the same conservative
// padding in CutOff's broad phase and fallback. Geometry-scale protrusions
// (including refitted arc centers) are still rejected, even below epsilon.
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");
occupied = System.Math.Max(occupied, sign > 0 ? entityBox.Right : -entityBox.Left);
hasMaterial |= entityBox.Length > 0 || entityBox.Width > 0;
}
Require(hasMaterial, "Real parts must contain material geometry.", "plate");
return box;
return occupied;
}
private static void ValidateProgram(Program program, HashSet<Program> path)
+26
View File
@@ -135,8 +135,15 @@ namespace OpenNest
)
{
var bb = part.BoundingBox;
var roundoff = GetBoundsRoundoff(part);
var (partMin, partMax) = AxisBounds(bb, clearance);
var (partStart, partEnd) = CrossAxisBounds(bb, clearance);
// Match the planner's representational allowance in BOTH pruning and
// fallback exclusions. Tolerating it only in validation could cut an edge.
partMin -= roundoff;
partMax += roundoff;
partStart -= roundoff;
partEnd += roundoff;
if (cutPosition < partMin || cutPosition > partMax)
return EmptyExclusions;
@@ -157,6 +164,25 @@ namespace OpenNest
return new List<(double Start, double End)> { (partStart, partEnd) };
}
/// <summary>
/// Bounds reconstructed as (local minimum + placement) + size can differ
/// from translated material endpoints by a few floating-point steps. This
/// is not a geometry tolerance: cap it well below epsilon so inconsistent
/// arcs still require repair. Do not change shared Part bounds or programs.
/// </summary>
internal static double GetBoundsRoundoff(Part part)
{
var bb = part.BoundingBox;
var magnitude = System.Math.Max(System.Math.Abs(bb.Left), System.Math.Abs(bb.Right));
magnitude = System.Math.Max(magnitude,
System.Math.Max(System.Math.Abs(bb.Bottom), System.Math.Abs(bb.Top)));
magnitude = System.Math.Max(magnitude, System.Math.Max(bb.Length, bb.Width));
magnitude = System.Math.Max(magnitude,
System.Math.Max(System.Math.Abs(part.Location.X), System.Math.Abs(part.Location.Y)));
var step = System.Math.BitIncrement(magnitude) - magnitude;
return double.IsFinite(step) ? System.Math.Min(8 * step, Math.Tolerance.Epsilon / 4) : 0;
}
private List<(double Start, double End)> IntersectPerimeter(
Entity perimeter,
double cutPosition,