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
+5 -2
View File
@@ -273,7 +273,10 @@ namespace OpenNest.Geometry
public override Entity OffsetEntity(double distance, OffsetSide side)
{
if (side == OffsetSide.Left && Rotation == RotationType.CCW)
// The center lies to the left of a CCW circle and to the right of a CW one.
var shrinks = (side == OffsetSide.Left) == (Rotation == RotationType.CCW);
if (shrinks)
{
return Radius <= distance
? null
@@ -281,7 +284,7 @@ namespace OpenNest.Geometry
}
else
{
return new Circle(center, Radius + distance) { Layer = Layer };
return new Circle(center, Radius + distance) { Layer = Layer, Rotation = Rotation };
}
}