feat: implement Polygon.OffsetEntity and use geometric offset for cut-off clearance

Polygon.OffsetEntity now computes proper miter-join offsets using edge
normals and winding direction, with self-intersection cleanup. CutOff
exclusion zones use geometric perimeter offset instead of scalar padding,
giving uniform clearance around parts regardless of cut angle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 23:46:18 -04:00
parent 8efdc8720c
commit 53d24ddaf1
2 changed files with 80 additions and 4 deletions

View File

@@ -129,9 +129,11 @@ namespace OpenNest
private List<(double Start, double End)> IntersectPerimeter(
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
{
var target = OffsetOutward(perimeter, clearance) ?? perimeter;
var usedOffset = target != perimeter;
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
if (!perimeter.Intersects(cutLine, out var pts) || pts.Count < 2)
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
return null;
var coords = pts
@@ -142,13 +144,31 @@ namespace OpenNest
if (coords.Count % 2 != 0)
return null;
var padding = usedOffset ? 0 : clearance;
var result = new List<(double Start, double End)>();
for (var i = 0; i < coords.Count; i += 2)
result.Add((coords[i] - clearance, coords[i + 1] + clearance));
result.Add((coords[i] - padding, coords[i + 1] + padding));
return result;
}
private static Entity OffsetOutward(Entity perimeter, double clearance)
{
if (clearance <= 0)
return null;
try
{
var offset = perimeter.OffsetEntity(clearance, OffsetSide.Left);
offset?.UpdateBounds();
return offset;
}
catch
{
return null;
}
}
private Vector MakePoint(double cutCoord, double lineCoord) =>
Axis == CutOffAxis.Vertical
? new Vector(cutCoord, lineCoord)