mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 02:41:06 -04:00
fix(cutoff): give uncached CutOff.Regenerate the converted-material envelope
A direct CutOff.Regenerate(plate, settings) call with no perimeter cache argument trusted Part.BoundingBox plus a sub-epsilon roundoff allowance. A legitimately larger arc refit (ConvertProgram fitting an imported arc's center to its chord) can protrude past that cached box by more than the roundoff, so an uncached manual cutoff could emit a segment crossing the part's true converted material. Every production call site (Plate.RegenerateCutOffs, AutomaticCutOffPlanner, the desktop cutoff tools) already builds and passes a Plate.BuildPerimeterCache dictionary, so this only affects a direct/uncached caller. Extract the per-part conversion out of BuildPerimeterCache into Plate.BuildPartPerimeter and use it as a lazy, on-the-fly fallback in CutOff.ComputeSegments whenever no cache dictionary is supplied at all, so the uncached path gets the same conservative envelope as the cached one instead of silently trusting the bare bounding box. Cached behavior (including a cache's own explicit null entries from a failed conversion) is unchanged. OpenNest PM 13f7a685-0564-4cbf-8f46-a7805a0288a0.
This commit is contained in:
1 parent
1db0ef04f0
commit
fb92b75c06
3 files changed
+87
-43
No files matched your search
+15
-2
@@ -144,8 +144,21 @@ namespace OpenNest
|
||||
if (part.BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
|
||||
Entity perimeter = null;
|
||||
cache?.TryGetValue(part, out perimeter);
|
||||
Entity perimeter;
|
||||
if (cache != null)
|
||||
{
|
||||
cache.TryGetValue(part, out perimeter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No pre-built cache: this is a direct/uncached caller (e.g. an
|
||||
// external API consumer calling Regenerate without going through
|
||||
// Plate.RegenerateCutOffs or the UI handlers, which always pass a
|
||||
// cache). Build the same converted-material envelope on the fly so
|
||||
// this path gets the same safety as the cached one, instead of
|
||||
// silently trusting the part's bare BoundingBox.
|
||||
perimeter = Plate.BuildPartPerimeter(part);
|
||||
}
|
||||
var partExclusions = GetPartExclusions(
|
||||
part,
|
||||
perimeter,
|
||||
|
||||
+50
-41
@@ -291,52 +291,61 @@ namespace OpenNest
|
||||
if (part.BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
|
||||
Geometry.Entity perimeter = null;
|
||||
try
|
||||
{
|
||||
var entities = Converters
|
||||
.ConvertProgram.ToGeometry(part.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ToList();
|
||||
|
||||
if (entities.Count > 0)
|
||||
{
|
||||
// Leads are cutting paths in scrap, not boundary edges. Chaining them
|
||||
// into the outline can make a closed part appear open and replace its
|
||||
// recesses with a convex hull. Keep the full geometry for the existing
|
||||
// conservative fallback when the material contour really is open.
|
||||
var outline = entities.Where(e => e.Layer != SpecialLayers.Leadin &&
|
||||
e.Layer != SpecialLayers.Leadout).ToList();
|
||||
var profile = new Geometry.ShapeProfile(outline);
|
||||
|
||||
if (profile.Perimeter.IsClosed())
|
||||
{
|
||||
perimeter = profile.Perimeter;
|
||||
perimeter.Offset(part.Location);
|
||||
}
|
||||
else
|
||||
{
|
||||
var points = entities.CollectPoints();
|
||||
if (points.Count >= 3)
|
||||
{
|
||||
var hull = Geometry.ConvexHull.Compute(points);
|
||||
hull.Offset(part.Location);
|
||||
perimeter = hull;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
perimeter = null;
|
||||
}
|
||||
|
||||
cache[part] = perimeter;
|
||||
cache[part] = BuildPartPerimeter(part);
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts one part's own program into its placed perimeter entity (closed
|
||||
/// profile, or a convex hull fallback for a genuinely open contour). This is
|
||||
/// the same conversion <see cref="BuildPerimeterCache"/> runs per part; callers
|
||||
/// without a pre-built cache (e.g. a direct, uncached <see cref="CutOff.Regenerate"/>
|
||||
/// call) use it to get the same conservative, converted-material envelope instead
|
||||
/// of trusting the part's cached bounding box alone.
|
||||
/// </summary>
|
||||
public static Geometry.Entity BuildPartPerimeter(Part part)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = Converters
|
||||
.ConvertProgram.ToGeometry(part.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ToList();
|
||||
|
||||
if (entities.Count == 0)
|
||||
return null;
|
||||
|
||||
// Leads are cutting paths in scrap, not boundary edges. Chaining them
|
||||
// into the outline can make a closed part appear open and replace its
|
||||
// recesses with a convex hull. Keep the full geometry for the existing
|
||||
// conservative fallback when the material contour really is open.
|
||||
var outline = entities.Where(e => e.Layer != SpecialLayers.Leadin &&
|
||||
e.Layer != SpecialLayers.Leadout).ToList();
|
||||
var profile = new Geometry.ShapeProfile(outline);
|
||||
|
||||
if (profile.Perimeter.IsClosed())
|
||||
{
|
||||
var perimeter = profile.Perimeter;
|
||||
perimeter.Offset(part.Location);
|
||||
return perimeter;
|
||||
}
|
||||
|
||||
var points = entities.CollectPoints();
|
||||
if (points.Count < 3)
|
||||
return null;
|
||||
|
||||
var hull = Geometry.ConvexHull.Compute(points);
|
||||
hull.Offset(part.Location);
|
||||
return hull;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of times to cut the plate.
|
||||
/// </summary>
|
||||
|
||||
@@ -860,6 +860,28 @@ public class AutomaticCutOffPlannerTests
|
||||
Assert.False(arc.Intersects(new Line(segment.From, segment.To), out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeArcRefit_UncachedDirectRegenerate_DoesNotCrossConvertedPerimeter()
|
||||
{
|
||||
// Same large-refit scenario as above, but the caller never built a
|
||||
// perimeter cache at all (OpenNest PM 13f7a685: a direct/uncached
|
||||
// CutOff.Regenerate call used to trust the bare BoundingBox and could
|
||||
// emit a segment crossing the arc's true converted extent).
|
||||
var plate = MakeArcPlate(0.0001);
|
||||
var part = Assert.Single(plate.Parts);
|
||||
var cachedBox = part.BoundingBox;
|
||||
var arc = Assert.Single(ConvertProgram.ToGeometry(part.Program).OfType<Arc>());
|
||||
arc.Offset(part.Location);
|
||||
var x = (cachedBox.Right + arc.BoundingBox.Right) / 2;
|
||||
Assert.True(x > cachedBox.Right); // The unsafe line sits outside the cached bounds.
|
||||
|
||||
var manual = new CutOff(new Vector(x, 0), CutOffAxis.Vertical);
|
||||
manual.Regenerate(plate, new CutOffSettings { PartClearance = 0 }); // No cache argument.
|
||||
|
||||
foreach (var segment in Segments(manual.Drawing.Program))
|
||||
Assert.False(arc.Intersects(new Line(segment.From, segment.To), out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactArcWithinCachedBounds_PlansAndMaterializesSafeCuts()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user