using System.Collections.Generic; using System.Linq; using OpenNest.Math; namespace OpenNest.Geometry { public static class SpatialQuery { /// /// 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. /// 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; } } /// /// Generalized ray-edge distance along an arbitrary unit direction vector. /// Returns double.MaxValue if the ray does not hit the segment. /// [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; } /// /// Solves ray-circle intersection, returning the two parametric t values. /// Returns false if no real intersection exists. /// [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; } /// /// 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. /// [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( System.Math.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( System.Math.Atan2(vy + t2 * dirY - cy, vx + t2 * dirX - cx) ); if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) best = t2 > Tolerance.Epsilon ? t2 : 0; } return best; } /// /// Computes the distance from a point along a direction to a full circle. /// Returns double.MaxValue if no hit. /// [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; } /// /// 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. /// 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) { // A zero-radius curve is a point: its angular range has no geometric meaning. if (arc == null || radius == 0) return true; var angle = Angle.NormalizeRad(System.Math.Atan2(y, x)); return Angle.IsBetweenRad(angle, arc.StartAngle, arc.EndAngle, arc.IsReversed); } /// /// 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 ). /// Returns double.MaxValue if no collision path exists. /// public static double DirectionalDistance( List movingLines, List stationaryLines, PushDirection direction ) { return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction); } /// /// Computes the directional distance with the moving lines translated /// by (movingDx, movingDy) without creating new Line objects. /// public static double DirectionalDistance( List movingLines, double movingDx, double movingDy, List stationaryLines, PushDirection direction ) { return DirectionalDistance( movingLines, movingDx, movingDy, stationaryLines, direction, SlideContactClassifier.FromLines( movingLines, new Vector(movingDx, movingDy), stationaryLines, Vector.Zero ) ); } /// /// /// with caller-supplied contact topology, for inputs that are not complete closed /// boundaries (for example direction-filtered edges). /// public static double DirectionalDistance( List movingLines, double movingDx, double movingDy, List 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); } /// /// Packs line segments into a flat double array [x1,y1,x2,y2, ...] for GPU transfer. /// public static double[] FlattenLines(List 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; } /// /// Computes the blocking directional distance using raw edge arrays and location /// offsets. Sorts both edge arrays in place for pruning. /// 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 ) ); } /// /// Edge-array overload with caller-supplied contact topology. The classifier's /// origins must match and /// in the frame of its boundaries. /// 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); } /// /// 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. /// 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 /// /// Computes how far a box can travel along the given unit direction /// before exiting the boundary box. /// 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; } /// /// Computes the directional gap between two boxes along an arbitrary unit direction. /// Positive means 'to' is ahead of 'from' in the push direction. /// 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; } /// /// Returns true if two boxes overlap when projected onto the axis /// perpendicular to the given unit direction. /// 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; } /// /// Computes the translation distance along an arbitrary unit direction before any /// edge of movingLines first blocks against an edge of stationaryLines. /// public static double DirectionalDistance( List movingLines, List stationaryLines, Vector direction ) { return DirectionalDistance( movingLines, stationaryLines, direction, SlideContactClassifier.FromLines( movingLines, Vector.Zero, stationaryLines, Vector.Zero ) ); } /// /// with /// caller-supplied contact topology. /// public static double DirectionalDistance( List movingLines, List 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); } /// /// 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. /// public static double DirectionalDistance( List movingEntities, List stationaryEntities, PushDirection direction ) { return DirectionalDistance( movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0) ); } /// /// 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 ). /// public static double DirectionalDistance( List movingEntities, List stationaryEntities, Vector direction ) { return DirectionalDistance( movingEntities, stationaryEntities, direction, new SlideContactClassifier(movingEntities, stationaryEntities) ); } /// /// with /// caller-supplied contact topology. /// public static double DirectionalDistance( List movingEntities, List stationaryEntities, 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, ExtractEntityVertices(movingEntities), 0, 0, stationaryEntities, ExtractEntityVertices(stationaryEntities), direction.X, direction.Y, arcToLine: true ); return SlideResolver.FirstBlocking(ref source, contacts, direction.X, direction.Y); } public static Vector[] ExtractEntityVertices(List entities) { var vertices = new HashSet(); 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(); } private static void AddArcExtremeVertices(HashSet 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 CollectVertices(List lines, Vector offset) { return CollectVertices(ToEdgeArray(lines), offset); } private static HashSet CollectVertices( (Vector start, Vector end)[] edges, Vector offset ) { var vertices = new HashSet(); 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 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 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 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 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 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; } } }