refactor(geometry): move polygon offset callers onto ClipperBridge

Per-entity offsetting left spikes and inverted loops wherever a feature is
narrower than the spacing (1.nest), and RemoveSelfIntersections only
caught proper crossings. The callers that already flatten to polygons now
take a single Clipper region offset instead:

- PolygonHelper (BestFit) and PartBoundary use the conservative mode, which
  keeps their never-under-estimate guarantee. PartBoundary also keeps holes
  that appear when a perimeter curls back on itself.
- NestValidator offsets perimeter and cutouts in one region; Clipper drops
  collapsed cutouts, so the collapsed-or-flipped heuristic goes away.
- CutOff.IntersectPerimeter offsets through the bridge. The old
  OffsetEntity(Left) grew CW perimeters but shrank CCW ones, so with the
  plate's perimeter cache a cut-off ran through the part; slots narrower
  than twice the clearance now close up instead of leaving a gap.
- GetOffsetPartLines (3 overloads) and the AddOffset* helpers had no
  callers and are removed, as is EntityView's never-defined DRAW_OFFSET
  block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-23 09:21:48 -04:00
co-authored by Claude Opus 5.5
parent 12f97474b7
commit 9b9386b029
8 changed files with 180 additions and 254 deletions
+52 -9
View File
@@ -18,6 +18,8 @@ namespace OpenNest.Geometry
private const double MiterLimit = 2.0;
private const double ConservativeJoinFactor = 0.25;
/// <summary>
/// Converts a polygon to a Clipper path, dropping the closing vertex and
/// orienting it positive (CCW) or negative (CW).
@@ -90,9 +92,9 @@ namespace OpenNest.Geometry
/// no more than <paramref name="tolerance"/> from the true arc.
/// </summary>
/// <param name="circumscribe">
/// When true, the result never under-estimates the offset: arcs are flattened
/// outside the true curve and the inflation is padded by the chord tolerance
/// and Clipper's rounding.
/// When true, the result never under-estimates the offset: perimeter arcs are
/// flattened outside the true curve, cutout arcs inside it, and the inflation is
/// padded by the round-join chord error and Clipper's rounding.
/// </param>
public static OffsetRegion Offset(
ShapeProfile profile,
@@ -105,8 +107,39 @@ namespace OpenNest.Geometry
return Offset(region, distance, tolerance, circumscribe);
}
/// <summary>
/// Offsets a single closed shape outward, ignoring any cutouts. A perimeter that
/// curls back on itself (a C shape with a narrow mouth) can gain holes.
/// </summary>
public static OffsetRegion OffsetPerimeter(
Shape perimeter,
double distance,
double tolerance,
bool circumscribe = false
)
{
var polygon = perimeter.ToPolygonWithTolerance(tolerance, circumscribe);
return OffsetPerimeter(polygon, distance, tolerance, circumscribe);
}
/// <summary>
/// Offsets a closed polygon outward, whatever its winding.
/// </summary>
public static OffsetRegion OffsetPerimeter(
Polygon perimeter,
double distance,
double tolerance,
bool circumscribe = false
)
{
var region = new PathsD(1);
AddPolygon(region, perimeter, positive: true);
return Offset(region, distance, tolerance, circumscribe);
}
/// <summary>
/// Offsets an already-flattened region (outers positive, holes negative).
/// A distance of zero only unions the region, with no conservative padding.
/// </summary>
public static OffsetRegion Offset(
PathsD region,
@@ -115,13 +148,20 @@ namespace OpenNest.Geometry
bool circumscribe = false
)
{
// Round joins put their vertices on the true arc, so each chord sits inside
// it by up to the join tolerance. In conservative mode, joins use a finer
// tolerance and the inflation is padded by it (plus Clipper's rounding).
var delta = distance;
var joinTolerance = tolerance;
if (circumscribe)
delta += tolerance + 0.5 * System.Math.Pow(10, -Precision);
if (circumscribe && distance > 0)
{
joinTolerance = tolerance * ConservativeJoinFactor;
delta += joinTolerance + 0.5 * System.Math.Pow(10, -Precision);
}
var inflated =
delta == 0
delta <= 0
? Union(region)
: Clipper.InflatePaths(
region,
@@ -130,7 +170,7 @@ namespace OpenNest.Geometry
EndType.Polygon,
MiterLimit,
Precision,
tolerance
joinTolerance
);
var result = new OffsetRegion(new List<Polygon>(), new List<Polygon>());
@@ -167,9 +207,12 @@ namespace OpenNest.Geometry
bool positive
)
{
var polygon = shape.ToPolygonWithTolerance(tolerance, circumscribe);
AddPolygon(region, shape.ToPolygonWithTolerance(tolerance, circumscribe), positive);
}
if (polygon.Vertices.Count < 4)
private static void AddPolygon(PathsD region, Polygon polygon, bool positive)
{
if (polygon.Vertices.Count < 3)
return;
var path = ToPath(polygon, positive);