Compare commits
3 Commits
5d6e018b81
...
3dca25c601
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dca25c601 | |||
| ebc1a5f980 | |||
| b729f92cd6 |
@@ -104,6 +104,39 @@ namespace OpenNest.Geometry
|
|||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solves ray-circle intersection, returning the two parametric t values.
|
||||||
|
/// Returns false if no real intersection exists.
|
||||||
|
/// </summary>
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool SolveRayCircle(
|
||||||
|
double vx, double vy,
|
||||||
|
double cx, double cy, double r,
|
||||||
|
double dirX, double dirY,
|
||||||
|
out double t1, out double t2)
|
||||||
|
{
|
||||||
|
var ox = vx - cx;
|
||||||
|
var oy = vy - cy;
|
||||||
|
|
||||||
|
var a = dirX * dirX + dirY * dirY;
|
||||||
|
var b = 2.0 * (ox * dirX + oy * dirY);
|
||||||
|
var c = ox * ox + oy * oy - r * r;
|
||||||
|
|
||||||
|
var discriminant = b * b - 4.0 * a * c;
|
||||||
|
if (discriminant < 0)
|
||||||
|
{
|
||||||
|
t1 = t2 = double.MaxValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sqrtD = System.Math.Sqrt(discriminant);
|
||||||
|
var inv2a = 1.0 / (2.0 * a);
|
||||||
|
t1 = (-b - sqrtD) * inv2a;
|
||||||
|
t2 = (-b + sqrtD) * inv2a;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes the distance from a point along a direction to an arc.
|
/// Computes the distance from a point along a direction to an arc.
|
||||||
/// Solves ray-circle intersection, then constrains hits to the arc's
|
/// Solves ray-circle intersection, then constrains hits to the arc's
|
||||||
@@ -117,25 +150,9 @@ namespace OpenNest.Geometry
|
|||||||
double startAngle, double endAngle, bool reversed,
|
double startAngle, double endAngle, bool reversed,
|
||||||
double dirX, double dirY)
|
double dirX, double dirY)
|
||||||
{
|
{
|
||||||
// Ray: P = (vx,vy) + t*(dirX,dirY)
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
// Circle: (x-cx)^2 + (y-cy)^2 = r^2
|
|
||||||
var ox = vx - cx;
|
|
||||||
var oy = vy - cy;
|
|
||||||
|
|
||||||
// a = dirX^2 + dirY^2 = 1 for unit direction, but handle general case
|
|
||||||
var a = dirX * dirX + dirY * dirY;
|
|
||||||
var b = 2.0 * (ox * dirX + oy * dirY);
|
|
||||||
var c = ox * ox + oy * oy - r * r;
|
|
||||||
|
|
||||||
var discriminant = b * b - 4.0 * a * c;
|
|
||||||
if (discriminant < 0)
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
|
|
||||||
var sqrtD = System.Math.Sqrt(discriminant);
|
|
||||||
var inv2a = 1.0 / (2.0 * a);
|
|
||||||
var t1 = (-b - sqrtD) * inv2a;
|
|
||||||
var t2 = (-b + sqrtD) * inv2a;
|
|
||||||
|
|
||||||
var best = double.MaxValue;
|
var best = double.MaxValue;
|
||||||
|
|
||||||
if (t1 > -Tolerance.Epsilon)
|
if (t1 > -Tolerance.Epsilon)
|
||||||
@@ -168,27 +185,13 @@ namespace OpenNest.Geometry
|
|||||||
double cx, double cy, double r,
|
double cx, double cy, double r,
|
||||||
double dirX, double dirY)
|
double dirX, double dirY)
|
||||||
{
|
{
|
||||||
var ox = vx - cx;
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
var oy = vy - cy;
|
|
||||||
|
|
||||||
var a = dirX * dirX + dirY * dirY;
|
|
||||||
var b = 2.0 * (ox * dirX + oy * dirY);
|
|
||||||
var c = ox * ox + oy * oy - r * r;
|
|
||||||
|
|
||||||
var discriminant = b * b - 4.0 * a * c;
|
|
||||||
if (discriminant < 0)
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
|
|
||||||
var sqrtD = System.Math.Sqrt(discriminant);
|
if (t1 > Tolerance.Epsilon) return t1;
|
||||||
var t = (-b - sqrtD) / (2.0 * a);
|
if (t1 >= -Tolerance.Epsilon) return 0;
|
||||||
|
if (t2 > Tolerance.Epsilon) return t2;
|
||||||
if (t > Tolerance.Epsilon) return t;
|
if (t2 >= -Tolerance.Epsilon) return 0;
|
||||||
if (t >= -Tolerance.Epsilon) return 0;
|
|
||||||
|
|
||||||
// First root is behind us, try the second
|
|
||||||
t = (-b + sqrtD) / (2.0 * a);
|
|
||||||
if (t > Tolerance.Epsilon) return t;
|
|
||||||
if (t >= -Tolerance.Epsilon) return 0;
|
|
||||||
|
|
||||||
return double.MaxValue;
|
return double.MaxValue;
|
||||||
}
|
}
|
||||||
@@ -200,57 +203,7 @@ namespace OpenNest.Geometry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
|
||||||
|
|
||||||
// Case 1: Each moving vertex -> each stationary edge
|
|
||||||
var movingVertices = new HashSet<Vector>();
|
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingLines[i].pt1);
|
|
||||||
movingVertices.Add(movingLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
|
||||||
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
|
||||||
|
|
||||||
// Sort edges for pruning if not already sorted (usually they aren't here)
|
|
||||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
|
||||||
{
|
|
||||||
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
|
||||||
if (d < minDist) minDist = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
|
||||||
var opposite = OppositeDirection(direction);
|
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
|
||||||
{
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
|
||||||
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
|
||||||
|
|
||||||
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
|
||||||
{
|
|
||||||
var d = OneWayDistance(sv, movingEdges, Vector.Zero, opposite);
|
|
||||||
if (d < minDist) minDist = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
return minDist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -265,21 +218,10 @@ namespace OpenNest.Geometry
|
|||||||
var movingOffset = new Vector(movingDx, movingDy);
|
var movingOffset = new Vector(movingDx, movingDy);
|
||||||
|
|
||||||
// Case 1: Each moving vertex -> each stationary edge
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
var movingVertices = new HashSet<Vector>();
|
var movingVertices = CollectVertices(movingLines, movingOffset);
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingLines[i].pt1 + movingOffset);
|
|
||||||
movingVertices.Add(movingLines[i].pt2 + movingOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
|
var stationaryEdges = ToEdgeArray(stationaryLines);
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
SortEdgesForPruning(stationaryEdges, direction);
|
||||||
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
|
|
||||||
|
|
||||||
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
@@ -289,21 +231,10 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
var opposite = OppositeDirection(direction);
|
var opposite = OppositeDirection(direction);
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||||
for (int i = 0; i < stationaryLines.Count; i++)
|
|
||||||
{
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
|
var movingEdges = ToEdgeArray(movingLines);
|
||||||
for (int i = 0; i < movingLines.Count; i++)
|
SortEdgesForPruning(movingEdges, opposite);
|
||||||
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
|
|
||||||
|
|
||||||
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
|
|
||||||
else
|
|
||||||
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
@@ -342,15 +273,11 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var minDist = double.MaxValue;
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
// Extract unique vertices from moving edges.
|
SortEdgesForPruning(stationaryEdges, direction);
|
||||||
var movingVertices = new HashSet<Vector>();
|
|
||||||
for (var i = 0; i < movingEdges.Length; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingEdges[i].start + movingOffset);
|
|
||||||
movingVertices.Add(movingEdges[i].end + movingOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 1: Each moving vertex -> each stationary edge
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
|
var movingVertices = CollectVertices(movingEdges, movingOffset);
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
||||||
@@ -359,12 +286,9 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
var opposite = OppositeDirection(direction);
|
var opposite = OppositeDirection(direction);
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
SortEdgesForPruning(movingEdges, opposite);
|
||||||
for (var i = 0; i < stationaryEdges.Length; i++)
|
|
||||||
{
|
var stationaryVertices = CollectVertices(stationaryEdges, stationaryOffset);
|
||||||
stationaryVertices.Add(stationaryEdges[i].start + stationaryOffset);
|
|
||||||
stationaryVertices.Add(stationaryEdges[i].end + stationaryOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
@@ -556,12 +480,7 @@ namespace OpenNest.Geometry
|
|||||||
var dirX = direction.X;
|
var dirX = direction.X;
|
||||||
var dirY = direction.Y;
|
var dirY = direction.Y;
|
||||||
|
|
||||||
var movingVertices = new HashSet<Vector>();
|
var movingVertices = CollectVertices(movingLines, Vector.Zero);
|
||||||
for (var i = 0; i < movingLines.Count; i++)
|
|
||||||
{
|
|
||||||
movingVertices.Add(movingLines[i].pt1);
|
|
||||||
movingVertices.Add(movingLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var mv in movingVertices)
|
foreach (var mv in movingVertices)
|
||||||
{
|
{
|
||||||
@@ -576,12 +495,7 @@ namespace OpenNest.Geometry
|
|||||||
var oppX = -dirX;
|
var oppX = -dirX;
|
||||||
var oppY = -dirY;
|
var oppY = -dirY;
|
||||||
|
|
||||||
var stationaryVertices = new HashSet<Vector>();
|
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||||
for (var i = 0; i < stationaryLines.Count; i++)
|
|
||||||
{
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt1);
|
|
||||||
stationaryVertices.Add(stationaryLines[i].pt2);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var sv in stationaryVertices)
|
foreach (var sv in stationaryVertices)
|
||||||
{
|
{
|
||||||
@@ -648,6 +562,57 @@ 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];
|
||||||
|
if (!TryGetCurveParams(me, out var mcx, out var mcy, out var mr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||||
|
{
|
||||||
|
var se = stationaryEntities[j];
|
||||||
|
if (!TryGetCurveParams(se, out var scx, out var scy, out var sr))
|
||||||
|
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;
|
return minDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,6 +693,62 @@ namespace OpenNest.Geometry
|
|||||||
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
|
||||||
|
{
|
||||||
|
var vertices = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < lines.Count; i++)
|
||||||
|
{
|
||||||
|
vertices.Add(lines[i].pt1 + offset);
|
||||||
|
vertices.Add(lines[i].pt2 + offset);
|
||||||
|
}
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
||||||
|
{
|
||||||
|
var vertices = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < edges.Length; i++)
|
||||||
|
{
|
||||||
|
vertices.Add(edges[i].start + offset);
|
||||||
|
vertices.Add(edges[i].end + offset);
|
||||||
|
}
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector start, Vector end)[] ToEdgeArray(List<Line> lines)
|
||||||
|
{
|
||||||
|
var edges = new (Vector start, Vector end)[lines.Count];
|
||||||
|
for (var i = 0; i < lines.Count; i++)
|
||||||
|
edges[i] = (lines[i].pt1, lines[i].pt2);
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction)
|
||||||
|
{
|
||||||
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
|
System.Array.Sort(edges, (a, b) =>
|
||||||
|
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y)));
|
||||||
|
else
|
||||||
|
System.Array.Sort(edges, (a, b) =>
|
||||||
|
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r)
|
||||||
|
{
|
||||||
|
if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
cx = cy = r = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static double BoxProjectionMin(Box box, double dx, double dy)
|
private static double BoxProjectionMin(Box box, double dx, double dy)
|
||||||
{
|
{
|
||||||
var x = dx >= 0 ? box.Left : box.Right;
|
var x = dx >= 0 ? box.Left : box.Right;
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ namespace OpenNest.Engine.BestFit
|
|||||||
var allMovingVerts = ExtractVerticesFromEntities(movingEntities);
|
var allMovingVerts = ExtractVerticesFromEntities(movingEntities);
|
||||||
var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities);
|
var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities);
|
||||||
|
|
||||||
|
var movingCurves = ExtractCurveParams(movingEntities);
|
||||||
|
var stationaryCurves = ExtractCurveParams(stationaryEntities);
|
||||||
|
|
||||||
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
|
||||||
|
|
||||||
foreach (var offset in offsets)
|
foreach (var offset in offsets)
|
||||||
@@ -165,12 +168,84 @@ namespace OpenNest.Engine.BestFit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 3: Curve-to-curve direct distance.
|
||||||
|
// Vertex sampling misses the true contact between two curved entities
|
||||||
|
// when the approach angle doesn't align with a sampled vertex.
|
||||||
|
for (var m = 0; m < movingCurves.Length; m++)
|
||||||
|
{
|
||||||
|
var mc = movingCurves[m];
|
||||||
|
var mcx = mc.Cx + offset.Dx;
|
||||||
|
var mcy = mc.Cy + offset.Dy;
|
||||||
|
|
||||||
|
for (var s = 0; s < stationaryCurves.Length; s++)
|
||||||
|
{
|
||||||
|
var sc = stationaryCurves[s];
|
||||||
|
var d = SpatialQuery.RayCircleDistance(
|
||||||
|
mcx, mcy, sc.Cx, sc.Cy, mc.Radius + sc.Radius, dirX, dirY);
|
||||||
|
|
||||||
|
if (d >= minDist || d == double.MaxValue)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (mc.Entity is Arc || sc.Entity is Arc)
|
||||||
|
{
|
||||||
|
var mx = mcx + d * dirX;
|
||||||
|
var my = mcy + d * dirY;
|
||||||
|
var toCx = sc.Cx - mx;
|
||||||
|
var toCy = sc.Cy - my;
|
||||||
|
|
||||||
|
if (mc.Entity is Arc mArc)
|
||||||
|
{
|
||||||
|
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||||
|
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sc.Entity 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) { results[i] = 0; return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
results[i] = minDist;
|
results[i] = minDist;
|
||||||
});
|
});
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly struct CurveParams
|
||||||
|
{
|
||||||
|
public readonly Entity Entity;
|
||||||
|
public readonly double Cx, Cy, Radius;
|
||||||
|
|
||||||
|
public CurveParams(Entity entity, double cx, double cy, double radius)
|
||||||
|
{
|
||||||
|
Entity = entity;
|
||||||
|
Cx = cx;
|
||||||
|
Cy = cy;
|
||||||
|
Radius = radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CurveParams[] ExtractCurveParams(List<Entity> entities)
|
||||||
|
{
|
||||||
|
var curves = new List<CurveParams>();
|
||||||
|
for (var i = 0; i < entities.Count; i++)
|
||||||
|
{
|
||||||
|
if (entities[i] is Circle circle)
|
||||||
|
curves.Add(new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius));
|
||||||
|
else if (entities[i] is Arc arc)
|
||||||
|
curves.Add(new CurveParams(arc, arc.Center.X, arc.Center.Y, arc.Radius));
|
||||||
|
}
|
||||||
|
return curves.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
private static double RayEntityDistance(
|
private static double RayEntityDistance(
|
||||||
double vx, double vy, Entity entity,
|
double vx, double vy, Entity entity,
|
||||||
double entityOffsetX, double entityOffsetY,
|
double entityOffsetX, double entityOffsetY,
|
||||||
|
|||||||
@@ -119,10 +119,11 @@ namespace OpenNest.Engine.Fill
|
|||||||
var maxCopyDistance = FindMaxPairDistance(
|
var maxCopyDistance = FindMaxPairDistance(
|
||||||
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
|
||||||
|
|
||||||
if (maxCopyDistance < Tolerance.Epsilon)
|
// The copy distance must be at least bboxDim + PartSpacing to prevent
|
||||||
return bboxDim + PartSpacing;
|
// bounding box overlap. Cross-pair slides can underestimate when the
|
||||||
|
// circumscribed polygon boundary overshoots the true arc, creating
|
||||||
return maxCopyDistance;
|
// spurious contacts between diagonal parts in adjacent copies.
|
||||||
|
return System.Math.Max(maxCopyDistance, bboxDim + PartSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using OpenNest;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Fill
|
||||||
|
{
|
||||||
|
public class FillLinearCircleTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public FillLinearCircleTests(ITestOutputHelper output) => _output = output;
|
||||||
|
|
||||||
|
private static Drawing MakeCircleDrawing(double radius)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
var startPt = new Vector(radius * 2, radius); // rightmost point
|
||||||
|
pgm.Codes.Add(new RapidMove(startPt));
|
||||||
|
pgm.Codes.Add(new ArcMove(startPt, new Vector(radius, radius), RotationType.CCW));
|
||||||
|
return new Drawing("circle", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Drawing MakeRingDrawing(double outerRadius, double innerRadius)
|
||||||
|
{
|
||||||
|
var pgm = new Program();
|
||||||
|
// Outer circle (CCW)
|
||||||
|
var outerStart = new Vector(outerRadius * 2, outerRadius);
|
||||||
|
pgm.Codes.Add(new RapidMove(outerStart));
|
||||||
|
pgm.Codes.Add(new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW));
|
||||||
|
// Inner circle (CW = hole)
|
||||||
|
var innerStart = new Vector(outerRadius + innerRadius, outerRadius);
|
||||||
|
pgm.Codes.Add(new RapidMove(innerStart));
|
||||||
|
pgm.Codes.Add(new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW));
|
||||||
|
return new Drawing("ring", pgm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
|
||||||
|
[InlineData(1.0, 0.125)] // 2" diameter circle
|
||||||
|
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
|
||||||
|
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
|
||||||
|
public void CircleFill_OffsetBoundaries_DoNotOverlap(double radius, double spacing)
|
||||||
|
{
|
||||||
|
var drawing = MakeCircleDrawing(radius);
|
||||||
|
var workArea = new Box(0, 0, 48, 48);
|
||||||
|
var engine = new FillLinear(workArea, spacing);
|
||||||
|
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
|
||||||
|
|
||||||
|
_output.WriteLine($"Circle R={radius}, spacing={spacing}: {parts.Count} parts");
|
||||||
|
|
||||||
|
AssertNoOffsetOverlap(parts, spacing, radius * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
|
||||||
|
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
|
||||||
|
public void RingFill_OffsetBoundaries_DoNotOverlap(double outerR, double innerR, double spacing)
|
||||||
|
{
|
||||||
|
var drawing = MakeRingDrawing(outerR, innerR);
|
||||||
|
var workArea = new Box(0, 0, 48, 48);
|
||||||
|
var engine = new FillLinear(workArea, spacing);
|
||||||
|
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
|
||||||
|
|
||||||
|
_output.WriteLine($"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts");
|
||||||
|
|
||||||
|
AssertNoOffsetOverlap(parts, spacing, outerR * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertNoOffsetOverlap(List<Part> parts, double spacing, double expectedDiameter)
|
||||||
|
{
|
||||||
|
if (parts.Count < 2)
|
||||||
|
{
|
||||||
|
_output.WriteLine(" Only 1 part placed, skipping overlap check");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var halfSpacing = spacing / 2;
|
||||||
|
var radius = expectedDiameter / 2;
|
||||||
|
var minGap = double.MaxValue;
|
||||||
|
var violationCount = 0;
|
||||||
|
|
||||||
|
// For circular parts, the center is at Location + (radius, radius).
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
var ci = parts[i].Location + new Vector(radius, radius);
|
||||||
|
|
||||||
|
for (var j = i + 1; j < parts.Count; j++)
|
||||||
|
{
|
||||||
|
var cj = parts[j].Location + new Vector(radius, radius);
|
||||||
|
var centerDist = ci.DistanceTo(cj);
|
||||||
|
|
||||||
|
// Gap between raw circle perimeters
|
||||||
|
var rawGap = centerDist - expectedDiameter;
|
||||||
|
|
||||||
|
// Gap between offset circle perimeters (halfSpacing each side)
|
||||||
|
var offsetGap = centerDist - expectedDiameter - spacing;
|
||||||
|
|
||||||
|
if (rawGap < minGap)
|
||||||
|
minGap = rawGap;
|
||||||
|
|
||||||
|
if (rawGap < spacing - Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
violationCount++;
|
||||||
|
if (violationCount <= 5)
|
||||||
|
{
|
||||||
|
_output.WriteLine($" SPACING VIOLATION parts[{i}] vs parts[{j}]: " +
|
||||||
|
$"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " +
|
||||||
|
$"expected>={spacing:F4}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_output.WriteLine($" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}");
|
||||||
|
|
||||||
|
if (violationCount > 0)
|
||||||
|
{
|
||||||
|
var maxDeficit = spacing - minGap;
|
||||||
|
_output.WriteLine($" Max deficit={maxDeficit:F6}");
|
||||||
|
Assert.Fail($"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user