fix(geometry): harden the arc-preserving per-entity offset

GetOffsetPerimeterEntities/GetOffsetPartEntities feed directional-distance
loops (FillLinear, Compactor, RotationSlideStrategy) that handle arcs
natively. Switching them to Clipper line output (plan option B) made
OpenNest.Tests run 48s -> 8m19s, Fill tests ~3x slower, and broke 20
exact-fit tests through tessellation and conservative padding, so they keep
the per-entity offset (option A), hardened:

- Arc, Circle and Line offsets are now side-symmetric. Right on a CCW arc
  shrank instead of growing, Right on a CW circle grew, and Right on a line
  offset to the left and reversed it. Only Left was used on hot paths, so
  this was latent (SimplifierViewer drew both tolerance bands on one side).
- Shape.OffsetEntity closes every gap between consecutive offset pieces:
  convex non-tangent line/arc corners get a round join about the original
  corner, lines across a collapsed fillet are mitered, and any other gap
  (concave arc corner, collapsed entity) is bridged with a line. Before,
  only line-line corners were joined, so a vertex could slip through.
- Zero-area spikes are left in place and documented: they lie inside the
  offset envelope, which is harmless for directional distance.
- OffsetOutward/OffsetInward become internal; PartGeometry is their only
  caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-23 09:43:02 -04:00
co-authored by Claude Opus 5.5
parent dceb5f7d18
commit 01789c5929
5 changed files with 360 additions and 75 deletions
+14 -10
View File
@@ -443,19 +443,23 @@ namespace OpenNest.Geometry
boundingBox.Width = maxY - minY;
}
/// <summary>
/// Offsets the arc to the given side of its travel direction. The center lies to
/// the left of a CCW arc and to the right of a CW (reversed) one, so the arc grows
/// on the other side and shrinks toward its center. Returns null when it shrinks
/// to nothing.
/// </summary>
public override Entity OffsetEntity(double distance, OffsetSide side)
{
if (side == OffsetSide.Left && reversed)
{
return new Arc(center, radius + distance, startAngle, endAngle, reversed);
}
else
{
if (distance >= radius)
return null;
var grows = (side == OffsetSide.Left) == reversed;
return new Arc(center, radius - distance, startAngle, endAngle, reversed);
}
if (grows)
return new Arc(center, radius + distance, startAngle, endAngle, reversed);
if (distance >= radius)
return null;
return new Arc(center, radius - distance, startAngle, endAngle, reversed);
}
public override Entity OffsetEntity(double distance, Vector pt)