fix: remove self-intersecting loops from polygon offset

Polygon offset at concave corners creates geometry that folds back
through itself. Added RemoveSelfIntersections() to Polygon that
detects non-adjacent edge crossings and removes the smaller loop
at each crossing. Applied to both collision detection and rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 22:23:44 -05:00
parent 08b31d0797
commit 4d270ae68e
3 changed files with 128 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Math;
namespace OpenNest.Geometry
{
@@ -473,6 +474,118 @@ namespace OpenNest.Geometry
get { return EntityType.Polygon; }
}
/// <summary>
/// Removes self-intersecting loops from the polygon by finding non-adjacent
/// edge crossings and keeping the larger contour at each crossing.
/// </summary>
public void RemoveSelfIntersections()
{
if (!IsClosed() || Vertices.Count < 5)
return;
bool found = true;
while (found)
{
found = false;
int n = Vertices.Count - 1; // exclude closing vertex
for (int i = 0; i < n && !found; i++)
{
var a1 = Vertices[i];
var a2 = Vertices[i + 1];
for (int j = i + 2; j < n && !found; j++)
{
// Skip edges that share a vertex (first and last edge)
if (i == 0 && j == n - 1)
continue;
var b1 = Vertices[j];
var b2 = Vertices[j + 1];
Vector pt;
if (SegmentsIntersect(a1, a2, b1, b2, out pt))
{
// Two loops formed by the crossing:
// Loop A: vertices[0..i], pt, vertices[j+1..n-1], close
// Loop B: pt, vertices[i+1..j], close
var loopA = new List<Vector>();
for (int k = 0; k <= i; k++)
loopA.Add(Vertices[k]);
loopA.Add(pt);
for (int k = j + 1; k < n; k++)
loopA.Add(Vertices[k]);
loopA.Add(loopA[0]);
var loopB = new List<Vector>();
loopB.Add(pt);
for (int k = i + 1; k <= j; k++)
loopB.Add(Vertices[k]);
loopB.Add(pt);
var areaA = System.Math.Abs(CalculateArea(loopA));
var areaB = System.Math.Abs(CalculateArea(loopB));
Vertices = areaA >= areaB ? loopA : loopB;
found = true;
}
}
}
}
}
private static bool SegmentsIntersect(Vector a1, Vector a2, Vector b1, Vector b2, out Vector pt)
{
var da = a2 - a1;
var db = b2 - b1;
var cross = da.X * db.Y - da.Y * db.X;
if (cross.IsEqualTo(0.0))
{
pt = Vector.Zero;
return false;
}
var dc = b1 - a1;
var t = (dc.X * db.Y - dc.Y * db.X) / cross;
var u = (dc.X * da.Y - dc.Y * da.X) / cross;
if (t > Tolerance.Epsilon && t < 1.0 - Tolerance.Epsilon &&
u > Tolerance.Epsilon && u < 1.0 - Tolerance.Epsilon)
{
pt = new Vector(a1.X + t * da.X, a1.Y + t * da.Y);
return true;
}
pt = Vector.Zero;
return false;
}
private static double CalculateArea(List<Vector> vertices)
{
double xsum = 0;
double ysum = 0;
for (int i = 0; i < vertices.Count - 1; i++)
{
var current = vertices[i];
var next = vertices[i + 1];
xsum += current.X * next.Y;
ysum += current.Y * next.X;
}
return (xsum - ysum) * 0.5;
}
internal void Cleanup()
{
for (int i = Vertices.Count - 1; i > 0; i--)

View File

@@ -789,6 +789,7 @@ namespace OpenNest
continue;
var polygon = offsetEntity.ToPolygonWithTolerance(PushChordTolerance);
polygon.RemoveSelfIntersections();
polygon.Offset(part.Location);
lines.AddRange(polygon.ToLines());
}
@@ -810,6 +811,7 @@ namespace OpenNest
continue;
var polygon = offsetEntity.ToPolygonWithTolerance(PushChordTolerance);
polygon.RemoveSelfIntersections();
polygon.Offset(part.Location);
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
}

View File

@@ -459,9 +459,20 @@ namespace OpenNest.Controls
if (offsetEntity == null)
continue;
offsetEntity.Offset(part.Location);
var polygon = offsetEntity.ToPolygonWithTolerance(0.01);
polygon.RemoveSelfIntersections();
polygon.Offset(part.Location);
var path = GraphicsHelper.GetGraphicsPath(offsetEntity);
if (polygon.Vertices.Count < 2)
continue;
var pts = new PointF[polygon.Vertices.Count];
for (int j = 0; j < pts.Length; j++)
pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y);
var path = new GraphicsPath();
path.AddLines(pts);
path.Transform(Matrix);
g.DrawPath(offsetPen, path);
path.Dispose();