mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-12 09:01:09 -05:00
Geometry, nesting and import code now call PortableMath instead of System.Math for Sin, Cos, Tan, Atan, Atan2, Asin and Acos (Core, Engine, IO, Data; 269 calls in 64 files). A test fails if those projects call the runtime's trig again. With the runtime's trig, Windows and Linux disagreed in the last bit at non-quarter angles and the same job could nest differently: the learned-angle nests (a cross fixture) placed 23 parts in Strip on Windows and 28 on Linux. Now every Engine.Tests pin gives the same layout on both (3600 passing on Linux and on DesktopPC), Linux layouts are unchanged, and the learned-angle rows are re-pinned to the shared layouts, which pass the overlap and work-area checks, and run on every platform again. Fixes Project Memory 518f3b58.
1021 lines
36 KiB
C#
1021 lines
36 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Geometry
|
|
{
|
|
public static class SpatialQuery
|
|
{
|
|
/// <summary>
|
|
/// Finds the distance from a vertex to a line segment along a push axis.
|
|
/// Returns double.MaxValue if the ray does not hit the segment.
|
|
/// </summary>
|
|
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
|
|
{
|
|
return RayEdgeDistance(
|
|
vertex.X,
|
|
vertex.Y,
|
|
edge.pt1.X,
|
|
edge.pt1.Y,
|
|
edge.pt2.X,
|
|
edge.pt2.Y,
|
|
direction
|
|
);
|
|
}
|
|
|
|
[System.Runtime.CompilerServices.MethodImpl(
|
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
|
)]
|
|
private static double RayEdgeDistance(
|
|
double vx,
|
|
double vy,
|
|
double p1x,
|
|
double p1y,
|
|
double p2x,
|
|
double p2y,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case PushDirection.Left:
|
|
case PushDirection.Right:
|
|
{
|
|
var dy = p2y - p1y;
|
|
if (System.Math.Abs(dy) < Tolerance.Epsilon)
|
|
return double.MaxValue;
|
|
|
|
var t = (vy - p1y) / dy;
|
|
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
|
return double.MaxValue;
|
|
|
|
var ix = p1x + t * (p2x - p1x);
|
|
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
|
|
|
|
if (dist > Tolerance.Epsilon)
|
|
return dist;
|
|
if (dist >= -Tolerance.Epsilon)
|
|
return 0;
|
|
return double.MaxValue;
|
|
}
|
|
|
|
case PushDirection.Down:
|
|
case PushDirection.Up:
|
|
{
|
|
var dx = p2x - p1x;
|
|
if (System.Math.Abs(dx) < Tolerance.Epsilon)
|
|
return double.MaxValue;
|
|
|
|
var t = (vx - p1x) / dx;
|
|
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
|
return double.MaxValue;
|
|
|
|
var iy = p1y + t * (p2y - p1y);
|
|
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
|
|
|
|
if (dist > Tolerance.Epsilon)
|
|
return dist;
|
|
if (dist >= -Tolerance.Epsilon)
|
|
return 0;
|
|
return double.MaxValue;
|
|
}
|
|
|
|
default:
|
|
return double.MaxValue;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generalized ray-edge distance along an arbitrary unit direction vector.
|
|
/// Returns double.MaxValue if the ray does not hit the segment.
|
|
/// </summary>
|
|
[System.Runtime.CompilerServices.MethodImpl(
|
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
|
)]
|
|
public static double RayEdgeDistance(
|
|
double vx,
|
|
double vy,
|
|
double p1x,
|
|
double p1y,
|
|
double p2x,
|
|
double p2y,
|
|
double dirX,
|
|
double dirY
|
|
)
|
|
{
|
|
var ex = p2x - p1x;
|
|
var ey = p2y - p1y;
|
|
|
|
var det = ex * dirY - ey * dirX;
|
|
if (System.Math.Abs(det) < Tolerance.Epsilon)
|
|
return double.MaxValue;
|
|
|
|
var dvx = p1x - vx;
|
|
var dvy = p1y - vy;
|
|
|
|
var t = (ex * dvy - ey * dvx) / det;
|
|
if (t < -Tolerance.Epsilon)
|
|
return double.MaxValue;
|
|
|
|
var s = (dirX * dvy - dirY * dvx) / det;
|
|
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
|
|
return double.MaxValue;
|
|
|
|
if (t > Tolerance.Epsilon)
|
|
return t;
|
|
if (t >= -Tolerance.Epsilon)
|
|
return 0;
|
|
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
|
|
)
|
|
{
|
|
return SlideCurvePrimitives.SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out t1, out t2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the distance from a point along a direction to an arc.
|
|
/// Solves ray-circle intersection, then constrains hits to the arc's
|
|
/// angular span. Returns double.MaxValue if no hit.
|
|
/// </summary>
|
|
[System.Runtime.CompilerServices.MethodImpl(
|
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
|
)]
|
|
public static double RayArcDistance(
|
|
double vx,
|
|
double vy,
|
|
double cx,
|
|
double cy,
|
|
double r,
|
|
double startAngle,
|
|
double endAngle,
|
|
bool reversed,
|
|
double dirX,
|
|
double dirY
|
|
)
|
|
{
|
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
|
return double.MaxValue;
|
|
|
|
var best = double.MaxValue;
|
|
|
|
if (t1 > -Tolerance.Epsilon)
|
|
{
|
|
var hitAngle = Angle.NormalizeRad(
|
|
PortableMath.Atan2(vy + t1 * dirY - cy, vx + t1 * dirX - cx)
|
|
);
|
|
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
|
best = t1 > Tolerance.Epsilon ? t1 : 0;
|
|
}
|
|
|
|
if (t2 > -Tolerance.Epsilon && t2 < best)
|
|
{
|
|
var hitAngle = Angle.NormalizeRad(
|
|
PortableMath.Atan2(vy + t2 * dirY - cy, vx + t2 * dirX - cx)
|
|
);
|
|
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
|
best = t2 > Tolerance.Epsilon ? t2 : 0;
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the distance from a point along a direction to a full circle.
|
|
/// Returns double.MaxValue if no hit.
|
|
/// </summary>
|
|
[System.Runtime.CompilerServices.MethodImpl(
|
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
|
|
)]
|
|
public static double RayCircleDistance(
|
|
double vx,
|
|
double vy,
|
|
double cx,
|
|
double cy,
|
|
double r,
|
|
double dirX,
|
|
double dirY
|
|
)
|
|
{
|
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
|
return double.MaxValue;
|
|
|
|
if (t1 > Tolerance.Epsilon)
|
|
return t1;
|
|
if (t1 >= -Tolerance.Epsilon)
|
|
return 0;
|
|
if (t2 > Tolerance.Epsilon)
|
|
return t2;
|
|
if (t2 >= -Tolerance.Epsilon)
|
|
return 0;
|
|
|
|
return double.MaxValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the first external or internal tangency along a unit direction, or
|
|
/// double.MaxValue if none exists. Centers must be in the same world frame;
|
|
/// radii must be nonnegative. An optional arc constrains the contact angle
|
|
/// (null means a full circle); only its angular range is used.
|
|
/// Endpoint contacts and coincident equal-radius curves remain the caller's
|
|
/// vertex-to-entity responsibility, so this is not a complete collision test.
|
|
/// </summary>
|
|
public static double CurveTangencyDistance(
|
|
double movingCx,
|
|
double movingCy,
|
|
double movingRadius,
|
|
Arc movingArc,
|
|
double stationaryCx,
|
|
double stationaryCy,
|
|
double stationaryRadius,
|
|
Arc stationaryArc,
|
|
double dirX,
|
|
double dirY
|
|
)
|
|
{
|
|
var best = double.MaxValue;
|
|
for (var kind = 0; kind < 2; kind++)
|
|
{
|
|
var internalContact = kind == 1;
|
|
var radius = internalContact
|
|
? System.Math.Abs(movingRadius - stationaryRadius)
|
|
: movingRadius + stationaryRadius;
|
|
|
|
// Equal-radius internal contact has coincident centers, not a unique
|
|
// tangent point. Endpoints detect any overlap of those angular spans.
|
|
if (radius == 0)
|
|
continue;
|
|
|
|
if (!SolveRayCircle(
|
|
movingCx, movingCy, stationaryCx, stationaryCy, radius,
|
|
dirX, dirY, out var t1, out var t2))
|
|
continue;
|
|
|
|
// The nearer center-circle root can be outside an arc while the farther
|
|
// root is its first contact. Check the actual tangent point at BOTH roots.
|
|
for (var root = 0; root < 2; root++)
|
|
{
|
|
var t = root == 0 ? t1 : t2;
|
|
if (t < -Tolerance.Epsilon || t >= best)
|
|
continue;
|
|
|
|
var toX = stationaryCx - (movingCx + t * dirX);
|
|
var toY = stationaryCy - (movingCy + t * dirY);
|
|
var movingSign = internalContact && movingRadius < stationaryRadius ? -1 : 1;
|
|
var stationarySign = internalContact ? movingSign : -1;
|
|
if (!ContainsContactAngle(movingArc, movingRadius, movingSign * toX, movingSign * toY)
|
|
|| !ContainsContactAngle(stationaryArc, stationaryRadius, stationarySign * toX, stationarySign * toY))
|
|
continue;
|
|
|
|
best = t > Tolerance.Epsilon ? t : 0;
|
|
if (best == 0)
|
|
return 0;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
private static bool ContainsContactAngle(Arc arc, double radius, double x, double y)
|
|
{
|
|
return SlideCurvePrimitives.ContainsContactAngle(arc, radius, x, y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the translation distance along a push direction before any edge of
|
|
/// movingLines first blocks against an edge of stationaryLines. A contact that
|
|
/// the push slides along or leaves does not block (see <see cref="SlideContact"/>).
|
|
/// Returns double.MaxValue if no collision path exists.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Line> movingLines,
|
|
List<Line> stationaryLines,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the directional distance with the moving lines translated
|
|
/// by (movingDx, movingDy) without creating new Line objects.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Line> movingLines,
|
|
double movingDx,
|
|
double movingDy,
|
|
List<Line> stationaryLines,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
return DirectionalDistance(
|
|
movingLines,
|
|
movingDx,
|
|
movingDy,
|
|
stationaryLines,
|
|
direction,
|
|
SlideContactClassifier.FromLines(
|
|
movingLines,
|
|
new Vector(movingDx, movingDy),
|
|
stationaryLines,
|
|
Vector.Zero
|
|
)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="DirectionalDistance(List{Line}, double, double, List{Line}, PushDirection)"/>
|
|
/// with caller-supplied contact topology, for inputs that are not complete closed
|
|
/// boundaries (for example direction-filtered edges).
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Line> movingLines,
|
|
double movingDx,
|
|
double movingDy,
|
|
List<Line> stationaryLines,
|
|
PushDirection direction,
|
|
SlideContactClassifier contacts
|
|
)
|
|
{
|
|
var movingOffset = new Vector(movingDx, movingDy);
|
|
var movingVertices = CollectVertices(movingLines, movingOffset).ToArray();
|
|
|
|
var stationaryEdges = ToEdgeArray(stationaryLines);
|
|
SortEdgesForPruning(stationaryEdges, direction);
|
|
|
|
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero).ToArray();
|
|
|
|
var movingEdges = ToEdgeArray(movingLines);
|
|
SortEdgesForPruning(movingEdges, OppositeDirection(direction));
|
|
|
|
var source = new AxisSlideEvents(
|
|
movingEdges,
|
|
movingOffset,
|
|
movingVertices,
|
|
stationaryEdges,
|
|
Vector.Zero,
|
|
stationaryVertices,
|
|
direction
|
|
);
|
|
var unit = DirectionToOffset(direction, 1.0);
|
|
return SlideResolver.FirstBlocking(ref source, contacts, unit.X, unit.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Packs line segments into a flat double array [x1,y1,x2,y2, ...] for GPU transfer.
|
|
/// </summary>
|
|
public static double[] FlattenLines(List<Line> lines)
|
|
{
|
|
var result = new double[lines.Count * 4];
|
|
for (int i = 0; i < lines.Count; i++)
|
|
{
|
|
var line = lines[i];
|
|
result[i * 4] = line.pt1.X;
|
|
result[i * 4 + 1] = line.pt1.Y;
|
|
result[i * 4 + 2] = line.pt2.X;
|
|
result[i * 4 + 3] = line.pt2.Y;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the blocking directional distance using raw edge arrays and location
|
|
/// offsets. Sorts both edge arrays in place for pruning.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
(Vector start, Vector end)[] movingEdges,
|
|
Vector movingOffset,
|
|
(Vector start, Vector end)[] stationaryEdges,
|
|
Vector stationaryOffset,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
return DirectionalDistance(
|
|
movingEdges,
|
|
movingOffset,
|
|
stationaryEdges,
|
|
stationaryOffset,
|
|
direction,
|
|
SlideContactClassifier.FromEdges(
|
|
movingEdges,
|
|
movingOffset,
|
|
stationaryEdges,
|
|
stationaryOffset
|
|
)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Edge-array overload with caller-supplied contact topology. The classifier's
|
|
/// origins must match <paramref name="movingOffset"/> and
|
|
/// <paramref name="stationaryOffset"/> in the frame of its boundaries.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
(Vector start, Vector end)[] movingEdges,
|
|
Vector movingOffset,
|
|
(Vector start, Vector end)[] stationaryEdges,
|
|
Vector stationaryOffset,
|
|
PushDirection direction,
|
|
SlideContactClassifier contacts
|
|
)
|
|
{
|
|
SortEdgesForPruning(stationaryEdges, direction);
|
|
var movingVertices = CollectVertices(movingEdges, movingOffset).ToArray();
|
|
|
|
SortEdgesForPruning(movingEdges, OppositeDirection(direction));
|
|
var stationaryVertices = CollectVertices(stationaryEdges, stationaryOffset).ToArray();
|
|
|
|
var source = new AxisSlideEvents(
|
|
movingEdges,
|
|
movingOffset,
|
|
movingVertices,
|
|
stationaryEdges,
|
|
stationaryOffset,
|
|
stationaryVertices,
|
|
direction
|
|
);
|
|
var unit = DirectionToOffset(direction, 1.0);
|
|
return SlideResolver.FirstBlocking(ref source, contacts, unit.X, unit.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Nearest raw hit from one vertex along a push direction against edges sorted for
|
|
/// pruning. This is a first-touch primitive; it does not classify sliding contacts.
|
|
/// </summary>
|
|
public static double OneWayDistance(
|
|
Vector vertex,
|
|
(Vector start, Vector end)[] edges,
|
|
Vector edgeOffset,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
var minDist = double.MaxValue;
|
|
var vx = vertex.X;
|
|
var vy = vertex.Y;
|
|
var horizontal = IsHorizontalDirection(direction);
|
|
|
|
// Pruning: edges are sorted by their perpendicular min-coordinate.
|
|
// For horizontal push, prune by Y range; for vertical push, prune by X range.
|
|
for (var i = 0; i < edges.Length; i++)
|
|
{
|
|
var e1 = edges[i].start + edgeOffset;
|
|
var e2 = edges[i].end + edgeOffset;
|
|
|
|
double perpValue,
|
|
edgeMin,
|
|
edgeMax;
|
|
if (horizontal)
|
|
{
|
|
perpValue = vy;
|
|
edgeMin = e1.Y < e2.Y ? e1.Y : e2.Y;
|
|
edgeMax = e1.Y > e2.Y ? e1.Y : e2.Y;
|
|
}
|
|
else
|
|
{
|
|
perpValue = vx;
|
|
edgeMin = e1.X < e2.X ? e1.X : e2.X;
|
|
edgeMax = e1.X > e2.X ? e1.X : e2.X;
|
|
}
|
|
|
|
// Since edges are sorted by edgeMin, if perpValue < edgeMin, all subsequent edges are also past.
|
|
if (perpValue < edgeMin - Tolerance.Epsilon)
|
|
break;
|
|
|
|
if (perpValue > edgeMax + Tolerance.Epsilon)
|
|
continue;
|
|
|
|
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
|
|
if (d < minDist)
|
|
minDist = d;
|
|
}
|
|
|
|
return minDist;
|
|
}
|
|
|
|
public static PushDirection OppositeDirection(PushDirection direction)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case PushDirection.Left:
|
|
return PushDirection.Right;
|
|
case PushDirection.Right:
|
|
return PushDirection.Left;
|
|
case PushDirection.Up:
|
|
return PushDirection.Down;
|
|
case PushDirection.Down:
|
|
return PushDirection.Up;
|
|
default:
|
|
return direction;
|
|
}
|
|
}
|
|
|
|
public static bool IsHorizontalDirection(PushDirection direction)
|
|
{
|
|
return direction is PushDirection.Left or PushDirection.Right;
|
|
}
|
|
|
|
public static double EdgeDistance(Box box, Box boundary, PushDirection direction)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case PushDirection.Left:
|
|
return box.Left - boundary.Left;
|
|
case PushDirection.Right:
|
|
return boundary.Right - box.Right;
|
|
case PushDirection.Up:
|
|
return boundary.Top - box.Top;
|
|
case PushDirection.Down:
|
|
return box.Bottom - boundary.Bottom;
|
|
default:
|
|
return double.MaxValue;
|
|
}
|
|
}
|
|
|
|
public static Vector DirectionToOffset(PushDirection direction, double distance)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case PushDirection.Left:
|
|
return new Vector(-distance, 0);
|
|
case PushDirection.Right:
|
|
return new Vector(distance, 0);
|
|
case PushDirection.Up:
|
|
return new Vector(0, distance);
|
|
case PushDirection.Down:
|
|
return new Vector(0, -distance);
|
|
default:
|
|
return new Vector();
|
|
}
|
|
}
|
|
|
|
public static double DirectionalGap(Box from, Box to, PushDirection direction)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case PushDirection.Left:
|
|
return from.Left - to.Right;
|
|
case PushDirection.Right:
|
|
return to.Left - from.Right;
|
|
case PushDirection.Up:
|
|
return to.Bottom - from.Top;
|
|
case PushDirection.Down:
|
|
return from.Bottom - to.Top;
|
|
default:
|
|
return double.MaxValue;
|
|
}
|
|
}
|
|
|
|
#region Generalized direction (Vector) overloads
|
|
|
|
/// <summary>
|
|
/// Computes how far a box can travel along the given unit direction
|
|
/// before exiting the boundary box.
|
|
/// </summary>
|
|
public static double EdgeDistance(Box box, Box boundary, Vector direction)
|
|
{
|
|
var dist = double.MaxValue;
|
|
|
|
if (direction.X < -Tolerance.Epsilon)
|
|
{
|
|
var d = (box.Left - boundary.Left) / -direction.X;
|
|
if (d < dist)
|
|
dist = d;
|
|
}
|
|
else if (direction.X > Tolerance.Epsilon)
|
|
{
|
|
var d = (boundary.Right - box.Right) / direction.X;
|
|
if (d < dist)
|
|
dist = d;
|
|
}
|
|
|
|
if (direction.Y < -Tolerance.Epsilon)
|
|
{
|
|
var d = (box.Bottom - boundary.Bottom) / -direction.Y;
|
|
if (d < dist)
|
|
dist = d;
|
|
}
|
|
else if (direction.Y > Tolerance.Epsilon)
|
|
{
|
|
var d = (boundary.Top - box.Top) / direction.Y;
|
|
if (d < dist)
|
|
dist = d;
|
|
}
|
|
|
|
return dist < 0 ? 0 : dist;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the directional gap between two boxes along an arbitrary unit direction.
|
|
/// Positive means 'to' is ahead of 'from' in the push direction.
|
|
/// </summary>
|
|
public static double DirectionalGap(Box from, Box to, Vector direction)
|
|
{
|
|
var fromMax = BoxProjectionMax(from, direction.X, direction.Y);
|
|
var toMin = BoxProjectionMin(to, direction.X, direction.Y);
|
|
return toMin - fromMax;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if two boxes overlap when projected onto the axis
|
|
/// perpendicular to the given unit direction.
|
|
/// </summary>
|
|
public static bool PerpendicularOverlap(Box a, Box b, Vector direction)
|
|
{
|
|
var px = -direction.Y;
|
|
var py = direction.X;
|
|
|
|
var aMin = BoxProjectionMin(a, px, py);
|
|
var aMax = BoxProjectionMax(a, px, py);
|
|
var bMin = BoxProjectionMin(b, px, py);
|
|
var bMax = BoxProjectionMax(b, px, py);
|
|
|
|
return aMin <= bMax + Tolerance.Epsilon && bMin <= aMax + Tolerance.Epsilon;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the translation distance along an arbitrary unit direction before any
|
|
/// edge of movingLines first blocks against an edge of stationaryLines.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Line> movingLines,
|
|
List<Line> stationaryLines,
|
|
Vector direction
|
|
)
|
|
{
|
|
return DirectionalDistance(
|
|
movingLines,
|
|
stationaryLines,
|
|
direction,
|
|
SlideContactClassifier.FromLines(
|
|
movingLines,
|
|
Vector.Zero,
|
|
stationaryLines,
|
|
Vector.Zero
|
|
)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="DirectionalDistance(List{Line}, List{Line}, Vector)"/> with
|
|
/// caller-supplied contact topology.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Line> movingLines,
|
|
List<Line> stationaryLines,
|
|
Vector direction,
|
|
SlideContactClassifier contacts
|
|
)
|
|
{
|
|
var source = new LineSlideEvents(
|
|
movingLines,
|
|
CollectVertices(movingLines, Vector.Zero).ToArray(),
|
|
0,
|
|
0,
|
|
stationaryLines,
|
|
CollectVertices(stationaryLines, Vector.Zero).ToArray(),
|
|
direction.X,
|
|
direction.Y
|
|
);
|
|
return SlideResolver.FirstBlocking(ref source, contacts, direction.X, direction.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the minimum translation distance along a push direction
|
|
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
|
/// stationaryEntities. Delegates to the Vector-based overload.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Entity> movingEntities,
|
|
List<Entity> stationaryEntities,
|
|
PushDirection direction
|
|
)
|
|
{
|
|
return DirectionalDistance(
|
|
movingEntities,
|
|
stationaryEntities,
|
|
DirectionToOffset(direction, 1.0)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the translation distance along an arbitrary unit direction before any
|
|
/// vertex/edge of movingEntities first blocks against stationaryEntities. Works with
|
|
/// native Line, Arc, and Circle entities without tessellation. A contact that the
|
|
/// push slides along or leaves does not block (see <see cref="SlideContact"/>).
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Entity> movingEntities,
|
|
List<Entity> stationaryEntities,
|
|
Vector direction
|
|
)
|
|
{
|
|
return DirectionalDistance(
|
|
movingEntities,
|
|
stationaryEntities,
|
|
direction,
|
|
new SlideContactClassifier(movingEntities, stationaryEntities)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="DirectionalDistance(List{Entity}, List{Entity}, Vector)"/> with
|
|
/// caller-supplied contact topology.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Entity> movingEntities,
|
|
List<Entity> stationaryEntities,
|
|
Vector direction,
|
|
SlideContactClassifier contacts
|
|
)
|
|
{
|
|
return DirectionalDistance(
|
|
movingEntities,
|
|
ExtractEntityVertices(movingEntities),
|
|
stationaryEntities,
|
|
ExtractEntityVertices(stationaryEntities),
|
|
direction,
|
|
contacts
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="DirectionalDistance(List{Entity}, List{Entity}, Vector, SlideContactClassifier)"/>
|
|
/// with each list's <see cref="ExtractEntityVertices"/> result supplied by a caller that
|
|
/// slides one list against several others. The vertices must come from these exact lists.
|
|
/// </summary>
|
|
public static double DirectionalDistance(
|
|
List<Entity> movingEntities,
|
|
Vector[] movingVertices,
|
|
List<Entity> stationaryEntities,
|
|
Vector[] stationaryVertices,
|
|
Vector direction,
|
|
SlideContactClassifier contacts
|
|
)
|
|
{
|
|
// Phases: vertex rays both ways, arc-to-line closest points (vertex sampling
|
|
// misses interior arc contact), then native curve tangency.
|
|
var source = new EntitySlideEvents(
|
|
movingEntities,
|
|
movingVertices,
|
|
0,
|
|
0,
|
|
stationaryEntities,
|
|
stationaryVertices,
|
|
direction.X,
|
|
direction.Y,
|
|
arcToLine: true
|
|
);
|
|
return SlideResolver.FirstBlocking(ref source, contacts, direction.X, direction.Y);
|
|
}
|
|
|
|
public static Vector[] ExtractEntityVertices(List<Entity> entities)
|
|
{
|
|
// Presized: a set without removals enumerates in insertion order whatever its
|
|
// capacity, so this only saves the rehashes of growing from empty.
|
|
var vertices = new HashSet<Vector>(entities.Count * 2);
|
|
|
|
for (var i = 0; i < entities.Count; i++)
|
|
{
|
|
var entity = entities[i];
|
|
|
|
if (entity is Line line)
|
|
{
|
|
vertices.Add(line.pt1);
|
|
vertices.Add(line.pt2);
|
|
}
|
|
else if (entity is Arc arc)
|
|
{
|
|
vertices.Add(arc.StartPoint());
|
|
vertices.Add(arc.EndPoint());
|
|
AddArcExtremeVertices(vertices, arc);
|
|
}
|
|
else if (entity is Circle circle)
|
|
{
|
|
vertices.Add(new Vector(circle.Center.X + circle.Radius, circle.Center.Y));
|
|
vertices.Add(new Vector(circle.Center.X - circle.Radius, circle.Center.Y));
|
|
vertices.Add(new Vector(circle.Center.X, circle.Center.Y + circle.Radius));
|
|
vertices.Add(new Vector(circle.Center.X, circle.Center.Y - circle.Radius));
|
|
}
|
|
}
|
|
|
|
return vertices.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the cardinal extreme points of an arc (0°, 90°, 180°, 270°)
|
|
/// if they fall within the arc's angular span.
|
|
/// </summary>
|
|
internal static void AddArcExtremeVertices(ICollection<Vector> points, Arc arc)
|
|
{
|
|
var a1 = arc.StartAngle;
|
|
var a2 = arc.EndAngle;
|
|
|
|
if (arc.IsReversed)
|
|
Generic.Swap(ref a1, ref a2);
|
|
|
|
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
|
|
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
|
|
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
|
|
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
|
|
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
|
|
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
|
|
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
|
|
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
|
}
|
|
|
|
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
|
|
{
|
|
return CollectVertices(ToEdgeArray(lines), offset);
|
|
}
|
|
|
|
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 double BoxProjectionMin(Box box, double dx, double dy)
|
|
{
|
|
var x = dx >= 0 ? box.Left : box.Right;
|
|
var y = dy >= 0 ? box.Bottom : box.Top;
|
|
return x * dx + y * dy;
|
|
}
|
|
|
|
private static double BoxProjectionMax(Box box, double dx, double dy)
|
|
{
|
|
var x = dx >= 0 ? box.Right : box.Left;
|
|
var y = dy >= 0 ? box.Top : box.Bottom;
|
|
return x * dx + y * dy;
|
|
}
|
|
|
|
#endregion
|
|
|
|
public static Box GetLargestBoxVertically(Vector pt, Box bounds, IEnumerable<Box> boxes)
|
|
{
|
|
var verticalBoxes = boxes.Where(b => !(b.Left > pt.X || b.Right < pt.X)).ToList();
|
|
|
|
if (!FindVerticalLimits(pt, bounds, verticalBoxes, out var top, out var btm))
|
|
return Box.Empty;
|
|
|
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList();
|
|
|
|
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
|
|
return Box.Empty;
|
|
|
|
return new Box(lft, btm, rgt - lft, top - btm);
|
|
}
|
|
|
|
public static Box GetLargestBoxHorizontally(Vector pt, Box bounds, IEnumerable<Box> boxes)
|
|
{
|
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom > pt.Y || b.Top < pt.Y)).ToList();
|
|
|
|
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
|
|
return Box.Empty;
|
|
|
|
var verticalBoxes = boxes.Where(b => !(b.Left >= rgt || b.Right <= lft)).ToList();
|
|
|
|
if (!FindVerticalLimits(pt, bounds, verticalBoxes, out var top, out var btm))
|
|
return Box.Empty;
|
|
|
|
return new Box(lft, btm, rgt - lft, top - btm);
|
|
}
|
|
|
|
private static bool FindVerticalLimits(
|
|
Vector pt,
|
|
Box bounds,
|
|
List<Box> boxes,
|
|
out double top,
|
|
out double btm
|
|
)
|
|
{
|
|
top = double.MaxValue;
|
|
btm = double.MinValue;
|
|
|
|
foreach (var box in boxes)
|
|
{
|
|
var boxBtm = box.Bottom;
|
|
var boxTop = box.Top;
|
|
|
|
if (boxBtm > pt.Y && boxBtm < top)
|
|
top = boxBtm;
|
|
else if (box.Top < pt.Y && boxTop > btm)
|
|
btm = boxTop;
|
|
}
|
|
|
|
if (top == double.MaxValue)
|
|
{
|
|
if (bounds.Top > pt.Y)
|
|
top = bounds.Top;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
if (btm == double.MinValue)
|
|
{
|
|
if (bounds.Bottom < pt.Y)
|
|
btm = bounds.Bottom;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool FindHorizontalLimits(
|
|
Vector pt,
|
|
Box bounds,
|
|
List<Box> boxes,
|
|
out double lft,
|
|
out double rgt
|
|
)
|
|
{
|
|
lft = double.MinValue;
|
|
rgt = double.MaxValue;
|
|
|
|
foreach (var box in boxes)
|
|
{
|
|
var boxLft = box.Left;
|
|
var boxRgt = box.Right;
|
|
|
|
if (boxLft > pt.X && boxLft < rgt)
|
|
rgt = boxLft;
|
|
else if (boxRgt < pt.X && boxRgt > lft)
|
|
lft = boxRgt;
|
|
}
|
|
|
|
if (rgt == double.MaxValue)
|
|
{
|
|
if (bounds.Right > pt.X)
|
|
rgt = bounds.Right;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
if (lft == double.MinValue)
|
|
{
|
|
if (bounds.Left < pt.X)
|
|
lft = bounds.Left;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|