From b729f92cd6d1a5b9c839adc591b5780fd9cb1249 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 7 Apr 2026 22:51:09 -0400 Subject: [PATCH] fix: correct compactor circle-to-circle directional distance The vertex-to-entity approach in DirectionalDistance only sampled 4 cardinal points per circle, missing the true closest contact when circles are offset diagonally from the push direction. This caused the distance to be overestimated, pushing circles too far and creating overlap that worsened with distance from center. Add a curve-to-curve pass that computes exact contact distance by treating the problem as a ray from one center to an expanded circle (radius = r1 + r2) at the other center. Includes arc angular range validation for arc-to-arc and arc-to-circle cases. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Geometry/SpatialQuery.cs | 69 ++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs index df457ac..32b5740 100644 --- a/OpenNest.Core/Geometry/SpatialQuery.cs +++ b/OpenNest.Core/Geometry/SpatialQuery.cs @@ -648,6 +648,75 @@ namespace OpenNest.Geometry } } + // Phase 3: Curve-to-curve direct distance. + // The vertex-to-entity approach misses the closest contact between two + // curved entities (circles/arcs) because only a few cardinal vertices are + // sampled. The true closest contact along the push direction is found by + // treating it as a ray from one center to an expanded circle at the other + // center (radius = r1 + r2). + for (var i = 0; i < movingEntities.Count; i++) + { + var me = movingEntities[i]; + double mcx, mcy, mr; + + if (me is Circle mc) + { + mcx = mc.Center.X; mcy = mc.Center.Y; mr = mc.Radius; + } + else if (me is Arc ma) + { + mcx = ma.Center.X; mcy = ma.Center.Y; mr = ma.Radius; + } + else continue; + + for (var j = 0; j < stationaryEntities.Count; j++) + { + var se = stationaryEntities[j]; + double scx, scy, sr; + + if (se is Circle sc) + { + scx = sc.Center.X; scy = sc.Center.Y; sr = sc.Radius; + } + else if (se is Arc sa) + { + scx = sa.Center.X; scy = sa.Center.Y; sr = sa.Radius; + } + else continue; + + var d = RayCircleDistance(mcx, mcy, scx, scy, mr + sr, dirX, dirY); + + if (d >= minDist || d == double.MaxValue) + continue; + + // For arcs, verify the contact point falls within both arcs' angular ranges. + if (me is Arc || se is Arc) + { + var mx = mcx + d * dirX; + var my = mcy + d * dirY; + var toCx = scx - mx; + var toCy = scy - my; + + if (me is Arc mArc) + { + var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx)); + if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed)) + continue; + } + + if (se is Arc sArc) + { + var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx)); + if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed)) + continue; + } + } + + minDist = d; + if (d <= 0) return 0; + } + } + return minDist; }