| | | 1 | | //======================================================================= |
| | | 2 | | // MixedEmbedded2DGeometry.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2026-present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using FixedMathSharp; |
| | | 9 | | using FixedMathSharp.Geometry; |
| | | 10 | | using Gravitas.Colliders; |
| | | 11 | | using System; |
| | | 12 | | |
| | | 13 | | namespace Gravitas.CollisionHandling; |
| | | 14 | | |
| | | 15 | | /// <summary> |
| | | 16 | | /// Shared fixed-point geometry helpers for embedded 2D mixed slabs. |
| | | 17 | | /// </summary> |
| | | 18 | | internal static class MixedEmbedded2DGeometry |
| | | 19 | | { |
| | | 20 | | public static Vector3d GetCenter3D(LSCollider2D embedded) => |
| | 7491 | 21 | | new(embedded.Center.X, embedded.MixedSlabCenterY, embedded.Center.Y); |
| | | 22 | | |
| | | 23 | | public static ContactAnchor GetSupportAnchor(LSCollider2D embedded, Vector3d worldDirection) |
| | | 24 | | { |
| | 534 | 25 | | Vector2d planarDirection = new(worldDirection.X, worldDirection.Z); |
| | 534 | 26 | | if (!TryGetPlanarSupportAnchor( |
| | 534 | 27 | | embedded, |
| | 534 | 28 | | planarDirection, |
| | 534 | 29 | | out ContactAnchor2D planarAnchor)) |
| | | 30 | | { |
| | 1 | 31 | | throw new InvalidOperationException( |
| | 1 | 32 | | "Committed embedded 2D geometry has no representable owner-relative support anchor."); |
| | | 33 | | } |
| | | 34 | | |
| | 533 | 35 | | Fixed64 yOffset = worldDirection.Y > Fixed64.Zero |
| | 533 | 36 | | ? embedded.MixedHalfThickness |
| | 533 | 37 | | : worldDirection.Y < Fixed64.Zero |
| | 533 | 38 | | ? -embedded.MixedHalfThickness |
| | 533 | 39 | | : Fixed64.Zero; |
| | 533 | 40 | | return EmbedPlanarAnchor( |
| | 533 | 41 | | planarAnchor, |
| | 533 | 42 | | embedded.MixedSlabCenterY, |
| | 533 | 43 | | yOffset); |
| | | 44 | | } |
| | | 45 | | |
| | | 46 | | public static ContactAnchor GetClosestAnchorOnEmbeddedVolume( |
| | | 47 | | LSCollider2D embedded, |
| | | 48 | | Vector3d point) |
| | | 49 | | { |
| | 1267 | 50 | | Vector2d planarPoint = new(point.X, point.Z); |
| | 1267 | 51 | | bool planarInside = embedded.ContainsPoint(planarPoint); |
| | 1267 | 52 | | GetClosestSlabOffset( |
| | 1267 | 53 | | point.Y, |
| | 1267 | 54 | | embedded.MixedSlabCenterY, |
| | 1267 | 55 | | embedded.MixedHalfThickness, |
| | 1267 | 56 | | out Fixed64 closestYOffset, |
| | 1267 | 57 | | out bool yInside, |
| | 1267 | 58 | | out Fixed64 signedCenterOffset); |
| | | 59 | | |
| | 1267 | 60 | | if (!planarInside || !yInside) |
| | | 61 | | { |
| | 1150 | 62 | | if (planarInside) |
| | | 63 | | { |
| | 504 | 64 | | return new ContactAnchor( |
| | 504 | 65 | | new Vector3d( |
| | 504 | 66 | | planarPoint.X, |
| | 504 | 67 | | embedded.MixedSlabCenterY, |
| | 504 | 68 | | planarPoint.Y), |
| | 504 | 69 | | new Vector3d( |
| | 504 | 70 | | Fixed64.Zero, |
| | 504 | 71 | | closestYOffset, |
| | 504 | 72 | | Fixed64.Zero)); |
| | | 73 | | } |
| | | 74 | | |
| | 646 | 75 | | if (TryGetPlanarBoundaryAnchor( |
| | 646 | 76 | | embedded, |
| | 646 | 77 | | planarPoint, |
| | 646 | 78 | | out ContactAnchor2D closestPlanar)) |
| | | 79 | | { |
| | 645 | 80 | | return EmbedPlanarAnchor( |
| | 645 | 81 | | closestPlanar, |
| | 645 | 82 | | embedded.MixedSlabCenterY, |
| | 645 | 83 | | closestYOffset); |
| | | 84 | | } |
| | | 85 | | |
| | 1 | 86 | | throw new InvalidOperationException( |
| | 1 | 87 | | "Committed embedded 2D geometry has no semantic boundary anchor."); |
| | | 88 | | } |
| | | 89 | | |
| | 117 | 90 | | bool upperYBoundary = signedCenterOffset > Fixed64.Zero; |
| | 117 | 91 | | Fixed64 yBoundaryOffset = upperYBoundary |
| | 117 | 92 | | ? embedded.MixedHalfThickness |
| | 117 | 93 | | : -embedded.MixedHalfThickness; |
| | 117 | 94 | | Fixed64 bestDistance = upperYBoundary |
| | 117 | 95 | | ? embedded.MixedHalfThickness - signedCenterOffset |
| | 117 | 96 | | : embedded.MixedHalfThickness + signedCenterOffset; |
| | 117 | 97 | | ContactAnchor closest = new( |
| | 117 | 98 | | new Vector3d( |
| | 117 | 99 | | planarPoint.X, |
| | 117 | 100 | | embedded.MixedSlabCenterY, |
| | 117 | 101 | | planarPoint.Y), |
| | 117 | 102 | | new Vector3d( |
| | 117 | 103 | | Fixed64.Zero, |
| | 117 | 104 | | yBoundaryOffset, |
| | 117 | 105 | | Fixed64.Zero)); |
| | | 106 | | |
| | | 107 | | // A point inside an admitted built-in shape is no farther from its |
| | | 108 | | // nearest boundary than that shape's representable dimensions. |
| | 117 | 109 | | _ = TryGetPlanarBoundaryAnchor( |
| | 117 | 110 | | embedded, |
| | 117 | 111 | | planarPoint, |
| | 117 | 112 | | out ContactAnchor2D planarBoundary, |
| | 117 | 113 | | out Fixed64 planarDistance); |
| | 117 | 114 | | if (planarDistance < bestDistance) |
| | | 115 | | { |
| | 36 | 116 | | closest = EmbedPlanarAnchor( |
| | 36 | 117 | | planarBoundary, |
| | 36 | 118 | | embedded.MixedSlabCenterY, |
| | 36 | 119 | | signedCenterOffset); |
| | | 120 | | } |
| | | 121 | | |
| | 117 | 122 | | return closest; |
| | | 123 | | } |
| | | 124 | | |
| | | 125 | | public static bool ContainsPointInSlab(LSCollider2D embedded, Fixed64 y) |
| | | 126 | | { |
| | 140 | 127 | | GetClosestSlabOffset( |
| | 140 | 128 | | y, |
| | 140 | 129 | | embedded.MixedSlabCenterY, |
| | 140 | 130 | | embedded.MixedHalfThickness, |
| | 140 | 131 | | out _, |
| | 140 | 132 | | out bool inside, |
| | 140 | 133 | | out _); |
| | 140 | 134 | | return inside; |
| | | 135 | | } |
| | | 136 | | |
| | | 137 | | public static bool TryGetPlanarBoundaryPoint( |
| | | 138 | | LSCollider2D embedded, |
| | | 139 | | Vector2d point, |
| | | 140 | | out Vector2d boundary, |
| | | 141 | | out Fixed64 distance) |
| | | 142 | | { |
| | 11 | 143 | | if (TryGetPlanarBoundaryAnchor( |
| | 11 | 144 | | embedded, |
| | 11 | 145 | | point, |
| | 11 | 146 | | out ContactAnchor2D anchor, |
| | 11 | 147 | | out distance) |
| | 11 | 148 | | && anchor.TryGetWorldPoint(out boundary)) |
| | | 149 | | { |
| | 9 | 150 | | return true; |
| | | 151 | | } |
| | | 152 | | |
| | 2 | 153 | | boundary = default; |
| | 2 | 154 | | return false; |
| | | 155 | | } |
| | | 156 | | |
| | | 157 | | public static bool TryGetPlanarBoundaryAnchor( |
| | | 158 | | LSCollider2D embedded, |
| | | 159 | | Vector2d point, |
| | | 160 | | out ContactAnchor2D boundary) |
| | | 161 | | { |
| | 646 | 162 | | if (embedded.TryGetClosestBoundaryAnchor( |
| | 646 | 163 | | point, |
| | 646 | 164 | | out FixedPointAnchor2d anchor)) |
| | | 165 | | { |
| | 645 | 166 | | boundary = new ContactAnchor2D(anchor); |
| | 645 | 167 | | return true; |
| | | 168 | | } |
| | | 169 | | |
| | 1 | 170 | | boundary = default; |
| | 1 | 171 | | return false; |
| | | 172 | | } |
| | | 173 | | |
| | | 174 | | public static bool TryGetPlanarBoundaryAnchor( |
| | | 175 | | LSCollider2D embedded, |
| | | 176 | | Vector2d point, |
| | | 177 | | out ContactAnchor2D boundary, |
| | | 178 | | out Fixed64 distance) |
| | | 179 | | { |
| | 134 | 180 | | if (embedded.TryGetClosestBoundaryAnchor( |
| | 134 | 181 | | point, |
| | 134 | 182 | | out FixedPointAnchor2d anchor, |
| | 134 | 183 | | out distance)) |
| | | 184 | | { |
| | 130 | 185 | | boundary = new ContactAnchor2D(anchor); |
| | 130 | 186 | | return true; |
| | | 187 | | } |
| | | 188 | | |
| | 4 | 189 | | boundary = default; |
| | 4 | 190 | | return false; |
| | | 191 | | } |
| | | 192 | | |
| | | 193 | | private static bool TryGetPlanarSupportAnchor( |
| | | 194 | | LSCollider2D embedded, |
| | | 195 | | Vector2d direction, |
| | | 196 | | out ContactAnchor2D anchor) |
| | | 197 | | { |
| | 534 | 198 | | switch (embedded.Shape) |
| | | 199 | | { |
| | | 200 | | case ColliderType2D.Circle: |
| | | 201 | | { |
| | 223 | 202 | | var circle = (LSCircleCollider2D)embedded; |
| | 223 | 203 | | return TryGetRoundSupportAnchor( |
| | 223 | 204 | | circle.Center, |
| | 223 | 205 | | circle.Rotation, |
| | 223 | 206 | | Fixed64.Zero, |
| | 223 | 207 | | circle.ScaledRadius, |
| | 223 | 208 | | direction, |
| | 223 | 209 | | out anchor); |
| | | 210 | | } |
| | | 211 | | case ColliderType2D.Capsule: |
| | | 212 | | { |
| | 307 | 213 | | var capsule = (LSCapsuleCollider2D)embedded; |
| | 307 | 214 | | return TryGetRoundSupportAnchor( |
| | 307 | 215 | | capsule.Center, |
| | 307 | 216 | | capsule.Rotation, |
| | 307 | 217 | | capsule.AxisLength, |
| | 307 | 218 | | capsule.ScaledRadius, |
| | 307 | 219 | | direction, |
| | 307 | 220 | | out anchor); |
| | | 221 | | } |
| | | 222 | | case ColliderType2D.AABox: |
| | | 223 | | case ColliderType2D.ConvexPolygon: |
| | 3 | 224 | | anchor = new ContactAnchor2D( |
| | 3 | 225 | | embedded.GetConvexSupportAnchor( |
| | 3 | 226 | | direction == Vector2d.Zero |
| | 3 | 227 | | ? Vector2d.Right |
| | 3 | 228 | | : direction)); |
| | 3 | 229 | | return true; |
| | | 230 | | default: |
| | 1 | 231 | | anchor = default; |
| | 1 | 232 | | return false; |
| | | 233 | | } |
| | | 234 | | } |
| | | 235 | | |
| | | 236 | | private static bool TryGetRoundSupportAnchor( |
| | | 237 | | Vector2d center, |
| | | 238 | | Fixed64 rotation, |
| | | 239 | | Fixed64 axisLength, |
| | | 240 | | Fixed64 radius, |
| | | 241 | | Vector2d direction, |
| | | 242 | | out ContactAnchor2D anchor) |
| | | 243 | | { |
| | 530 | 244 | | Vector2d localDirection = direction == Vector2d.Zero |
| | 530 | 245 | | ? Vector2d.Zero |
| | 530 | 246 | | : Vector2d.Rotate(direction.Normalized, -rotation); |
| | | 247 | | |
| | 530 | 248 | | FixedPointAnchor2d localAnchor = |
| | 530 | 249 | | FixedSegment2d.GetCenteredCapsuleSupportAnchor( |
| | 530 | 250 | | center, |
| | 530 | 251 | | rotation, |
| | 530 | 252 | | Vector2d.Forward, |
| | 530 | 253 | | axisLength, |
| | 530 | 254 | | radius, |
| | 530 | 255 | | localDirection); |
| | 530 | 256 | | anchor = new ContactAnchor2D(localAnchor); |
| | 530 | 257 | | return true; |
| | | 258 | | } |
| | | 259 | | |
| | | 260 | | private static ContactAnchor EmbedPlanarAnchor( |
| | | 261 | | ContactAnchor2D planar, |
| | | 262 | | Fixed64 slabCenterY, |
| | | 263 | | Fixed64 localYDisplacement = default) |
| | | 264 | | { |
| | 1214 | 265 | | return new ContactAnchor( |
| | 1214 | 266 | | new Vector3d(planar.Origin.X, slabCenterY, planar.Origin.Y), |
| | 1214 | 267 | | FixedQuaternion.FromAxisAngle(Vector3d.Up, -planar.Rotation), |
| | 1214 | 268 | | new Vector3d( |
| | 1214 | 269 | | planar.LocalPoint.X, |
| | 1214 | 270 | | Fixed64.Zero, |
| | 1214 | 271 | | planar.LocalPoint.Y), |
| | 1214 | 272 | | new Vector3d( |
| | 1214 | 273 | | planar.LocalDisplacement.X, |
| | 1214 | 274 | | localYDisplacement, |
| | 1214 | 275 | | planar.LocalDisplacement.Y)); |
| | | 276 | | } |
| | | 277 | | |
| | | 278 | | private static void GetClosestSlabOffset( |
| | | 279 | | Fixed64 y, |
| | | 280 | | Fixed64 slabCenterY, |
| | | 281 | | Fixed64 halfThickness, |
| | | 282 | | out Fixed64 closestOffset, |
| | | 283 | | out bool inside, |
| | | 284 | | out Fixed64 signedCenterOffset) |
| | | 285 | | { |
| | 1407 | 286 | | if (!Fixed64.TrySubtract(y, slabCenterY, out signedCenterOffset)) |
| | | 287 | | { |
| | 3 | 288 | | closestOffset = y < slabCenterY |
| | 3 | 289 | | ? -halfThickness |
| | 3 | 290 | | : halfThickness; |
| | 3 | 291 | | inside = false; |
| | 3 | 292 | | signedCenterOffset = closestOffset; |
| | 3 | 293 | | return; |
| | | 294 | | } |
| | | 295 | | |
| | 1404 | 296 | | if (signedCenterOffset < -halfThickness) |
| | | 297 | | { |
| | 11 | 298 | | closestOffset = -halfThickness; |
| | 11 | 299 | | inside = false; |
| | 11 | 300 | | return; |
| | | 301 | | } |
| | | 302 | | |
| | 1393 | 303 | | if (signedCenterOffset > halfThickness) |
| | | 304 | | { |
| | 560 | 305 | | closestOffset = halfThickness; |
| | 560 | 306 | | inside = false; |
| | 560 | 307 | | return; |
| | | 308 | | } |
| | | 309 | | |
| | 833 | 310 | | closestOffset = signedCenterOffset; |
| | 833 | 311 | | inside = true; |
| | 833 | 312 | | } |
| | | 313 | | } |